@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
620 lines (614 loc) • 26.9 kB
JavaScript
import { existsSync, readFileSync } from 'node:fs';
import { runCodegen, discoverMigrations } from '@lunora/codegen';
import { readLinkedProject, writeLinkedProject, validateWranglerProject, inferLunoraBindings, reconcileWranglerBindings, reconcileWranglerCompatibilityDate, reconcileWranglerCrons, DEV_VARS_FILE, parseDevVariableEntries, isMintableSecretKey, findWranglerFile, readWranglerJsonc, discoverContainerInfo, packageNamesFromBindings, requiredSecrets, generateSecretValue } from '@lunora/config';
import { join } from '@visulima/path';
import { Spinner } from '@visulima/spinner';
import { Project } from 'ts-morph';
import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
import { r as readWranglerName } from '../packem_shared/wrangler-name-cy4yhm9j.mjs';
import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
import { e as execArgsFor, d as detectPackageManager } from '../packem_shared/detect-package-manager-v4hHpQd0.mjs';
import { a as isRailpackAvailable, i as isDockerAvailable } from '../packem_shared/docker-hMQ97KSQ.mjs';
import { v as validateOutputFormat, i as isJsonFormat, p as printJson, l as loggerForFormat } from '../packem_shared/output-format-B4642rjE.mjs';
import { containerBuildTag } from '@lunora/container';
import { defaultSpawner } from '../packem_shared/createRecordingSpawner-WuSn20kb.mjs';
import { r as resolveWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
import { r as runSchemaDriftGate } from '../packem_shared/schema-drift-gate-BtBt0as0.mjs';
import { c as createTuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
import { l as listRemoteSecrets } from '../packem_shared/wrangler-secrets-Coni-mER.mjs';
import { runMigrateDataCommand } from './runMigrateGenerateCommand.mjs';
const WORKERS_DEV_URL = /https?:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/u;
const ANY_HTTPS_URL = /https:\/\/[^\s"'<>]+/u;
const parseDeployedUrl = (output) => {
const workersDev = WORKERS_DEV_URL.exec(output);
if (workersDev) {
return workersDev[0];
}
return ANY_HTTPS_URL.exec(output)?.[0];
};
const autoLinkFromDeployOutput = ({ cwd, env, logger, now, output }) => {
if (output === void 0 || readLinkedProject(cwd) !== void 0) {
return;
}
const url = parseDeployedUrl(output);
if (url === void 0) {
return;
}
try {
const stamp = (now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
writeLinkedProject(cwd, { env, linkedAt: stamp, workerName: readWranglerName(cwd), workerUrl: url });
logger.success(`link: recorded ${url} in .lunora/project.json (run \`lunora link\` to change)`);
} catch {
}
};
const renderDeploySummary = (inputs) => {
const { cwd, env, logger, migrated } = inputs;
try {
const link = readLinkedProject(cwd);
const workerName = link?.workerName ?? readWranglerName(cwd);
logger.success("deploy complete");
logger.info(` worker: ${workerName ?? "(see wrangler output above)"}`);
if (env !== void 0) {
logger.info(` env: ${env}`);
}
if (link?.workerUrl === void 0) {
logger.info(" url: run `lunora link --url <https://your-worker>` to record it");
} else {
logger.info(` url: ${link.workerUrl}`);
}
if (migrated) {
logger.info(" migrations: applied");
}
logger.info(" studio: lunora view --remote");
logger.info(" logs: lunora logs");
} catch {
}
};
const buildRailpackImages = async (options) => {
if (options.targets.length === 0) {
return { builtTags: [], code: 0 };
}
if (!(options.railpackAvailable ?? isRailpackAvailable)()) {
const message = "deploy blocked: a container uses `image: { build }` (Railpack), but Railpack isn't ready. Install the `railpack` CLI and start a BuildKit instance, e.g. `docker run --rm --privileged -d --name buildkit moby/buildkit` then `export BUILDKIT_HOST=docker-container://buildkit`. Alternatively switch the container's `image` to a Dockerfile path or a pre-built registry reference.";
options.logger.error(message);
return { builtTags: [], code: 1, error: message };
}
const spawner = options.spawner ?? defaultSpawner;
const builtTags = [];
for (const target of options.targets) {
const tag = containerBuildTag(target.exportName);
const build = { args: ["build", target.buildDir, "--name", tag], command: "railpack", cwd: options.cwd };
const pushExec = execArgsFor(detectPackageManager(options.cwd), "wrangler", ["containers", "push", tag]);
const push = { args: pushExec.args, command: pushExec.command, cwd: options.cwd };
options.logger.info(`railpack: building "${target.exportName}" → ${tag} from ${target.buildDir}`);
const buildResult = await spawner(build);
if (buildResult.code !== 0) {
const message = `railpack build failed for container "${target.exportName}" (${target.buildDir})`;
options.logger.error(message);
return { builtTags, code: buildResult.code, error: message };
}
options.logger.info(`railpack: pushing ${tag} to the Cloudflare Registry`);
const pushResult = await spawner(push);
if (pushResult.code !== 0) {
const message = `wrangler containers push failed for "${target.exportName}" (${tag})`;
options.logger.error(message);
return { builtTags, code: pushResult.code, error: message };
}
builtTags.push(tag);
}
return { builtTags, code: 0 };
};
const D1_PLACEHOLDER_ID = "<replace-with-d1-create-id>";
const ORIGIN_VAR_NAMES = ["LUNORA_ORIGIN_URL", "LUNORA_WORKER_ORIGIN", "AUTH_URL"];
const isLocalhostUrl = (value) => {
try {
const { hostname } = new URL(value);
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
} catch {
return false;
}
};
const isLocalImagePath = (image) => image.startsWith("./") || image.startsWith("../") || image.startsWith("/") || image.includes("Dockerfile");
const checkContainerDockerPreflight = (cwd, logger, dockerAvailable) => {
const wranglerPath = findWranglerFile(cwd);
if (!wranglerPath) {
return void 0;
}
const { parsed } = readWranglerJsonc(wranglerPath);
const localImages = (parsed?.containers ?? []).filter((entry) => typeof entry?.image === "string" && isLocalImagePath(entry.image));
if (localImages.length === 0 || dockerAvailable()) {
return void 0;
}
const message = `deploy blocked: wrangler.jsonc declares ${String(localImages.length)} container(s) built from a local Dockerfile, but no Docker-compatible engine is available. Start Docker (or Colima), or point the container's \`image\` at a pre-built registry reference. Note: container images must target linux/amd64.`;
logger.error(message);
return message;
};
const resolveComposedWorkerEntry = (cwd) => existsSync(join(cwd, "src", "worker.ts")) ? "src/worker.ts" : void 0;
const checkContainerSourcesExist = (cwd, logger) => {
for (const container of discoverContainerInfo(cwd, "lunora").containers) {
const { image } = container;
if (image.kind === "dockerfile" && !existsSync(join(cwd, image.dockerfilePath))) {
const message = `deploy blocked: container "${container.exportName}" references a Dockerfile at "${image.dockerfilePath}" that does not exist. Create it or fix the \`image\` path in lunora/containers.ts.`;
logger.error(message);
return message;
}
if (image.kind === "build" && !existsSync(join(cwd, image.buildDir))) {
const message = `deploy blocked: container "${container.exportName}" references a Railpack build directory "${image.buildDir}" that does not exist. Create it or fix the \`image.build\` path in lunora/containers.ts.`;
logger.error(message);
return message;
}
}
return void 0;
};
const isInteractive = (options) => {
if (isJsonFormat(options.format)) {
return false;
}
if (options.interactive !== void 0) {
return options.interactive;
}
return process.stdout.isTTY && !process.env.CI;
};
const findD1PlaceholderBinding = (cwd) => {
const wranglerPath = findWranglerFile(cwd);
if (!wranglerPath) {
return void 0;
}
const { parsed } = readWranglerJsonc(wranglerPath);
if (!parsed) {
return void 0;
}
const placeholder = (parsed.d1_databases ?? []).find((entry) => entry.database_id === D1_PLACEHOLDER_ID);
return placeholder?.binding;
};
const buildContainerImages = async (cwd, options) => {
const targets = discoverContainerInfo(cwd, "lunora").containers.filter((container) => container.image.kind === "build").map((container) => {
return { buildDir: container.image.buildDir, exportName: container.exportName };
});
if (targets.length === 0) {
return void 0;
}
const result = await buildRailpackImages({
cwd,
logger: options.logger,
railpackAvailable: options.railpackAvailable,
spawner: options.spawner,
targets
});
return result.code === 0 ? void 0 : result.error ?? "railpack build failed";
};
const provisionBindings = async (cwd, logger, cronTriggers = []) => {
try {
const inferred = await inferLunoraBindings({ projectRoot: cwd });
const reconciled = reconcileWranglerBindings(cwd, inferred);
if (reconciled.changed) {
logger.success(`provisioned bindings: ${reconciled.added.join(", ")} → ${reconciled.wranglerPath ?? "wrangler.jsonc"}`);
}
for (const warning of reconciled.warnings) {
logger.warn(warning);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.warn(`binding inference skipped: ${message}`);
}
try {
const reconciled = reconcileWranglerCompatibilityDate(cwd);
if (reconciled.changed) {
logger.success(
`bumped compatibility_date to ${reconciled.date ?? "unknown"} (Workers Cache enabled) → ${reconciled.wranglerPath ?? "wrangler.jsonc"}`
);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.warn(`compatibility date sync skipped: ${message}`);
}
try {
const reconciled = reconcileWranglerCrons(cwd, cronTriggers);
if (reconciled.changed) {
logger.success(`synced ${String(cronTriggers.length)} cron trigger(s) → ${reconciled.wranglerPath ?? "wrangler.jsonc"}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.warn(`cron trigger sync skipped: ${message}`);
}
};
const warnDevVariablesNotPushed = (cwd, logger) => {
const devVariablesPath = join(cwd, DEV_VARS_FILE);
if (!existsSync(devVariablesPath)) {
return;
}
let keyCount;
try {
keyCount = parseDevVariableEntries(readFileSync(devVariablesPath, "utf8")).length;
} catch {
return;
}
if (keyCount === 0) {
return;
}
logger.warn(
`Note: \`lunora deploy\` does not push secrets. ${DEV_VARS_FILE} has ${String(keyCount)} key(s); if you changed them, run \`lunora env push --yes\` to update the deployed secrets.`
);
};
const SECRET_LIKE_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
const resolveRequiredSecretKeys = async (cwd) => {
let packages = [];
try {
packages = packageNamesFromBindings(await inferLunoraBindings({ projectRoot: cwd }));
} catch {
}
const fromPackages = requiredSecrets(packages).map((entry) => entry.key);
let fromLocal = [];
try {
const devVariablesPath = join(cwd, DEV_VARS_FILE);
if (existsSync(devVariablesPath)) {
fromLocal = parseDevVariableEntries(readFileSync(devVariablesPath, "utf8")).map((entry) => entry.key).filter((key) => SECRET_LIKE_KEY.test(key));
}
} catch {
}
return [.../* @__PURE__ */ new Set([...fromPackages, ...fromLocal])];
};
const pushMintableSecrets = async (cwd, options, keys) => {
const { logger } = options;
const spawner = options.spawner ?? defaultSpawner;
const manager = detectPackageManager(cwd);
const environmentFlag = options.env === void 0 ? "" : ` --env ${options.env}`;
for (const key of keys) {
const args = ["secret", "put", key];
if (options.env !== void 0) {
args.push("--env", options.env);
}
if (options.temporary === true) {
args.push("--temporary");
}
const exec = execArgsFor(manager, "wrangler", args);
const pushResult = await spawner({ args: exec.args, command: exec.command, cwd, input: generateSecretValue() });
if (pushResult.code !== 0) {
logger.error(
`failed to push secret ${key} (exit ${String(pushResult.code)}); set it manually with \`wrangler secret put ${key}${environmentFlag}\`.`
);
return false;
}
logger.success(`generated + pushed ${key}`);
}
return true;
};
const offerMissingSecrets = async (cwd, options, interactive) => {
if (options.dryRun === true || options.preview === true) {
return void 0;
}
const { logger } = options;
const environmentFlag = options.env === void 0 ? "" : ` --env ${options.env}`;
let remote;
try {
remote = await (options.secretLister ?? listRemoteSecrets)({ cwd, env: options.env, temporary: options.temporary });
} catch {
return void 0;
}
if (!remote.ok) {
return void 0;
}
const remoteNames = new Set(remote.names);
const required = await resolveRequiredSecretKeys(cwd);
const missing = required.filter((key) => !remoteNames.has(key));
if (missing.length === 0) {
return void 0;
}
if (!interactive) {
return `missing required secret(s) on the deploy target: ${missing.join(", ")}. Set them with \`wrangler secret put <KEY>${environmentFlag}\` (or \`lunora env generate --set\` then \`lunora env push --yes${options.env === void 0 ? "" : " --prod"}\`), then re-deploy.`;
}
for (const key of missing.filter((name) => !isMintableSecretKey(name))) {
logger.warn(`required secret ${key} is not set on the target — set it with: wrangler secret put ${key}${environmentFlag}`);
}
const mintable = missing.filter((key) => isMintableSecretKey(key));
if (mintable.length === 0) {
return void 0;
}
const confirm = options.secretConfirm ?? createTuiConfirm();
if (await confirm(`${String(mintable.length)} required secret(s) not set on the target (${mintable.join(", ")}). Generate strong values and push them now?`)) {
await pushMintableSecrets(cwd, options, mintable);
return void 0;
}
logger.warn(
`${String(mintable.length)} required secret(s) not set on the target: ${mintable.join(", ")}. Generate + push with \`lunora env generate --set\` then \`lunora env push --yes${options.env === void 0 ? "" : " --prod"}\`.`
);
return void 0;
};
const runPostDeployMigrations = async (options, cwd) => {
const project = new Project({ skipAddingFilesFromTsConfig: true });
const lunoraDirectory = join(cwd, "lunora");
let migrations;
try {
migrations = discoverMigrations(project, lunoraDirectory);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
options.logger.warn(`--migrate: could not discover migrations (${message}); skipping`);
return 0;
}
if (migrations.length === 0) {
options.logger.info("--migrate: no data migrations declared in lunora/");
return 0;
}
options.logger.info(`--migrate: running ${String(migrations.length)} migration(s) against deployed worker`);
for (const migration of migrations) {
options.logger.info(`--migrate: up "${migration.id}" (table "${migration.table}")`);
const migrateOptions = {
cwd,
fetchImpl: options.fetchImpl,
id: migration.id,
logger: options.logger,
// A `--migrate-url` is always set by this point (guarded above), so this
// is a production migration — gate it behind the operator's explicit
// `--migrate-yes`/`--yes` rather than auto-confirming.
prod: true,
subcommand: "up",
token: options.migrateToken,
url: options.migrateUrl,
yes: options.migrateYes === true
};
const migrateResult = await runMigrateDataCommand(migrateOptions);
if (migrateResult.code !== 0) {
options.logger.error(`--migrate: migration "${migration.id}" failed — see output above`);
return migrateResult.code;
}
options.logger.success(`--migrate: "${migration.id}" applied`);
}
return 0;
};
const validateMigrateDeployPreflight = (options) => {
if (!options.migrate || options.dryRun || options.preview) {
return void 0;
}
if (options.migrateUrl === void 0) {
const message = "--migrate requires --migrate-url <https://your-worker> — the deploy target URL is not captured automatically, refusing to default to localhost";
options.logger.error(message);
return message;
}
if (options.migrateYes !== true) {
const message = "--migrate runs production data migrations after deploy. Re-run with --migrate-yes to confirm.";
options.logger.error(message);
return message;
}
if ((options.migrateToken ?? process.env.LUNORA_ADMIN_TOKEN) === void 0 || (options.migrateToken ?? process.env.LUNORA_ADMIN_TOKEN) === "") {
const message = "admin token required for --migrate — pass --migrate-token or set LUNORA_ADMIN_TOKEN";
options.logger.error(message);
return message;
}
return void 0;
};
const runCodegenStep = (cwd, interactive, logger, apiSpec) => {
let codegenSpinner;
if (interactive) {
codegenSpinner = new Spinner({ name: "dots" });
codegenSpinner.start("running codegen");
} else {
logger.info("running codegen");
}
try {
const result = runCodegen({ apiSpec, projectRoot: cwd });
codegenSpinner?.succeed("codegen complete");
if (!codegenSpinner) {
logger.success("codegen complete");
}
return { result };
} catch (error) {
codegenSpinner?.failed("codegen failed");
const message = error instanceof Error ? error.message : String(error);
logger.error(`codegen failed: ${message}`);
return { error: `codegen failed: ${message}` };
}
};
const checkD1Placeholder = (cwd, logger) => {
const placeholderBinding = findD1PlaceholderBinding(cwd);
if (placeholderBinding === void 0) {
return void 0;
}
const message = `deploy blocked: the "${placeholderBinding}" D1 binding has a placeholder database_id ("${D1_PLACEHOLDER_ID}"). Run \`wrangler d1 create <name>\` to create the database, then replace the placeholder in wrangler.jsonc with the real id before deploying.`;
logger.error(message);
return message;
};
const checkLocalhostOriginVariables = (cwd, logger) => {
const wranglerPath = findWranglerFile(cwd);
if (!wranglerPath) {
return void 0;
}
const { parsed } = readWranglerJsonc(wranglerPath);
const variables = parsed?.vars;
if (!variables) {
return void 0;
}
const offenders = ORIGIN_VAR_NAMES.filter((name) => typeof variables[name] === "string" && isLocalhostUrl(variables[name]));
if (offenders.length === 0) {
return void 0;
}
const message = `deploy blocked: ${offenders.join(", ")} in wrangler.jsonc point at localhost. A deployed Worker can't reach a loopback address, so this silently breaks scheduled-job dispatch / auth callbacks. Set each to the deployed worker's public URL (or move it to a secret with \`wrangler secret put\`) before deploying.`;
logger.error(message);
return message;
};
const finalizeSuccessfulDeploy = async (options, cwd, descriptor, validation, reblessSchemaBaseline) => {
if (options.migrate) {
const migrateCode = await runPostDeployMigrations(options, cwd);
if (migrateCode === 0) {
reblessSchemaBaseline?.();
}
return { code: migrateCode, descriptor, validation };
}
reblessSchemaBaseline?.();
return { code: 0, descriptor, validation };
};
const runPreDeployGates = async (cwd, options) => {
const d1Error = checkD1Placeholder(cwd, options.logger);
if (d1Error !== void 0) {
return d1Error;
}
const localhostOriginError = checkLocalhostOriginVariables(cwd, options.logger);
if (localhostOriginError !== void 0) {
return localhostOriginError;
}
const sourceError = checkContainerSourcesExist(cwd, options.logger);
if (sourceError !== void 0) {
return sourceError;
}
const dockerError = checkContainerDockerPreflight(cwd, options.logger, options.dockerAvailable ?? isDockerAvailable);
if (dockerError !== void 0) {
return dockerError;
}
return buildContainerImages(cwd, options);
};
const buildWranglerDeployArgs = (cwd, options) => {
const args = options.preview ? ["versions", "upload"] : ["deploy"];
const composedEntry = resolveComposedWorkerEntry(cwd);
if (composedEntry !== void 0) {
args.push(composedEntry);
options.logger.info(`class-B composition: deploying ${composedEntry} (overrides wrangler main)`);
}
if (options.env !== void 0) {
args.push("--env", options.env);
}
if (options.temporary) {
args.push("--temporary");
options.logger.info("temporary account: deploying to a short-lived Cloudflare account (~60min); wrangler will print a claim URL");
}
if (options.dryRun) {
args.push("--dry-run");
options.logger.info("dry run: validating + bundling without publishing");
}
if (options.outDir !== void 0) {
args.push("--outdir", options.outDir, "--metafile");
options.logger.info(`build artifact: emitting bundle to ${options.outDir}`);
}
return args;
};
const reportWranglerProblems = (validation, logger) => {
if (validation.problems.length === 0) {
return false;
}
logger.error("wrangler.jsonc validation failed:");
for (const problem of validation.problems) {
logger.error(` - ${problem}`);
}
return true;
};
const executeDeploy = async (options) => {
const cwd = options.cwd ?? process.cwd();
const interactive = isInteractive(options);
let codegen;
if (!options.skipCodegen) {
const codegenStep = runCodegenStep(cwd, interactive, options.logger, options.apiSpec);
if (codegenStep.error !== void 0) {
return {
code: 1,
descriptor: void 0,
error: codegenStep.error,
validation: { problems: [], wranglerPath: void 0 }
};
}
codegen = codegenStep.result;
}
let reblessSchemaBaseline;
if (codegen !== void 0) {
const gate = runSchemaDriftGate({
allowDrift: options.allowSchemaDrift === true,
codegen,
logger: options.logger,
updateBaseline: options.updateSchemaBaseline === true
});
if (gate.blocked) {
return {
code: 1,
descriptor: void 0,
error: "schema drift gate blocked deploy",
schemaDrift: { blocked: true, reason: gate.reason },
validation: { problems: [], wranglerPath: void 0 }
};
}
reblessSchemaBaseline = gate.rebless;
}
await provisionBindings(cwd, options.logger, codegen?.cronTriggers ?? []);
const migratePreflightError = validateMigrateDeployPreflight(options);
if (migratePreflightError !== void 0) {
return { code: 1, descriptor: void 0, error: migratePreflightError, validation: { problems: [], wranglerPath: void 0 } };
}
const preflightError = await runPreDeployGates(cwd, options);
if (preflightError !== void 0) {
return { code: 1, descriptor: void 0, error: preflightError, validation: { problems: [], wranglerPath: void 0 } };
}
const validation = validateWranglerProject({ projectRoot: cwd });
if (reportWranglerProblems(validation, options.logger)) {
return { code: 1, descriptor: void 0, error: "wrangler validation failed", validation };
}
warnDevVariablesNotPushed(cwd, options.logger);
const secretAbort = await offerMissingSecrets(cwd, options, interactive);
if (secretAbort !== void 0) {
options.logger.error(secretAbort);
return { code: 1, descriptor: void 0, error: secretAbort, validation };
}
const shouldAutoLink = !isJsonFormat(options.format) && options.dryRun !== true && options.preview !== true && readLinkedProject(cwd) === void 0;
const exec = execArgsFor(detectPackageManager(cwd), "wrangler", buildWranglerDeployArgs(cwd, options));
const descriptor = {
args: exec.args,
captureStdout: shouldAutoLink,
command: exec.command,
cwd,
// In `--format json` mode stdout is reserved for the single JSON document,
// so route wrangler's progress + deployed-URL output to stderr instead.
stdoutToStderr: isJsonFormat(options.format)
};
options.logger.info(`deploying via ${descriptor.command} ${descriptor.args.join(" ")}`);
const spawner = options.spawner ?? defaultSpawner;
const result = await spawner(descriptor);
if (result.code !== 0) {
return { code: result.code, descriptor, validation };
}
if (options.dryRun) {
return { code: 0, descriptor, validation };
}
if (options.preview) {
return { code: 0, descriptor, validation };
}
autoLinkFromDeployOutput({ cwd, env: options.env, logger: options.logger, output: result.stdout });
return finalizeSuccessfulDeploy(options, cwd, descriptor, validation, reblessSchemaBaseline);
};
const runDeployCommand = async (options) => {
const formatError = validateOutputFormat("deploy", options.format);
if (formatError !== void 0) {
options.logger.error(formatError);
return { code: 1, descriptor: void 0, error: formatError, validation: { problems: [], wranglerPath: void 0 } };
}
const result = await executeDeploy({ ...options, logger: loggerForFormat(options.format, options.logger) });
if (isJsonFormat(options.format)) {
printJson(result);
return result;
}
if (result.code === 0 && options.dryRun !== true && options.preview !== true) {
renderDeploySummary({ cwd: options.cwd ?? process.cwd(), env: options.env, logger: options.logger, migrated: options.migrate === true });
} else if (result.code === 0 && options.preview === true) {
options.logger.success("preview version uploaded — see the preview URL in the wrangler output above");
}
return result;
};
const execute = defineHandler(async ({ cwd, logger, options }) => {
const result = await runDeployCommand({
allowSchemaDrift: options.allowSchemaDrift === true,
apiSpec: parseApiSpec(options.apiSpec),
cwd,
dryRun: options.dryRun === true,
env: options.env,
format: options.format,
logger,
migrate: options.migrate === true,
migrateToken: options.migrateToken,
// Fall back to the `.lunora/project.json` link so a linked checkout no
// longer needs --migrate-url repeated on every `deploy --migrate`.
migrateUrl: resolveWorkerUrl({ cwd, url: options.migrateUrl }),
migrateYes: options.migrateYes === true,
preview: options.preview === true,
// `--prebuilt` trusts a prior `lunora build`/`prepare`: skip codegen (and
// thus the drift gate, which has no fresh snapshot to measure).
skipCodegen: options.prebuilt === true,
temporary: options.temporary === true,
updateSchemaBaseline: options.updateSchemaBaseline === true
});
return { code: result.code };
});
export { execute, runDeployCommand };