alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
753 lines (752 loc) • 29.2 kB
JavaScript
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