UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

620 lines (614 loc) 26.9 kB
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 };