UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

753 lines (752 loc) 29.2 kB
import { $context, $inject, $module, $state, AlephaError, z } from "alepha"; import { AlephaCli, AppEntryProvider, ViteBuildProvider } from "alepha/cli"; import { AlephaPlatformLibPlugin, CloudflareAdapter, GitHubSecretStore, NamingService, PlatformInspector, PlatformOrchestrator, SecretFilterService, VercelAdapter, platformOptions, resolveTenant } from "alepha/cli/platform-lib"; import { $command, EnvUtils } from "alepha/command"; import { $logger, ConsoleColorProvider } from "alepha/logger"; //#region ../../src/cli/platform/commands/SecretsCommand.ts var SecretsCommand = class { log = $logger(); options = $state(platformOptions); inspector = $inject(PlatformInspector); naming = $inject(NamingService); envUtils = $inject(EnvUtils); githubStore = $inject(GitHubSecretStore); filter = $inject(SecretFilterService); color = $inject(ConsoleColorProvider); envFlags = z.object({ env: z.text({ aliases: ["e"], description: "Target environment" }).optional() }); list = $command({ name: "list", aliases: ["ls"], description: "List secret names stored in the remote store for an environment (values are never returned). Use --format=gha to emit a ready-to-paste GitHub Actions env: block.", flags: z.object({ env: z.text({ aliases: ["e"], description: "Target environment" }).optional(), format: z.text({ aliases: ["f"], description: "Output format: table (default), gha" }).optional() }), handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envName = this.resolveEnvironmentName(config.project, env); const format = flags.format ?? "table"; const store = this.resolveStore(); await store.ensureAvailable(); const remoteSecrets = await store.list(envName); run.end(); if (format === "gha") { process.stdout.write(`# Generated by alepha — environment: ${envName}\n`); process.stdout.write("env:\n"); for (const secret of remoteSecrets) { const localName = this.filter.toLocalName(secret.name); process.stdout.write(` ${localName}: \${{ secrets.${secret.name} }}\n`); } return; } const c = this.color; if (remoteSecrets.length === 0) { process.stdout.write(`\nNo secrets in environment ${c.set("CYAN", envName)}.\n\n`); return; } process.stdout.write(`\n${c.set("GREY_LIGHT", "Environment:")} ${c.set("CYAN", envName)}\n\n`); const maxNameLen = Math.max(...remoteSecrets.map((s) => s.name.length), 4); process.stdout.write(` ${c.set("GREY_LIGHT", "NAME".padEnd(maxNameLen + 2))}${c.set("GREY_LIGHT", "UPDATED")}\n`); for (const secret of remoteSecrets) { const updated = secret.updatedAt ? new Date(secret.updatedAt).toLocaleString() : "-"; process.stdout.write(` ${c.set("CYAN", secret.name.padEnd(maxNameLen + 2))}${c.set("GREY_DARK", updated)}\n`); } process.stdout.write("\n"); } }); diff = $command({ name: "diff", description: "Compare keys in local .env.<env> against the remote store. Shows + (only local, would be pushed), - (only remote, orphaned), = (in sync). Compares names only — values are not read from remote.", flags: this.envFlags, handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envName = this.resolveEnvironmentName(config.project, env); const envVars = await this.envUtils.parseEnv(root, [`.env.${env}`]); const filtered = this.filter.filter(envVars); const localKeys = new Set(Object.keys(filtered).map((k) => this.filter.toRemoteName(k))); const store = this.resolveStore(); await store.ensureAvailable(); const remoteSecrets = await store.list(envName); const remoteKeys = new Set(remoteSecrets.map((s) => s.name)); run.end(); const c = this.color; const allKeys = [.../* @__PURE__ */ new Set([...localKeys, ...remoteKeys])].sort(); process.stdout.write(`\n${c.set("GREY_LIGHT", "Environment:")} ${c.set("CYAN", envName)}\n\n`); let hasChanges = false; for (const key of allKeys) { const inLocal = localKeys.has(key); const inRemote = remoteKeys.has(key); if (inLocal && !inRemote) { process.stdout.write(` ${c.set("GREEN", "+")} ${key}\n`); hasChanges = true; } else if (!inLocal && inRemote) { process.stdout.write(` ${c.set("RED", "-")} ${key}\n`); hasChanges = true; } else process.stdout.write(` ${c.set("GREY_DARK", "=")} ${key}\n`); } if (!hasChanges) process.stdout.write(` ${c.set("GREEN", "Local and remote are in sync.")}\n`); process.stdout.write("\n"); } }); apply = $command({ name: "apply", description: "Push secrets from local .env.<env> to the remote store (one upsert per key). Use --dry-run to preview. Existing remote keys not present locally are left untouched — this command never deletes.", flags: z.object({ env: z.text({ aliases: ["e"], description: "Target environment" }).optional(), "dry-run": z.boolean().describe("Preview changes without pushing").optional() }), handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envName = this.resolveEnvironmentName(config.project, env); const dryRun = flags["dry-run"] ?? false; const envVars = await this.envUtils.parseEnv(root, [`.env.${env}`]); const filtered = this.filter.filter(envVars); const keys = Object.keys(filtered); if (keys.length === 0) { run.end(); process.stdout.write("\nNo secrets to push.\n\n"); return; } if (dryRun) { run.end(); const c = this.color; process.stdout.write(`\n${c.set("GREY_LIGHT", "Dry run — environment:")} ${c.set("CYAN", envName)}\n\n`); for (const key of keys) process.stdout.write(` ${c.set("GREEN", "+")} ${key}\n`); process.stdout.write(`\n ${keys.length} secret(s) would be pushed.\n\n`); return; } const store = this.resolveStore(); await store.ensureAvailable(); await store.ensureEnvironment(envName); for (const key of keys) { const remoteName = this.filter.toRemoteName(key); await run({ name: `push ${remoteName}`, handler: async () => { await store.set(envName, remoteName, filtered[key]); } }); } run.end(); process.stdout.write(`\n${keys.length} secret(s) pushed to ${envName}.\n`); } }); secrets = $command({ name: "secrets", aliases: ["sec"], description: "Sync secrets from .env.<env> to an external CI store (currently GitHub Actions environments via the `gh` CLI). Resolves the target environment name from `secrets.environmentPattern` (default `{project}-{env}`) and filters keys via SecretFilterService. Runtime secrets for Cloudflare Workers / Vercel are pushed separately during `alepha platform up`.", children: [ this.list, this.diff, this.apply ], handler: async ({ help }) => { help(); } }); resolveStore() { const storeName = this.options?.secrets?.store ?? "github"; switch (storeName) { case "github": return this.githubStore; default: throw new AlephaError(`Unknown secret store "${storeName}". Available: github`); } } resolveEnvironmentName(project, env) { return (this.options?.secrets?.environmentPattern ?? "{project}-{env}").replace("{project}", this.naming.slugify(project)).replace("{env}", this.naming.slugify(env)); } }; //#endregion //#region ../../src/cli/platform/commands/platform.ts var PlatformCommand = class { log = $logger(); options = $state(platformOptions); orchestrator = $inject(PlatformOrchestrator); inspector = $inject(PlatformInspector); naming = $inject(NamingService); boot = $inject(AppEntryProvider); viteBuild = $inject(ViteBuildProvider); color = $inject(ConsoleColorProvider); envUtils = $inject(EnvUtils); secretsCommand = $inject(SecretsCommand); /** * Common flags for env targeting. */ envFlags = z.object({ env: z.text({ aliases: ["e"], description: "Target environment" }).optional(), tenant: z.text({ description: "Tenant slug (apps with tenancy: optional | required). Names resources <tenant>-<project>-<env> and serves <tenant>.<domain>." }).optional(), verbose: z.boolean().meta({ aliases: ["v"] }).describe("Verbose output").optional(), json: z.boolean().describe("Output as JSON").optional() }); plan = $command({ name: "plan", description: "Show project topology and resource names", flags: this.envFlags, handler: async ({ flags, root }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const adapterName = config.environments[env]?.adapter ?? "cloudflare"; const apps = await this.resolveApps(root, config, this.isServerless(adapterName)); const tenant = resolveTenant(config.tenancy, flags.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const hasDB = apps.some((a) => a.resources.hasDatabase); const hasBucket = apps.some((a) => a.resources.hasBucket); const envVars = await this.envUtils.parseEnv(root, [`.env.${env}`]); const resources = []; const deployLabel = adapterName === "vercel" ? "Project" : "Worker"; resources.push({ label: deployLabel, value: namingCtx.worker() }); if (adapterName === "cloudflare") { if (hasDB) if ((envVars.DATABASE_URL ?? process.env.DATABASE_URL)?.startsWith("postgres:")) resources.push({ label: "Hyperdrive", value: namingCtx.hyperdrive() }); else resources.push({ label: "D1", value: namingCtx.d1() }); if (hasBucket) resources.push({ label: "R2", value: namingCtx.r2() }); for (const app of apps) { if (app.resources.hasKV) resources.push({ label: "KV", value: namingCtx.kv() }); if (app.resources.hasQueue) resources.push({ label: "Queue", value: namingCtx.queue() }); } } const excludedKeys = adapterName === "vercel" ? VercelAdapter.EXCLUDED_SECRET_KEYS : CloudflareAdapter.EXCLUDED_SECRET_KEYS; const secretCount = Object.entries(envVars).filter(([key, value]) => value && !excludedKeys.has(key) && !key.startsWith("VITE_")).length; if (flags.json) { const environments = {}; for (const [key, val] of Object.entries(config.environments)) environments[key] = { adapter: val.adapter, ...val.domain ? { domain: val.domain } : {} }; const output = { project: config.project, env, mode: "standalone", apps: apps.map((a) => ({ name: a.name, path: a.path, resources: a.resources })), environments, resources, secretCount }; process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); return; } const c = this.color; process.stdout.write(`\n\u{1F4E6} ${c.set("WHITE_BOLD", config.project)} ${c.set("GREY_DARK", "—")} ${c.set("CYAN", env)}\n\n`); process.stdout.write(` ${c.set("GREY_LIGHT", "Mode:")} standalone\n`); process.stdout.write(`\n ${c.set("GREY_LIGHT", "Environments:")}\n`); const envKeys = Object.keys(config.environments); for (const [i, envKey] of envKeys.entries()) { const envConfig = config.environments[envKey]; const prefix = i === envKeys.length - 1 ? "└──" : "├──"; const domain = envConfig.domain ? ` ${c.set("GREY_DARK", envConfig.domain)}` : ""; const marker = envKey === env ? ` ${c.set("GREEN", "◀")}` : ""; process.stdout.write(` ${c.set("GREY_DARK", prefix)} ${c.set("CYAN", envKey.padEnd(10))} ${c.set("GREY_LIGHT", envConfig.adapter)}${domain}${marker}\n`); } process.stdout.write(`\n ${c.set("GREY_LIGHT", "Resources:")}\n`); if (secretCount > 0) resources.push({ label: "Secrets", value: `${secretCount} from .env.${env}` }); for (const [i, res] of resources.entries()) { const branch = i === resources.length - 1 ? "└──" : "├──"; process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("GREY_LIGHT", res.label.padEnd(11))} ${c.set("CYAN", res.value)}\n`); } process.stdout.write("\n"); } }); up = $command({ name: "up", mode: "production", description: "Build, migrate, and deploy", flags: z.object({ ...this.envFlags.properties, prebuilt: z.boolean().describe("Pre-built mode. Skips the Vite bundle steps; only regenerates the target deploy config (wrangler.jsonc) so it reflects current bindings and per-tenant overrides. Use when `dist/` is already produced upstream (e.g. inside Alepha Rocket).").optional() }), handler: async ({ flags, root, run }) => { process.env.NODE_ENV = "production"; const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const adapter = config.environments[env]?.adapter ?? "cloudflare"; const apps = await this.resolveApps(root, config, this.isServerless(adapter), { prebuilt: flags.prebuilt }); const result = await this.orchestrator.up({ root, env, entry: apps[0].entry, resources: apps[0].resources, run, prebuilt: flags.prebuilt, tenant: flags.tenant }); if (flags.json) process.stdout.write(`${JSON.stringify({ status: "succeeded", project: config.project, env, urls: result.urls, domain: result.domain }, null, 2)}\n`); else this.orchestrator.printUpSummary(result); } }); down = $command({ name: "down", description: "Tear down an environment", flags: z.object({ ...this.envFlags.properties, yes: z.boolean().meta({ aliases: ["y"] }).describe("Skip the interactive confirmation. Required for non-interactive callers (CI, Alepha Rocket). The caller is responsible for not invoking this accidentally — there's no second chance.").optional() }), handler: async ({ flags, root, run, ask }) => { if (!flags.env) throw new AlephaError("--env is required for teardown. This command deletes resources."); const config = await this.inspector.resolveConfig(root); const adapter = config.environments[flags.env]?.adapter ?? "cloudflare"; const apps = await this.resolveApps(root, config, this.isServerless(adapter)); const completed = await this.orchestrator.down({ root, env: flags.env, entry: apps[0].entry, resources: apps[0].resources, run, tenant: flags.tenant, confirm: async (prompt) => { if (flags.yes) return flags.env; ask.intro("Confirm teardown"); const value = await ask(prompt); ask.outro(""); return value; } }); if (flags.json) process.stdout.write(`${JSON.stringify({ status: completed ? "succeeded" : "aborted", project: config.project, env: flags.env }, null, 2)}\n`); } }); status = $command({ name: "status", aliases: ["s"], description: "Show deployed state", flags: this.envFlags, handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const adapter = config.environments[env]?.adapter ?? "cloudflare"; const apps = await this.resolveApps(root, config, this.isServerless(adapter)); const { state } = await this.orchestrator.status({ root, env, entry: apps[0].entry, resources: apps[0].resources, run, tenant: flags.tenant }); if (flags.json) { run.end(); const output = { project: config.project, env, adapter: config.environments[env].adapter, ...state }; process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); return; } run.end(); const c = this.color; process.stdout.write(`\n\u{1F4E6} ${c.set("WHITE_BOLD", config.project)} ${c.set("GREY_DARK", "—")} ${c.set("CYAN", env)} ${c.set("GREY_DARK", `(${config.environments[env].adapter})`)}\n\n`); const hasDB = state.databases.length > 0; const hasBuckets = state.buckets.length > 0; process.stdout.write(` ${c.set("GREY_LIGHT", "Workers:")}\n`); for (const [i, w] of state.workers.entries()) { const branch = i === state.workers.length - 1 ? "└──" : "├──"; if (w.exists) { const versionShort = w.version?.slice(0, 8) ?? "unknown"; const tag = w.tag ? ` ${c.set("GREY_DARK", `(${w.tag})`)}` : ""; const date = w.createdAt ? ` ${c.set("GREY_DARK", "—")} ${c.set("GREY_DARK", new Date(w.createdAt).toLocaleString())}` : ""; process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", w.name)} ${c.set("GREEN", "✓")} ${c.set("GREY_LIGHT", versionShort)}${tag}${date}\n`); } else process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", w.name)} ${c.set("RED", "✗")} ${c.set("RED", "not deployed")}\n`); } if (hasDB) { const dbLabel = ((await this.envUtils.parseEnv(root, [`.env.${env}`])).DATABASE_URL ?? process.env.DATABASE_URL)?.startsWith("postgres:") ? "Hyperdrive:" : "Database:"; process.stdout.write(`\n ${c.set("GREY_LIGHT", dbLabel)}\n`); for (const [i, db] of state.databases.entries()) { const branch = i === state.databases.length - 1 ? "└──" : "├──"; if (db.exists) { const id = db.id ? ` ${c.set("GREY_LIGHT", db.id.slice(0, 8))}` : ""; const detail = db.detail ? ` ${c.set("GREY_DARK", "—")} ${c.set("GREY_DARK", db.detail.slice(0, 40))}` : ""; process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", db.name)} ${c.set("GREEN", "✓")}${id}${detail}\n`); } else process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", db.name)} ${c.set("RED", "✗")} ${c.set("RED", "not provisioned")}\n`); } } if (hasBuckets) { process.stdout.write(`\n ${c.set("GREY_LIGHT", "Buckets:")}\n`); for (const [i, b] of state.buckets.entries()) { const branch = i === state.buckets.length - 1 ? "└──" : "├──"; if (b.exists) process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", b.name)} ${c.set("GREEN", "✓")}\n`); else process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", b.name)} ${c.set("RED", "✗")} ${c.set("RED", "not provisioned")}\n`); } } if (state.kvNamespaces.length > 0) { process.stdout.write(`\n ${c.set("GREY_LIGHT", "KV:")}\n`); for (const [i, kv] of state.kvNamespaces.entries()) { const branch = i === state.kvNamespaces.length - 1 ? "└──" : "├──"; if (kv.exists) { const id = kv.id ? ` ${c.set("GREY_LIGHT", kv.id.slice(0, 8))}` : ""; process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", kv.name)} ${c.set("GREEN", "✓")}${id}\n`); } else process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", kv.name)} ${c.set("RED", "✗")} ${c.set("RED", "not provisioned")}\n`); } } if (state.queues.length > 0) { process.stdout.write(`\n ${c.set("GREY_LIGHT", "Queues:")}\n`); for (const [i, q] of state.queues.entries()) { const branch = i === state.queues.length - 1 ? "└──" : "├──"; if (q.exists) { const id = q.id ? ` ${c.set("GREY_LIGHT", q.id.slice(0, 8))}` : ""; process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", q.name)} ${c.set("GREEN", "✓")}${id}\n`); } else process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", q.name)} ${c.set("RED", "✗")} ${c.set("RED", "not provisioned")}\n`); } } if (state.secrets.length > 0) { process.stdout.write(`\n ${c.set("GREY_LIGHT", "Secrets:")}\n`); for (const [i, s] of state.secrets.entries()) { const branch = i === state.secrets.length - 1 ? "└──" : "├──"; const icon = s.deployed ? c.set("GREEN", "✓") : c.set("RED", "✗"); process.stdout.write(` ${c.set("GREY_DARK", branch)} ${c.set("CYAN", s.name)} ${icon}\n`); } } process.stdout.write("\n"); } }); build = $command({ name: "build", mode: "production", description: "Build all apps locally", flags: this.envFlags, handler: async ({ flags, root, run }) => { process.env.NODE_ENV = "production"; const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envConfig = config.environments[env]; const adapter = this.orchestrator.resolveAdapter(envConfig.adapter); const apps = await this.resolveApps(root, config, this.isServerless(envConfig.adapter)); const tenant = resolveTenant(config.tenancy, flags.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx = { project: config.project, env, envConfig, entry: apps[0].entry, resources: apps[0].resources, root, naming: namingCtx, tenant }; await adapter.build(ctx, run); } }); deploy = $command({ name: "deploy", description: "Deploy apps to cloud", flags: this.envFlags, handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envConfig = config.environments[env]; const adapter = this.orchestrator.resolveAdapter(envConfig.adapter); const apps = await this.resolveApps(root, config, this.isServerless(envConfig.adapter)); const tenant = resolveTenant(config.tenancy, flags.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx = { project: config.project, env, envConfig, entry: apps[0].entry, resources: apps[0].resources, root, naming: namingCtx, tenant }; await adapter.authenticate(ctx, run); await adapter.deploy(ctx, run); } }); migrate = $command({ name: "migrate", description: "Run database migrations", flags: this.envFlags, handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envConfig = config.environments[env]; const adapter = this.orchestrator.resolveAdapter(envConfig.adapter); const apps = await this.resolveApps(root, config, this.isServerless(envConfig.adapter)); const tenant = resolveTenant(config.tenancy, flags.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx = { project: config.project, env, envConfig, entry: apps[0].entry, resources: apps[0].resources, root, naming: namingCtx, tenant }; await adapter.authenticate(ctx, run); await adapter.migrate(ctx, run); if (flags.json) process.stdout.write(`${JSON.stringify({ status: "succeeded", project: config.project, env }, null, 2)}\n`); } }); dbExport = $command({ name: "export", description: "Export the deployed database to a local snapshot (remote → local dev DB).", flags: z.object({ ...this.envFlags.properties, output: z.text({ description: "Destination SQLite file. Defaults to the dev DB path (node_modules/.alepha/sqlite.db)." }).optional(), keepSql: z.boolean().describe("Keep the intermediate .sql dump file.").optional() }), handler: async ({ flags, root, run }) => { const config = await this.inspector.resolveConfig(root); const env = flags.env ?? config.defaultEnv; const envConfig = config.environments[env]; const adapter = this.orchestrator.resolveAdapter(envConfig.adapter); const app = await this.resolveApp(root, config, this.isServerless(envConfig.adapter)); const tenant = resolveTenant(config.tenancy, flags.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx = { project: config.project, env, envConfig, entry: app.entry, resources: app.resources, root, naming: namingCtx, tenant }; await adapter.authenticate(ctx, run); await adapter.exportDb(ctx, run, { output: flags.output, keepSql: flags.keepSql }); } }); /** * `db` subgroup — operations against the *deployed* database (export, * migrate). They live under `platform` (not core `alepha db`) because * they need the env config, tenancy, adapter, and resource naming. */ db = $command({ name: "db", description: "Deployed-database operations (export, migrate).", children: [this.dbExport, this.migrate], handler: async ({ help, root }) => { await this.inspector.resolveConfig(root); help(); } }); platform = $command({ name: "platform", aliases: ["p"], description: "Cloud deployment orchestrator", children: [ this.plan, this.up, this.down, this.status, this.build, this.deploy, this.db, this.secretsCommand.secrets ], handler: async ({ help, root }) => { await this.inspector.resolveConfig(root); help(); } }); /** * Resolve app definitions. * * For standalone: returns a single app from the root. * For monorepo: resolves each app path, introspects for resources. * * NOTE: Resource detection (hasDatabase, hasBucket, etc.) requires * ViteBuildProvider.init() per app. This is expensive -- only done * for up/down/status, not for plan. */ async resolveApp(root, _config, isServerless, options = {}) { if (options.prebuilt) { const manifest = await this.readManifest(root); if (manifest) return { entry: { root, server: "" }, resources: manifest.resources }; } const entry = await this.boot.getAppEntry(root); if (isServerless) process.env.ALEPHA_SERVERLESS = "true"; const appAlepha = await this.viteBuild.init({ entry }); delete process.env.ALEPHA_SERVERLESS; return { entry, resources: this.detectResources(appAlepha) }; } /** * @deprecated single-app projects; use `resolveApp` directly. * Kept temporarily so existing commands can be migrated one at a time. */ async resolveApps(root, config, isServerless, options = {}) { const { entry, resources } = await this.resolveApp(root, config, isServerless, options); return [{ name: config.project, path: "", entry, resources }]; } isServerless(adapter) { return adapter === "vercel" || adapter === "cloudflare"; } /** * Read `dist/manifest.json` if present. Returns `null` when the file * doesn't exist or isn't parseable — caller falls back to the * Vite-introspection path. */ async readManifest(root) { try { const fs = await import("node:fs/promises"); const path = await import("node:path"); const raw = await fs.readFile(path.join(root, "dist", "manifest.json"), "utf-8"); return JSON.parse(raw); } catch { return null; } } detectResources(alepha) { let hasDatabase = false; let hasBucket = false; let hasKV = false; let hasQueue = false; let hasCron = false; try { hasDatabase = alepha.inject("RepositoryProvider").getRepositories().length > 0; } catch {} try { hasBucket = alepha.primitives("$bucket").length > 0; } catch {} try { hasKV = alepha.primitives("cache").filter((it) => it.options?.provider == null).length > 0; } catch {} try { hasQueue = alepha.primitives("queue").length > 0; } catch {} try { hasCron = alepha.inject("CronProvider").getCronJobs().length > 0; } catch {} return { hasDatabase, hasBucket, hasKV, hasQueue, hasCron }; } }; //#endregion //#region ../../src/cli/platform/index.ts /** * CLI plugin for multi-cloud deployment orchestration. * * Wraps `AlephaPlatformLibPlugin` (the framework-agnostic deploy * services) with `$command` instances so the orchestration is * reachable from `alepha platform …`. Non-CLI consumers (e.g. Alepha * Rocket) should depend on `alepha/cli/platform-lib` directly instead * of pulling in this command surface. * * Commands: * - `alepha platform plan` — show project topology and resource names * - `alepha platform up` — full deployment pipeline * - `alepha platform down` — teardown an environment * - `alepha platform status` — inspect deployed resources * - `alepha platform build` — build apps locally * - `alepha platform deploy` — deploy to cloud * - `alepha platform db migrate` — run database migrations * - `alepha platform db export` — pull the deployed DB into a local snapshot * - `alepha platform secrets` — manage external secret stores * * Configuration in `alepha.config.ts`: * * ```typescript * import { platform } from "alepha/cli/platform"; * * export default defineConfig({ * plugins: [ * platform({ * environments: { * production: { adapter: "cloudflare", domain: "myapp.com" }, * }, * }), * ], * }); * ``` */ const AlephaCliPlatformPlugin = $module({ name: "alepha.cli.plugins.platform", services: [ AlephaCli, AlephaPlatformLibPlugin, PlatformCommand, SecretsCommand ] }); const platform = (options) => { if (!process.env.PUBLIC_URL) { const productionDomain = options?.environments?.production?.domain; if (productionDomain) process.env.PUBLIC_URL = `https://${productionDomain}`; } return () => { const { alepha } = $context(); alepha.with(AlephaCliPlatformPlugin).set(platformOptions, options); }; }; //#endregion export { AlephaCliPlatformPlugin, PlatformCommand, SecretsCommand, platform }; //# sourceMappingURL=index.js.map