@m-kopa/launchpad-cli 0.62.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.62.0";
22
+ var CLI_VERSION = "0.64.0";
23
23
 
24
24
  // src/config.ts
25
25
  import { readFileSync } from "node:fs";
@@ -1888,8 +1888,8 @@ function describe10(e) {
1888
1888
  }
1889
1889
 
1890
1890
  // src/commands/deploy.ts
1891
- import { existsSync as existsSync7 } from "node:fs";
1892
- import * as path8 from "node:path";
1891
+ import { existsSync as existsSync8 } from "node:fs";
1892
+ import * as path9 from "node:path";
1893
1893
 
1894
1894
  // src/bundle/orchestrate.ts
1895
1895
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -2206,8 +2206,14 @@ function deployBundleError(envelope, body) {
2206
2206
  ...typeof body.head_sha === "string" ? { head_sha: body.head_sha } : {},
2207
2207
  ...paths === undefined ? {} : { paths },
2208
2208
  ...typeof body.reason === "string" ? { reason: body.reason } : {},
2209
+ ...readDeployGuidance(body)
2210
+ };
2211
+ }
2212
+ function readDeployGuidance(body) {
2213
+ return {
2209
2214
  ...typeof body.minimum === "string" ? { minimum: body.minimum } : {},
2210
2215
  ...typeof body.yours === "string" ? { yours: body.yours } : {},
2216
+ ...typeof body.requires_admin_approval === "boolean" ? { requires_admin_approval: body.requires_admin_approval } : {},
2211
2217
  ...typeof body.statusPageUrl === "string" ? { statusPageUrl: body.statusPageUrl } : {}
2212
2218
  };
2213
2219
  }
@@ -3357,7 +3363,10 @@ var SpecSchema = z3.object({
3357
3363
  env_vars: z3.record(z3.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
3358
3364
  containers: z3.array(ContainerSchema).min(1).optional(),
3359
3365
  secrets: SecretsSchema.optional(),
3360
- fronted_by_concierge: z3.boolean().optional()
3366
+ fronted_by_concierge: z3.boolean().optional(),
3367
+ bindings: z3.object({
3368
+ d1: z3.string().min(1).max(64).regex(ENV_NAME_REGEX, "bindings.d1 must be an UPPER_SNAKE_CASE binding name").optional()
3369
+ }).strict().optional()
3361
3370
  }).strict().superRefine((s, ctx) => {
3362
3371
  if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
3363
3372
  ctx.addIssue({
@@ -3426,14 +3435,27 @@ var SpecSchema = z3.object({
3426
3435
  message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3427
3436
  });
3428
3437
  }
3429
- for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
3430
- if (envVar.type === "secret_text") {
3431
- ctx.addIssue({
3432
- code: z3.ZodIssueCode.custom,
3433
- path: ["env_vars", name],
3434
- message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
3435
- });
3436
- }
3438
+ }
3439
+ if (s.bindings?.d1 !== undefined) {
3440
+ if (s.origin !== "dispatch") {
3441
+ ctx.addIssue({
3442
+ code: z3.ZodIssueCode.custom,
3443
+ path: ["bindings", "d1"],
3444
+ message: "bindings.d1 requires origin: dispatch. A pages-origin app declares its database with a v1 pages target d1_binding."
3445
+ });
3446
+ } else if (s.appType !== "react+api") {
3447
+ ctx.addIssue({
3448
+ code: z3.ZodIssueCode.custom,
3449
+ path: ["bindings", "d1"],
3450
+ message: `bindings.d1 requires appType 'react+api' (current appType: '${s.appType}'); only a react+api app ships the Worker code that can read a database.`
3451
+ });
3452
+ }
3453
+ if (s.env_vars?.[s.bindings.d1] !== undefined) {
3454
+ ctx.addIssue({
3455
+ code: z3.ZodIssueCode.custom,
3456
+ path: ["env_vars", s.bindings.d1],
3457
+ message: `env var '${s.bindings.d1}' has the same name as bindings.d1. Rename one of them.`
3458
+ });
3437
3459
  }
3438
3460
  }
3439
3461
  if (!isContainer && s.containers !== undefined) {
@@ -4443,7 +4465,7 @@ function deploymentAuthIssue(appType, auth) {
4443
4465
  function newAppOrigin(appType, requested) {
4444
4466
  if (appType === "container")
4445
4467
  return;
4446
- return requested ?? (appType === "react+api" ? "pages" : "dispatch");
4468
+ return requested ?? "dispatch";
4447
4469
  }
4448
4470
  // src/bundle/boundary.ts
4449
4471
  var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
@@ -7005,7 +7027,7 @@ async function verifyFirstDeployServed(args, liveUrl, io, deps) {
7005
7027
 
7006
7028
  // src/commands/deploy.ts
7007
7029
  import { parse as parseYaml6 } from "yaml";
7008
- import { readFileSync as readFileSync11 } from "node:fs";
7030
+ import { readFileSync as readFileSync12 } from "node:fs";
7009
7031
 
7010
7032
  // src/deploy/git-files.ts
7011
7033
  import { spawn as spawn3 } from "node:child_process";
@@ -8613,6 +8635,494 @@ function rejectUnsupportedCliRuntime(args) {
8613
8635
  return 1;
8614
8636
  }
8615
8637
 
8638
+ // src/commands/migrate.ts
8639
+ import { closeSync, existsSync as existsSync7, lstatSync as lstatSync4, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
8640
+ import path8 from "node:path";
8641
+ import { stringify } from "yaml";
8642
+
8643
+ // src/manifest/migrate-dispatch.ts
8644
+ var PLATFORM_ENV_NAMES = new Set([
8645
+ "GATEWAY_ISSUER",
8646
+ "GATEWAY_AUD",
8647
+ "GATEWAY_JWKS_URL",
8648
+ "CF_ACCESS_TEAM_DOMAIN",
8649
+ "CF_ACCESS_AUD",
8650
+ "JWKS_URL"
8651
+ ]);
8652
+ function dispatchManifest(manifest, wranglerD1) {
8653
+ const v2 = manifest.apiVersion === "launchpad.m-kopa.us/v2";
8654
+ const source = v2 ? manifest.spec : manifest;
8655
+ const appType = v2 ? manifest.spec.appType : manifest.deployment.type;
8656
+ if (appType === "container" || source.containers !== undefined) {
8657
+ throw new Error("Containers use their own runtime and cannot convert to dispatch.");
8658
+ }
8659
+ if (!v2)
8660
+ assertConvertibleV1(manifest);
8661
+ const d1 = d1Binding(v2 ? manifest.spec.bindings?.d1 : pagesTargetD1(manifest), wranglerD1);
8662
+ const plainEnv = v2 ? manifest.spec.env_vars : plainTextEnv(manifest.production_env);
8663
+ const envVars = { ...plainEnv, ...secretEnv(source.secrets, plainEnv) };
8664
+ const spec = v2 ? { ...manifest.spec, secrets: undefined } : {
8665
+ appType,
8666
+ display: {
8667
+ name: manifest.metadata.name,
8668
+ team: manifest.metadata.team,
8669
+ ...manifest.metadata.description === undefined ? {} : { description: manifest.metadata.description }
8670
+ },
8671
+ owner: { email: manifest.metadata.owner },
8672
+ access: {
8673
+ audience: manifest.access.audience,
8674
+ private_owner: manifest.access.private_owner,
8675
+ allowed_entra_groups: [...allowedEntraGroups(manifest.access)],
8676
+ identity_types: manifest.access.identity_types
8677
+ },
8678
+ build: manifest.build,
8679
+ app: manifest.app,
8680
+ verify: manifest.verify,
8681
+ sensitive: manifest.sensitive,
8682
+ catalogue: manifest.catalogue
8683
+ };
8684
+ const candidate = {
8685
+ apiVersion: "launchpad.m-kopa.us/v2",
8686
+ kind: "App",
8687
+ metadata: v2 ? manifest.metadata : { slug: manifest.metadata.name, schemaVersion: 2 },
8688
+ spec: {
8689
+ ...spec,
8690
+ ...d1 === undefined ? {} : { bindings: { d1 } },
8691
+ env_vars: Object.keys(envVars).length === 0 ? undefined : envVars,
8692
+ origin: "dispatch",
8693
+ auth: "gateway",
8694
+ confine_origin: undefined
8695
+ }
8696
+ };
8697
+ const parsed = parseManifestV2(candidate);
8698
+ if (parsed.kind !== "ok") {
8699
+ throw new Error(`Cannot convert manifest: ${parsed.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
8700
+ }
8701
+ return parsed.manifest;
8702
+ }
8703
+ function assertConvertibleV1(manifest) {
8704
+ const unsupported2 = [
8705
+ manifest.hostnames !== undefined && "hostnames",
8706
+ manifest.targets !== undefined && pagesTargetD1(manifest) === undefined && "targets",
8707
+ manifest.access.session_duration !== undefined && "access.session_duration",
8708
+ manifest.deployment.runtime !== undefined && "deployment.runtime"
8709
+ ].filter(Boolean);
8710
+ if (unsupported2.length > 0) {
8711
+ throw new Error(`Cannot preserve ${unsupported2.join(", ")} on dispatch. Review these settings with the platform team before migrating.`);
8712
+ }
8713
+ }
8714
+ function d1Binding(declared, wranglerD1) {
8715
+ if (declared !== undefined && wranglerD1 !== undefined && declared !== wranglerD1) {
8716
+ throw new Error(`The manifest binds D1 as ${declared} but the wrangler config binds ${wranglerD1}. Use one name for both.`);
8717
+ }
8718
+ return declared ?? wranglerD1;
8719
+ }
8720
+ function plainTextEnv(env) {
8721
+ if (env === undefined)
8722
+ return;
8723
+ return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, { type: "plain_text", value }]));
8724
+ }
8725
+ function secretEnv(secrets, env) {
8726
+ const out = {};
8727
+ for (const binding of secrets?.bindings ?? []) {
8728
+ const refusal = secretRefusal(binding, env);
8729
+ if (refusal !== undefined)
8730
+ throw new Error(`Secret ${binding.name} ${refusal}`);
8731
+ out[binding.name] = { type: "secret_text", secret_ref: binding.name };
8732
+ }
8733
+ return out;
8734
+ }
8735
+ function secretRefusal(binding, env) {
8736
+ const targets = binding.targets === undefined || binding.targets === "all" ? [] : binding.targets;
8737
+ const checks = [
8738
+ [binding.source !== "env-file", "is platform-managed and cannot be converted automatically. Ask the platform team to migrate it."],
8739
+ [targets.some((target) => target !== "primary"), "targets another surface, which dispatch does not have."],
8740
+ [PLATFORM_ENV_NAMES.has(binding.name), "is set by the platform on dispatch apps. Rename it before converting."],
8741
+ [env?.[binding.name] !== undefined, "is also declared as an environment variable. Keep one."]
8742
+ ];
8743
+ return checks.find(([refused]) => refused)?.[1];
8744
+ }
8745
+ function pagesTargetD1(manifest) {
8746
+ const [target, ...rest] = manifest.targets ?? [];
8747
+ if (rest.length > 0 || target?.kind !== "pages")
8748
+ return;
8749
+ return target.d1_binding;
8750
+ }
8751
+
8752
+ // src/manifest/wrangler-config.ts
8753
+ var ALLOWED_KEYS = new Set([
8754
+ "name",
8755
+ "compatibility_date",
8756
+ "compatibility_flags",
8757
+ "pages_build_output_dir",
8758
+ "d1_databases"
8759
+ ]);
8760
+ var PLATFORM_FLAGS = new Set(["nodejs_compat"]);
8761
+
8762
+ class UnsupportedWranglerConfigError extends Error {
8763
+ }
8764
+ function wranglerD1Binding(file, text) {
8765
+ const toml = file.endsWith(".toml");
8766
+ const config = toml ? readToml(file, text) : readJsonc(file, text);
8767
+ for (const key of Object.keys(config)) {
8768
+ if (!ALLOWED_KEYS.has(key) && (toml || key !== "$schema"))
8769
+ throw unsupportedKey(file, key);
8770
+ }
8771
+ if (config.compatibility_flags !== undefined) {
8772
+ const extra = asArray(file, "compatibility_flags", config.compatibility_flags).find((flag) => !PLATFORM_FLAGS.has(String(flag)));
8773
+ if (extra !== undefined) {
8774
+ throw new UnsupportedWranglerConfigError(`${file} sets compatibility flag \`${String(extra)}\`; dispatch runs with nodejs_compat only`);
8775
+ }
8776
+ }
8777
+ if (config.d1_databases === undefined)
8778
+ return;
8779
+ const databases = asArray(file, "d1_databases", config.d1_databases);
8780
+ if (databases.length !== 1) {
8781
+ throw new UnsupportedWranglerConfigError(`${file} declares ${databases.length} \`d1_databases\` entries; dispatch binds exactly one D1 database`);
8782
+ }
8783
+ const binding = databases[0]?.binding;
8784
+ if (typeof binding !== "string" || binding === "") {
8785
+ throw new UnsupportedWranglerConfigError(`${file} has a \`d1_databases\` entry without a binding name`);
8786
+ }
8787
+ return binding;
8788
+ }
8789
+ function unsupportedKey(file, key) {
8790
+ return new UnsupportedWranglerConfigError(`${file} sets \`${key}\`, which dispatch cannot preserve`);
8791
+ }
8792
+ function asArray(file, key, value) {
8793
+ if (!Array.isArray(value))
8794
+ throw new UnsupportedWranglerConfigError(`${file} sets \`${key}\` to something other than a list`);
8795
+ return value;
8796
+ }
8797
+ function readJsonc(file, text) {
8798
+ const stripped = stripOutsideStrings(text, '"', (at) => {
8799
+ if (text.startsWith("//", at))
8800
+ return lineEnd(text, at) - at;
8801
+ if (!text.startsWith("/*", at))
8802
+ return 0;
8803
+ const end = text.indexOf("*/", at + 2);
8804
+ return (end === -1 ? text.length : end + 2) - at;
8805
+ }).replace(/,(\s*[}\]])/g, "$1");
8806
+ let parsed;
8807
+ try {
8808
+ parsed = JSON.parse(stripped);
8809
+ } catch {
8810
+ throw new UnsupportedWranglerConfigError(`${file} could not be read as JSON`);
8811
+ }
8812
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8813
+ throw new UnsupportedWranglerConfigError(`${file} is not a JSON object`);
8814
+ }
8815
+ return parsed;
8816
+ }
8817
+ function readToml(file, text) {
8818
+ const config = {};
8819
+ const databases = [];
8820
+ let table = config;
8821
+ for (const line of logicalTomlLines(text)) {
8822
+ if (line.startsWith("[")) {
8823
+ table = d1Table(file, line);
8824
+ databases.push(table);
8825
+ } else {
8826
+ assign(file, table, line, table === config);
8827
+ }
8828
+ }
8829
+ if (databases.length === 0)
8830
+ return config;
8831
+ if (config.d1_databases !== undefined)
8832
+ throw new UnsupportedWranglerConfigError(`${file} declares d1_databases twice`);
8833
+ return { ...config, d1_databases: databases };
8834
+ }
8835
+ function d1Table(file, line) {
8836
+ const name = /^\[\[?\s*([^\]]*?)\s*\]\]?$/.exec(line)?.[1] ?? line;
8837
+ if (!line.startsWith("[[") || name !== "d1_databases")
8838
+ throw unsupportedKey(file, topKey(name));
8839
+ return {};
8840
+ }
8841
+ function assign(file, table, line, topLevel) {
8842
+ const assignment = /^([\w\-."]+)\s*=\s*(.*)$/.exec(line);
8843
+ if (assignment === null)
8844
+ throw new UnsupportedWranglerConfigError(`${file} could not be read as TOML near \`${line.slice(0, 40)}\``);
8845
+ const key = topKey(assignment[1]);
8846
+ if (topLevel && assignment[1].includes("."))
8847
+ throw unsupportedKey(file, key);
8848
+ table[key] = tomlValue(assignment[2].trim());
8849
+ }
8850
+ function topKey(dotted) {
8851
+ return dotted.split(".")[0].replace(/"/g, "").trim();
8852
+ }
8853
+ function tomlValue(raw) {
8854
+ const quoted = /^"((?:[^"\\]|\\.)*)"$|^'([^']*)'$/.exec(raw);
8855
+ if (quoted !== null)
8856
+ return quoted[1] ?? quoted[2];
8857
+ if (!raw.startsWith("["))
8858
+ return raw;
8859
+ const inner = raw.slice(1, -1);
8860
+ if (inner.includes("{")) {
8861
+ return [...inner.matchAll(/\{([^}]*)\}/g)].map((table) => ({
8862
+ binding: /(?:^|[\s,])binding\s*=\s*["']([^"']*)["']/.exec(table[1])?.[1]
8863
+ }));
8864
+ }
8865
+ return [...inner.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map((item) => item[1] ?? item[2]);
8866
+ }
8867
+ function logicalTomlLines(text) {
8868
+ const stripped = stripOutsideStrings(text, `"'`, (at) => text[at] === "#" ? lineEnd(text, at) - at : 0);
8869
+ const lines = [];
8870
+ let pending = "";
8871
+ for (const raw of stripped.split(/\r?\n/)) {
8872
+ pending = `${pending} ${raw.trim()}`.trim();
8873
+ if (pending === "" || openBrackets(pending) > 0)
8874
+ continue;
8875
+ lines.push(pending);
8876
+ pending = "";
8877
+ }
8878
+ if (pending !== "")
8879
+ lines.push(pending);
8880
+ return lines;
8881
+ }
8882
+ function lineEnd(text, at) {
8883
+ const end = text.indexOf(`
8884
+ `, at);
8885
+ return end === -1 ? text.length : end;
8886
+ }
8887
+ function openBrackets(line) {
8888
+ if (/^\[\[?[^\]=]*\]\]?$/.test(line))
8889
+ return 0;
8890
+ let open = 0;
8891
+ stripOutsideStrings(line, `"'`, (at) => {
8892
+ if (line[at] === "[" || line[at] === "{")
8893
+ open++;
8894
+ if (line[at] === "]" || line[at] === "}")
8895
+ open--;
8896
+ return 0;
8897
+ });
8898
+ return open;
8899
+ }
8900
+ function stripOutsideStrings(text, quotes, skip) {
8901
+ let out = "";
8902
+ for (let i = 0;i < text.length; ) {
8903
+ if (quotes.includes(text[i])) {
8904
+ const end = stringEnd(text, i);
8905
+ out += text.slice(i, end);
8906
+ i = end;
8907
+ continue;
8908
+ }
8909
+ const skipped = skip(i);
8910
+ if (skipped === 0)
8911
+ out += text[i];
8912
+ i += Math.max(skipped, 1);
8913
+ }
8914
+ return out;
8915
+ }
8916
+ function stringEnd(text, start) {
8917
+ const quote = text[start];
8918
+ for (let i = start + 1;i < text.length; i++) {
8919
+ if (text[i] === "\\" && quote === '"')
8920
+ i++;
8921
+ else if (text[i] === quote)
8922
+ return i + 1;
8923
+ }
8924
+ return text.length;
8925
+ }
8926
+
8927
+ // src/commands/migrate.ts
8928
+ var USAGE = "usage: launchpad migrate --origin dispatch [--dry-run]";
8929
+ var migrateCommand = {
8930
+ name: "migrate",
8931
+ summary: "convert an unprovisioned static, React or React API app to Workers dispatch",
8932
+ run: runMigrate
8933
+ };
8934
+ async function runMigrate(args, io) {
8935
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
8936
+ io.out(USAGE);
8937
+ return 0;
8938
+ }
8939
+ const flags = args.filter((arg) => arg !== "--dry-run");
8940
+ if (flags.length !== 2 || flags[0] !== "--origin" || flags[1] !== "dispatch" || args.length > 3) {
8941
+ io.err(USAGE);
8942
+ return 64;
8943
+ }
8944
+ try {
8945
+ const result = await convertToDispatch(process.cwd(), args.includes("--dry-run"));
8946
+ if (result.kind === "already") {
8947
+ io.out("launchpad.yaml already selects Workers dispatch. Run `launchpad deploy`.");
8948
+ } else if (result.kind === "eligible") {
8949
+ io.out(`${result.slug}: eligible for v2 Workers dispatch with gateway authentication. No files changed.`);
8950
+ } else {
8951
+ io.out(`${result.slug}: launchpad.yaml now uses v2 Workers dispatch with gateway authentication.`);
8952
+ io.out(`Audience, owner and application settings are preserved. Original: ${BACKUP_PATH}`);
8953
+ io.out("Run `launchpad deploy`. No Pages project is needed.");
8954
+ const next = secretsNextStep(result.secrets);
8955
+ if (next !== null)
8956
+ io.out(next);
8957
+ }
8958
+ return 0;
8959
+ } catch (error) {
8960
+ io.err(`launchpad migrate: ${error instanceof Error ? error.message : String(error)}`);
8961
+ return 1;
8962
+ }
8963
+ }
8964
+ var BACKUP_PATH = ".launchpad/manifest-before-dispatch.yaml";
8965
+ function secretsNextStep(secrets) {
8966
+ if (secrets.length === 0)
8967
+ return null;
8968
+ return `Secrets (${secrets.join(", ")}) carry over as secret_text env vars. Once \`launchpad status\` shows the first deploy delivered, run \`launchpad secrets push\` to set them.`;
8969
+ }
8970
+ async function convertToDispatch(cwd, dryRun) {
8971
+ const file = path8.join(cwd, "launchpad.yaml");
8972
+ if (!lstatSync4(file).isFile())
8973
+ throw new Error("launchpad.yaml must be a regular file, not a symlink.");
8974
+ const original = readFileSync11(file, "utf8");
8975
+ const loaded = parseManifestAnyVersion(original, file);
8976
+ if (loaded.kind !== "ok")
8977
+ throw new Error("launchpad.yaml is invalid. Run `launchpad validate` and correct it first.");
8978
+ const manifest = loaded.manifest;
8979
+ if (manifest.apiVersion === "launchpad.m-kopa.us/v2" && manifest.spec.origin === "dispatch")
8980
+ return { kind: "already" };
8981
+ const slug = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.metadata.slug : manifest.metadata.name;
8982
+ let converted;
8983
+ try {
8984
+ converted = convertLocally(cwd, manifest);
8985
+ } catch (error) {
8986
+ await refuseIfRegistered(slug);
8987
+ throw error;
8988
+ }
8989
+ await assertUnregistered(slug);
8990
+ const secrets = Object.entries(converted.spec.env_vars ?? {}).filter(([, value]) => value.type === "secret_text").map(([name]) => name);
8991
+ if (dryRun)
8992
+ return { kind: "eligible", slug, secrets };
8993
+ try {
8994
+ saveMigration(file, original, converted);
8995
+ } catch (error) {
8996
+ throw new MigrationSaveError(error);
8997
+ }
8998
+ return { kind: "converted", slug, secrets };
8999
+ }
9000
+ function convertLocally(cwd, manifest) {
9001
+ const v2 = manifest.apiVersion === "launchpad.m-kopa.us/v2";
9002
+ const spec = v2 ? manifest.spec : manifest;
9003
+ const appType = v2 ? manifest.spec.appType : manifest.deployment.type;
9004
+ return dispatchManifest(manifest, inspectRuntime(cwd, appType, spec.app?.root, spec.build));
9005
+ }
9006
+ async function refuseIfRegistered(slug) {
9007
+ try {
9008
+ await assertUnregistered(slug);
9009
+ } catch (error) {
9010
+ if (error instanceof RegisteredAppError)
9011
+ throw error;
9012
+ }
9013
+ }
9014
+ async function assertUnregistered(slug) {
9015
+ try {
9016
+ await apiRaw(loadConfig(), { path: `/apps/${encodeURIComponent(slug)}/manifest/state` });
9017
+ } catch (error) {
9018
+ if (error instanceof NotFoundError && error.serverCode === "not_found")
9019
+ return;
9020
+ if (error instanceof UpgradeRequiredError || error instanceof UnauthenticatedError)
9021
+ throw error;
9022
+ throw new RegistryUnavailableError(error);
9023
+ }
9024
+ throw new RegisteredAppError;
9025
+ }
9026
+
9027
+ class MigrationSaveError extends Error {
9028
+ constructor(cause) {
9029
+ super(`Could not save the converted manifest: ${cause instanceof Error ? cause.message : String(cause)}`);
9030
+ }
9031
+ }
9032
+
9033
+ class RegistryUnavailableError extends Error {
9034
+ constructor(cause) {
9035
+ super(`Could not confirm whether this app is registered: ${cause instanceof Error ? cause.message : String(cause)}`);
9036
+ }
9037
+ }
9038
+
9039
+ class RegisteredAppError extends Error {
9040
+ constructor() {
9041
+ super("This app is already registered. This command only converts apps that have never provisioned; ask the platform team to migrate an existing app.");
9042
+ }
9043
+ }
9044
+ var WRANGLER_FILES = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
9045
+ function inspectRuntime(cwd, appType, appRootDir, build) {
9046
+ const d1 = new Set;
9047
+ for (const root of runtimeRoots(cwd, appRootDir, build)) {
9048
+ assertNoUncompiledCode(root, appType);
9049
+ for (const binding2 of wranglerD1Bindings(cwd, root))
9050
+ d1.add(binding2);
9051
+ }
9052
+ if (d1.size > 1)
9053
+ throw new Error(`The wrangler configs bind D1 under different names (${[...d1].join(", ")}). Use one name.`);
9054
+ const [binding] = d1;
9055
+ if (binding !== undefined && appType !== "react+api") {
9056
+ throw new Error("The wrangler config declares a D1 database, which only react+api apps can use on dispatch.");
9057
+ }
9058
+ return binding;
9059
+ }
9060
+ function runtimeRoots(cwd, appRootDir, build) {
9061
+ const appRoot = path8.resolve(cwd, appRootDir ?? ".");
9062
+ const buildRoot = path8.resolve(appRoot, build?.root_dir ?? ".");
9063
+ return new Set([
9064
+ cwd,
9065
+ appRoot,
9066
+ buildRoot,
9067
+ path8.resolve(cwd, build?.root_dir ?? "."),
9068
+ path8.resolve(buildRoot, build?.destination_dir ?? "dist")
9069
+ ]);
9070
+ }
9071
+ function assertNoUncompiledCode(root, appType) {
9072
+ const api = appType === "react+api";
9073
+ for (const name of api ? ["_worker.js"] : ["_worker.js", "functions"]) {
9074
+ if (existsSync7(path8.join(root, name))) {
9075
+ throw new Error(`${name} needs a runtime review before conversion. Pages Functions and custom Worker code must not be dropped. Ask the platform team to review this app.`);
9076
+ }
9077
+ }
9078
+ if (api)
9079
+ assertWorkerRoutesFit(root);
9080
+ }
9081
+ function wranglerD1Bindings(cwd, root) {
9082
+ return WRANGLER_FILES.map((name) => path8.join(root, name)).filter((file) => existsSync7(file)).map((file) => wranglerD1Binding(path8.relative(cwd, file), readFileSync11(file, "utf8"))).filter((binding) => binding !== undefined);
9083
+ }
9084
+ function assertWorkerRoutesFit(root) {
9085
+ const functions = path8.join(root, "functions");
9086
+ if (!existsSync7(functions) || !lstatSync4(functions).isDirectory())
9087
+ return;
9088
+ const catchAll = readdirSync2(functions).find((name) => /^_middleware\.[cm]?[jt]sx?$/.test(name) || name.startsWith("["));
9089
+ if (catchAll !== undefined) {
9090
+ throw new Error(`functions/${catchAll} would run the Worker for every path, including static assets, which dispatch does not support. Move it under a prefix such as functions/api/.`);
9091
+ }
9092
+ }
9093
+ function saveMigration(file, original, converted) {
9094
+ if (readFileSync11(file, "utf8") !== original)
9095
+ throw new Error("launchpad.yaml changed during the check. Retry migration.");
9096
+ const backupDir = path8.join(path8.dirname(file), ".launchpad");
9097
+ if (existsSync7(backupDir) && !lstatSync4(backupDir).isDirectory()) {
9098
+ throw new Error(".launchpad must be a regular directory, not a symlink or file.");
9099
+ }
9100
+ mkdirSync2(backupDir, { recursive: true });
9101
+ const backup = path8.join(backupDir, "manifest-before-dispatch.yaml");
9102
+ const attemptDir = mkdtempSync(path8.join(backupDir, "migration-"));
9103
+ let backupCreated = false;
9104
+ try {
9105
+ const fd = openSync(backup, "wx", 384);
9106
+ backupCreated = true;
9107
+ try {
9108
+ writeFileSync2(fd, original);
9109
+ } finally {
9110
+ closeSync(fd);
9111
+ }
9112
+ const temporary = path8.join(attemptDir, "launchpad.yaml");
9113
+ writeFileSync2(temporary, stringify(converted), { flag: "wx", mode: 384 });
9114
+ if (readFileSync11(file, "utf8") !== original)
9115
+ throw new Error("launchpad.yaml changed during migration. Retry migration.");
9116
+ renameSync(temporary, file);
9117
+ } catch (error) {
9118
+ if (backupCreated)
9119
+ rmSync2(backup);
9120
+ throw error;
9121
+ } finally {
9122
+ rmSync2(attemptDir, { recursive: true, force: true });
9123
+ }
9124
+ }
9125
+
8616
9126
  // src/commands/deploy.ts
8617
9127
  var deployCommand = {
8618
9128
  name: "deploy",
@@ -8665,8 +9175,8 @@ async function runDeploy(args, io) {
8665
9175
  }, io);
8666
9176
  }
8667
9177
  const cwd = process.cwd();
8668
- const manifestPath = path8.join(cwd, "launchpad.yaml");
8669
- if (existsSync7(manifestPath)) {
9178
+ const manifestPath = path9.join(cwd, "launchpad.yaml");
9179
+ if (existsSync8(manifestPath)) {
8670
9180
  const contentMode = flags.mode.kind === "content" ? flags.mode : {
8671
9181
  allowStale: false,
8672
9182
  rebaseOntoHead: false,
@@ -8700,7 +9210,7 @@ async function runDeploy(args, io) {
8700
9210
  } else {
8701
9211
  const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
8702
9212
  if (inferred === null) {
8703
- io.err(`launchpad deploy: could not infer slug from cwd (${path8.basename(process.cwd())});
9213
+ io.err(`launchpad deploy: could not infer slug from cwd (${path9.basename(process.cwd())});
8704
9214
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
8705
9215
  return 64;
8706
9216
  }
@@ -8720,10 +9230,10 @@ async function runDeploy(args, io) {
8720
9230
  return 1;
8721
9231
  }
8722
9232
  let contentManifestYaml = null;
8723
- const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
8724
- if (existsSync7(contentManifestPath)) {
9233
+ const contentManifestPath = path9.join(process.cwd(), "launchpad.yaml");
9234
+ if (existsSync8(contentManifestPath)) {
8725
9235
  try {
8726
- contentManifestYaml = readFileSync11(contentManifestPath, "utf8");
9236
+ contentManifestYaml = readFileSync12(contentManifestPath, "utf8");
8727
9237
  } catch {
8728
9238
  contentManifestYaml = null;
8729
9239
  }
@@ -8933,8 +9443,8 @@ function surfaceStaleBlock(body, io) {
8933
9443
  const head = body.head_sha ?? null;
8934
9444
  if (body.error === "stale_overlap" && body.paths !== undefined) {
8935
9445
  io.err(`launchpad deploy: BLOCKED — this deploy would roll back ${body.paths.length} file(s) a teammate changed on main since you cloned:`);
8936
- for (const path9 of body.paths.slice(0, 20)) {
8937
- io.err(` - ${path9}`);
9446
+ for (const path10 of body.paths.slice(0, 20)) {
9447
+ io.err(` - ${path10}`);
8938
9448
  }
8939
9449
  if (body.paths.length > 20) {
8940
9450
  io.err(` … and ${body.paths.length - 20} more`);
@@ -8967,7 +9477,7 @@ function withTimeout(p, ms) {
8967
9477
  async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
8968
9478
  let localYaml;
8969
9479
  try {
8970
- localYaml = readFileSync11(manifestPath, "utf8");
9480
+ localYaml = readFileSync12(manifestPath, "utf8");
8971
9481
  } catch {
8972
9482
  return;
8973
9483
  }
@@ -9016,8 +9526,21 @@ async function runModelADeploy(args) {
9016
9526
  let slug;
9017
9527
  let appType = "static";
9018
9528
  let declaredOrigin;
9529
+ let pagesNote;
9019
9530
  try {
9020
- const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
9531
+ pagesNote = await defaultToDispatch(cwd, manifestPath, io);
9532
+ } catch (e) {
9533
+ if (e instanceof UpgradeRequiredError) {
9534
+ io.err(e.message);
9535
+ return EXIT_CLI_UPGRADE_REQUIRED;
9536
+ }
9537
+ io.err(`launchpad deploy: ${describe17(e)}. Nothing was deployed.`);
9538
+ if (e instanceof UnauthenticatedError)
9539
+ io.err(" run `launchpad login` to refresh your session.");
9540
+ return 1;
9541
+ }
9542
+ try {
9543
+ const manifestObj = parseYaml6(readFileSync12(manifestPath, "utf8"));
9021
9544
  const metaSlug = resolveManifestSlug(manifestObj);
9022
9545
  if (metaSlug === null) {
9023
9546
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
@@ -9084,9 +9607,8 @@ async function runModelADeploy(args) {
9084
9607
  io.out(`Bundling ${cwd} for ${slug} (Model A) …`);
9085
9608
  if (declaredOrigin !== "dispatch" && appType !== "container") {
9086
9609
  io.out("Deployment target: Cloudflare Pages, selected by launchpad.yaml.");
9087
- if (appType === "static" || appType === "react") {
9088
- io.out("For an app that has never provisioned, `launchpad migrate --origin dispatch` checks whether it can use Workers without a Pages project.");
9089
- }
9610
+ if (pagesNote !== null)
9611
+ io.out(pagesNote);
9090
9612
  }
9091
9613
  let cfg;
9092
9614
  try {
@@ -9172,7 +9694,7 @@ async function runModelADeploy(args) {
9172
9694
  const errorCode = body.error;
9173
9695
  if (errorCode === "audience_confirmation_required") {
9174
9696
  io.err(String(body.message));
9175
- io.err("Only the human builder may confirm. Retry this deployment after confirmation.");
9697
+ io.err(body.requires_admin_approval === true ? "This sensitive app needs builder confirmation and platform-admin approval. Retry after both are complete." : "The server still requires browser confirmation. Ask the platform team to update it, then retry.");
9176
9698
  return 1;
9177
9699
  }
9178
9700
  if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
@@ -9375,6 +9897,49 @@ async function runModelADeploy(args) {
9375
9897
  function surfaceCommittedHandoff(dispatchOrigin, io, pagesStyle = "unicode") {
9376
9898
  io.out(committedHandoffMessage(dispatchOrigin, pagesStyle));
9377
9899
  }
9900
+ function isIneligible(e) {
9901
+ return !(e instanceof RegistryUnavailableError || e instanceof MigrationSaveError || e instanceof UpgradeRequiredError || e instanceof UnauthenticatedError);
9902
+ }
9903
+ async function defaultToDispatch(cwd, manifestPath, io) {
9904
+ if (!impliesPagesForGateway(manifestPath))
9905
+ return null;
9906
+ try {
9907
+ const result = await convertToDispatch(cwd, false);
9908
+ if (result.kind === "converted")
9909
+ reportConversion(io, result.secrets);
9910
+ return null;
9911
+ } catch (e) {
9912
+ if (e instanceof RegisteredAppError)
9913
+ return null;
9914
+ if (!(e instanceof Error) || !isIneligible(e))
9915
+ throw e;
9916
+ return `Not converted to Workers dispatch: ${describe17(e)}`;
9917
+ }
9918
+ }
9919
+ var CONVERTIBLE_APP_TYPES = new Set(["static", "react", "react+api"]);
9920
+ function impliesPagesForGateway(manifestPath) {
9921
+ const fields = originFields(manifestPath);
9922
+ return CONVERTIBLE_APP_TYPES.has(fields.appType) && fields.origin === undefined && fields.auth === "gateway";
9923
+ }
9924
+ function originFields(manifestPath) {
9925
+ let raw;
9926
+ try {
9927
+ raw = parseYaml6(readFileSync12(manifestPath, "utf8")) ?? {};
9928
+ } catch {
9929
+ return {};
9930
+ }
9931
+ if (raw.apiVersion === "launchpad.m-kopa.us/v2")
9932
+ return raw.spec ?? {};
9933
+ return { origin: raw.origin, auth: raw.auth, appType: raw.deployment?.type };
9934
+ }
9935
+ function reportConversion(io, secrets) {
9936
+ io.out("New apps deploy on Workers dispatch by default, so launchpad.yaml was converted (no Pages project needed).");
9937
+ io.out(` Audience, owner and build settings are preserved. Original: ${BACKUP_PATH}`);
9938
+ io.out(` To stay on Pages instead, restore ${BACKUP_PATH} as launchpad.yaml, set \`origin: pages\` (v1) or \`spec.origin: pages\` (v2) and deploy again.`);
9939
+ const next = secretsNextStep(secrets);
9940
+ if (next !== null)
9941
+ io.out(` ${next}`);
9942
+ }
9378
9943
  function declaredOriginOf(isV22, origin) {
9379
9944
  if (!isV22)
9380
9945
  return "pages";
@@ -9409,7 +9974,7 @@ function pendingReconcileOutcome(body) {
9409
9974
  }
9410
9975
 
9411
9976
  // src/commands/redeploy.ts
9412
- import { existsSync as existsSync8, readFileSync as readFileSync12 } from "node:fs";
9977
+ import { existsSync as existsSync9, readFileSync as readFileSync13 } from "node:fs";
9413
9978
  import { resolve as resolvePath2 } from "node:path";
9414
9979
  import { parse as parseYaml7 } from "yaml";
9415
9980
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
@@ -9487,10 +10052,10 @@ async function runRedeploy(opts, io, deps = {}) {
9487
10052
  }
9488
10053
  function readLocalAppType(cwd, file) {
9489
10054
  const manifestPath = resolvePath2(cwd, file ?? "launchpad.yaml");
9490
- if (!existsSync8(manifestPath))
10055
+ if (!existsSync9(manifestPath))
9491
10056
  return null;
9492
10057
  try {
9493
- const obj = parseYaml7(readFileSync12(manifestPath, "utf8"));
10058
+ const obj = parseYaml7(readFileSync13(manifestPath, "utf8"));
9494
10059
  return coerceAppType(obj?.deployment?.type);
9495
10060
  } catch {
9496
10061
  return null;
@@ -9584,7 +10149,7 @@ function printHelp(io) {
9584
10149
  }
9585
10150
 
9586
10151
  // src/commands/envvars.ts
9587
- import * as path9 from "node:path";
10152
+ import * as path10 from "node:path";
9588
10153
  var envvarsCommand = {
9589
10154
  name: "envvars",
9590
10155
  summary: "list / set / remove production env vars (slug-scoped)",
@@ -9604,7 +10169,7 @@ async function runEnvvars(args, io) {
9604
10169
  } else {
9605
10170
  const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
9606
10171
  if (inferred === null) {
9607
- io.err(`launchpad envvars: could not infer slug from cwd (${path9.basename(process.cwd())});
10172
+ io.err(`launchpad envvars: could not infer slug from cwd (${path10.basename(process.cwd())});
9608
10173
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
9609
10174
  return 64;
9610
10175
  }
@@ -9774,7 +10339,7 @@ function describe18(e) {
9774
10339
  }
9775
10340
 
9776
10341
  // src/commands/generate.ts
9777
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
10342
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9778
10343
  import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
9779
10344
  var generateCommand = {
9780
10345
  name: "generate",
@@ -9824,7 +10389,7 @@ async function runGenerate(args, io) {
9824
10389
  ];
9825
10390
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
9826
10391
  }
9827
- function applyOne(artefact, path10, out, force) {
10392
+ function applyOne(artefact, path11, out, force) {
9828
10393
  if (out.kind === "not-applicable" || out.kind === "unsupported") {
9829
10394
  return {
9830
10395
  artefact,
@@ -9835,32 +10400,32 @@ function applyOne(artefact, path10, out, force) {
9835
10400
  warnings: []
9836
10401
  };
9837
10402
  }
9838
- const existing = readIfExists(path10);
10403
+ const existing = readIfExists(path11);
9839
10404
  if (existing.kind === "read-error") {
9840
10405
  return {
9841
10406
  artefact,
9842
- action: { kind: "write-error", path: path10, message: existing.message },
10407
+ action: { kind: "write-error", path: path11, message: existing.message },
9843
10408
  warnings: out.warnings
9844
10409
  };
9845
10410
  }
9846
10411
  if (existing.kind === "ok" && existing.content === out.content) {
9847
- return { artefact, action: { kind: "unchanged", path: path10 }, warnings: out.warnings };
10412
+ return { artefact, action: { kind: "unchanged", path: path11 }, warnings: out.warnings };
9848
10413
  }
9849
10414
  if (existing.kind === "ok" && existing.content !== out.content && !force) {
9850
10415
  return {
9851
10416
  artefact,
9852
- action: { kind: "would-overwrite", path: path10 },
10417
+ action: { kind: "would-overwrite", path: path11 },
9853
10418
  warnings: out.warnings
9854
10419
  };
9855
10420
  }
9856
10421
  try {
9857
- mkdirSync2(dirname7(path10), { recursive: true });
9858
- writeFileSync2(path10, out.content, "utf8");
10422
+ mkdirSync3(dirname7(path11), { recursive: true });
10423
+ writeFileSync3(path11, out.content, "utf8");
9859
10424
  return {
9860
10425
  artefact,
9861
10426
  action: {
9862
10427
  kind: "written",
9863
- path: path10,
10428
+ path: path11,
9864
10429
  bytes: Buffer.byteLength(out.content, "utf8")
9865
10430
  },
9866
10431
  warnings: out.warnings
@@ -9870,16 +10435,16 @@ function applyOne(artefact, path10, out, force) {
9870
10435
  artefact,
9871
10436
  action: {
9872
10437
  kind: "write-error",
9873
- path: path10,
10438
+ path: path11,
9874
10439
  message: err.message ?? String(err)
9875
10440
  },
9876
10441
  warnings: out.warnings
9877
10442
  };
9878
10443
  }
9879
10444
  }
9880
- function readIfExists(path10) {
10445
+ function readIfExists(path11) {
9881
10446
  try {
9882
- return { kind: "ok", content: readFileSync13(path10, "utf8") };
10447
+ return { kind: "ok", content: readFileSync14(path11, "utf8") };
9883
10448
  } catch (err) {
9884
10449
  const e = err;
9885
10450
  if (e.code === "ENOENT")
@@ -10618,17 +11183,17 @@ function describe21(e) {
10618
11183
 
10619
11184
  // src/commands/init.ts
10620
11185
  import { createInterface } from "node:readline/promises";
10621
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync3 } from "node:fs";
11186
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync4 } from "node:fs";
10622
11187
  import { dirname as dirname8, resolve as resolve11 } from "node:path";
10623
11188
  import { stringify as yamlStringify } from "yaml";
10624
11189
 
10625
11190
  // src/detect/index.ts
10626
- import { existsSync as existsSync9, readFileSync as readFileSync14, statSync } from "node:fs";
11191
+ import { existsSync as existsSync10, readFileSync as readFileSync15, statSync } from "node:fs";
10627
11192
  import { join as join13 } from "node:path";
10628
11193
  function detectAppShape(cwd) {
10629
- const hasPackageJson = existsSync9(join13(cwd, "package.json"));
10630
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join13(cwd, file)));
10631
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join13(cwd, n)));
11194
+ const hasPackageJson = existsSync10(join13(cwd, "package.json"));
11195
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync10(join13(cwd, file)));
11196
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync10(join13(cwd, n)));
10632
11197
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
10633
11198
  return {
10634
11199
  kind: "not-applicable",
@@ -10674,7 +11239,7 @@ var LOCKFILES = [
10674
11239
  { file: "yarn.lock", pm: "yarn" }
10675
11240
  ];
10676
11241
  function detectPackageManager(cwd) {
10677
- const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
11242
+ const present = LOCKFILES.filter(({ file }) => existsSync10(join13(cwd, file)));
10678
11243
  if (present.length === 0) {
10679
11244
  return {
10680
11245
  kind: "ambiguous",
@@ -10697,7 +11262,7 @@ function detectPackageManager(cwd) {
10697
11262
  function detectVitePresence(cwd) {
10698
11263
  for (const name of VITE_CONFIG_NAMES) {
10699
11264
  const p = join13(cwd, name);
10700
- if (existsSync9(p)) {
11265
+ if (existsSync10(p)) {
10701
11266
  return { kind: "ok", value: { path: p } };
10702
11267
  }
10703
11268
  }
@@ -10709,7 +11274,7 @@ function detectVitePresence(cwd) {
10709
11274
  function detectAppType(cwd) {
10710
11275
  const fnDir = join13(cwd, "functions");
10711
11276
  let hasFunctionsDir = false;
10712
- if (existsSync9(fnDir)) {
11277
+ if (existsSync10(fnDir)) {
10713
11278
  try {
10714
11279
  hasFunctionsDir = statSync(fnDir).isDirectory();
10715
11280
  } catch {
@@ -10720,7 +11285,7 @@ function detectAppType(cwd) {
10720
11285
  }
10721
11286
  function detectBuildCommand(cwd, pm) {
10722
11287
  const pkgJsonPath = join13(cwd, "package.json");
10723
- if (!existsSync9(pkgJsonPath)) {
11288
+ if (!existsSync10(pkgJsonPath)) {
10724
11289
  return {
10725
11290
  kind: "ambiguous",
10726
11291
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -10728,7 +11293,7 @@ function detectBuildCommand(cwd, pm) {
10728
11293
  }
10729
11294
  let pkgJson;
10730
11295
  try {
10731
- pkgJson = JSON.parse(readFileSync14(pkgJsonPath, "utf8"));
11296
+ pkgJson = JSON.parse(readFileSync15(pkgJsonPath, "utf8"));
10732
11297
  } catch (e) {
10733
11298
  return {
10734
11299
  kind: "ambiguous",
@@ -10763,7 +11328,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
10763
11328
  function detectDestinationDir(cwd, vite) {
10764
11329
  let text;
10765
11330
  try {
10766
- text = readFileSync14(vite.path, "utf8");
11331
+ text = readFileSync15(vite.path, "utf8");
10767
11332
  } catch (e) {
10768
11333
  return {
10769
11334
  kind: "ambiguous",
@@ -10793,7 +11358,7 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10793
11358
  }
10794
11359
  const { inputs, options } = parsed;
10795
11360
  const outPath = resolve11(process.cwd(), options.out);
10796
- if (existsSync10(outPath) && !options.force) {
11361
+ if (existsSync11(outPath) && !options.force) {
10797
11362
  io.err(`launchpad init: ${outPath} already exists`);
10798
11363
  io.err("Pass --force to overwrite.");
10799
11364
  return 64;
@@ -10830,10 +11395,6 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10830
11395
  return 64;
10831
11396
  }
10832
11397
  const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
10833
- if (dispatch && fillResult.inputs.type === "react+api") {
10834
- io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
10835
- return 64;
10836
- }
10837
11398
  if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
10838
11399
  io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
10839
11400
  return 64;
@@ -10849,7 +11410,7 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10849
11410
  io.out(`Audience: private. Only you (${privateOwner.email}) and platform administrators can open this app.`);
10850
11411
  } else {
10851
11412
  io.out(`Audience: selected Entra groups (${fillResult.inputs.groups.join(", ")}), plus platform administrators.`);
10852
- io.out("Company-wide grants require your explicit browser confirmation before publishing.");
11413
+ io.out("The deploy skill confirms company-wide grants with you. Sensitive apps also require browser confirmation and platform-admin approval.");
10853
11414
  }
10854
11415
  const manifest = buildManifest({ ...fillResult.inputs, ...privateOwner === undefined ? {} : { privateOwner } }, detected);
10855
11416
  const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
@@ -10862,7 +11423,7 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10862
11423
  }
10863
11424
  const yamlText = renderYaml(manifest);
10864
11425
  try {
10865
- writeFileSync3(outPath, yamlText, { encoding: "utf8" });
11426
+ writeFileSync4(outPath, yamlText, { encoding: "utf8" });
10866
11427
  } catch (err) {
10867
11428
  const e = err;
10868
11429
  io.err(`launchpad init: failed to write ${outPath}`);
@@ -10888,10 +11449,10 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10888
11449
  if (fillResult.inputs.type === "static") {
10889
11450
  const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
10890
11451
  const indexPath = resolve11(serveDir, "index.html");
10891
- if (!existsSync10(indexPath)) {
11452
+ if (!existsSync11(indexPath)) {
10892
11453
  try {
10893
- mkdirSync3(serveDir, { recursive: true });
10894
- writeFileSync3(indexPath, starterIndexHtml(fillResult.inputs.name), {
11454
+ mkdirSync4(serveDir, { recursive: true });
11455
+ writeFileSync4(indexPath, starterIndexHtml(fillResult.inputs.name), {
10895
11456
  encoding: "utf8"
10896
11457
  });
10897
11458
  io.out(`✓ scaffolded ${indexPath}`);
@@ -10905,11 +11466,10 @@ async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(),
10905
11466
  io.out("");
10906
11467
  io.out("Next steps:");
10907
11468
  io.out(" launchpad validate # confirm the manifest");
10908
- if (!dispatch)
10909
- io.out(" launchpad secrets template # scaffold .env.example");
11469
+ io.out(" launchpad secrets template # scaffold .env.example");
10910
11470
  io.out(" launchpad deploy # build and deploy the app");
10911
11471
  if (dispatch)
10912
- io.out(" Dispatch currently supports plain-text environment variables, but not secret bindings.");
11472
+ io.out(" Push secrets with `launchpad secrets push` once the first deploy has been delivered.");
10913
11473
  return 0;
10914
11474
  }
10915
11475
  function parseArgs6(args) {
@@ -10961,11 +11521,11 @@ function parseArgs6(args) {
10961
11521
  i++;
10962
11522
  return next;
10963
11523
  };
10964
- const setFromNext = (assign) => {
11524
+ const setFromNext = (assign2) => {
10965
11525
  const got = needsValue();
10966
11526
  if (typeof got !== "string")
10967
11527
  return got;
10968
- assign(got);
11528
+ assign2(got);
10969
11529
  return;
10970
11530
  };
10971
11531
  if (a === "--name") {
@@ -11310,10 +11870,10 @@ function starterIndexHtml(slug) {
11310
11870
  function renderYaml(manifest) {
11311
11871
  return yamlStringify(manifest, { lineWidth: 0 });
11312
11872
  }
11313
- function ensureGitignoreEntries(path10, entries) {
11873
+ function ensureGitignoreEntries(path11, entries) {
11314
11874
  let current = "";
11315
- if (existsSync10(path10)) {
11316
- current = readFileSync15(path10, "utf8");
11875
+ if (existsSync11(path11)) {
11876
+ current = readFileSync16(path11, "utf8");
11317
11877
  }
11318
11878
  const lines = current.split(/\r?\n/);
11319
11879
  const present = new Set(lines.map((l) => l.trim()));
@@ -11331,7 +11891,7 @@ function ensureGitignoreEntries(path10, entries) {
11331
11891
  }
11332
11892
  }
11333
11893
  if (added.length > 0) {
11334
- writeFileSync3(path10, out, { encoding: "utf8" });
11894
+ writeFileSync4(path11, out, { encoding: "utf8" });
11335
11895
  }
11336
11896
  return added;
11337
11897
  }
@@ -11503,7 +12063,7 @@ function describe23(e) {
11503
12063
  }
11504
12064
 
11505
12065
  // src/commands/logs.ts
11506
- import * as path10 from "node:path";
12066
+ import * as path11 from "node:path";
11507
12067
  var logsCommand = {
11508
12068
  name: "logs",
11509
12069
  summary: "show recent Pages deployment history (slug-scoped)",
@@ -11525,7 +12085,7 @@ async function runLogs(args, io) {
11525
12085
  } else {
11526
12086
  const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
11527
12087
  if (inferred === null) {
11528
- io.err(`launchpad logs: could not infer slug from cwd (${path10.basename(process.cwd())});
12088
+ io.err(`launchpad logs: could not infer slug from cwd (${path11.basename(process.cwd())});
11529
12089
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
11530
12090
  return 64;
11531
12091
  }
@@ -11653,7 +12213,7 @@ function describe24(e) {
11653
12213
  }
11654
12214
 
11655
12215
  // src/commands/merge.ts
11656
- import * as path11 from "node:path";
12216
+ import * as path12 from "node:path";
11657
12217
  var mergeCommand = {
11658
12218
  name: "merge",
11659
12219
  summary: "squash-merge a review-passed PR (slug-scoped)",
@@ -11674,7 +12234,7 @@ async function runMerge(args, io) {
11674
12234
  } else {
11675
12235
  const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
11676
12236
  if (inferred === null) {
11677
- io.err(`launchpad merge: could not infer slug from cwd (${path11.basename(process.cwd())});
12237
+ io.err(`launchpad merge: could not infer slug from cwd (${path12.basename(process.cwd())});
11678
12238
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
11679
12239
  return 64;
11680
12240
  }
@@ -11840,178 +12400,6 @@ function describe25(e) {
11840
12400
  return e instanceof Error ? e.message : String(e);
11841
12401
  }
11842
12402
 
11843
- // src/commands/migrate.ts
11844
- import { closeSync, existsSync as existsSync11, lstatSync as lstatSync4, mkdirSync as mkdirSync4, mkdtempSync, openSync, readFileSync as readFileSync16, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
11845
- import path12 from "node:path";
11846
- import { stringify } from "yaml";
11847
-
11848
- // src/manifest/migrate-dispatch.ts
11849
- function dispatchManifest(manifest) {
11850
- const v2 = manifest.apiVersion === "launchpad.m-kopa.us/v2";
11851
- const source = v2 ? manifest.spec : manifest;
11852
- const appType = v2 ? manifest.spec.appType : manifest.deployment.type;
11853
- if (appType !== "static" && appType !== "react") {
11854
- throw new Error("Only static and React apps can use this converter. Pages Functions (react+api) need a compiled Worker build before switching to dispatch; containers use their own runtime.");
11855
- }
11856
- if (source.secrets !== undefined || source.containers !== undefined) {
11857
- throw new Error("Secret and container bindings cannot be converted automatically.");
11858
- }
11859
- if (!v2)
11860
- assertConvertibleV1(manifest);
11861
- const spec = v2 ? { ...manifest.spec } : {
11862
- appType,
11863
- display: {
11864
- name: manifest.metadata.name,
11865
- team: manifest.metadata.team,
11866
- ...manifest.metadata.description === undefined ? {} : { description: manifest.metadata.description }
11867
- },
11868
- owner: { email: manifest.metadata.owner },
11869
- access: {
11870
- audience: manifest.access.audience,
11871
- private_owner: manifest.access.private_owner,
11872
- allowed_entra_groups: [...allowedEntraGroups(manifest.access)],
11873
- identity_types: manifest.access.identity_types
11874
- },
11875
- build: manifest.build,
11876
- app: manifest.app,
11877
- verify: manifest.verify,
11878
- sensitive: manifest.sensitive,
11879
- catalogue: manifest.catalogue,
11880
- ...manifest.production_env === undefined ? {} : {
11881
- env_vars: Object.fromEntries(Object.entries(manifest.production_env).map(([name, value]) => [name, { type: "plain_text", value }]))
11882
- }
11883
- };
11884
- const candidate = {
11885
- apiVersion: "launchpad.m-kopa.us/v2",
11886
- kind: "App",
11887
- metadata: v2 ? manifest.metadata : { slug: manifest.metadata.name, schemaVersion: 2 },
11888
- spec: { ...spec, origin: "dispatch", auth: "gateway", confine_origin: undefined }
11889
- };
11890
- const parsed = parseManifestV2(candidate);
11891
- if (parsed.kind !== "ok") {
11892
- throw new Error(`Cannot convert manifest: ${parsed.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
11893
- }
11894
- return parsed.manifest;
11895
- }
11896
- function assertConvertibleV1(manifest) {
11897
- const unsupported2 = [
11898
- manifest.hostnames !== undefined && "hostnames",
11899
- manifest.targets !== undefined && "targets",
11900
- manifest.access.session_duration !== undefined && "access.session_duration",
11901
- manifest.deployment.runtime !== undefined && "deployment.runtime"
11902
- ].filter(Boolean);
11903
- if (unsupported2.length > 0) {
11904
- throw new Error(`Cannot preserve ${unsupported2.join(", ")} on dispatch. Review these settings with the platform team before migrating.`);
11905
- }
11906
- }
11907
-
11908
- // src/commands/migrate.ts
11909
- var USAGE = "usage: launchpad migrate --origin dispatch [--dry-run]";
11910
- var migrateCommand = {
11911
- name: "migrate",
11912
- summary: "convert an unprovisioned static or React app to Workers dispatch",
11913
- run: runMigrate
11914
- };
11915
- async function runMigrate(args, io) {
11916
- if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
11917
- io.out(USAGE);
11918
- return 0;
11919
- }
11920
- const flags = args.filter((arg) => arg !== "--dry-run");
11921
- if (flags.length !== 2 || flags[0] !== "--origin" || flags[1] !== "dispatch" || args.length > 3) {
11922
- io.err(USAGE);
11923
- return 64;
11924
- }
11925
- const cwd = process.cwd();
11926
- const file = path12.join(cwd, "launchpad.yaml");
11927
- try {
11928
- if (!lstatSync4(file).isFile())
11929
- throw new Error("launchpad.yaml must be a regular file, not a symlink.");
11930
- const original = readFileSync16(file, "utf8");
11931
- const loaded = parseManifestAnyVersion(original, file);
11932
- if (loaded.kind !== "ok")
11933
- throw new Error("launchpad.yaml is invalid. Run `launchpad validate` and correct it first.");
11934
- const manifest = loaded.manifest;
11935
- if (manifest.apiVersion === "launchpad.m-kopa.us/v2" && manifest.spec.origin === "dispatch") {
11936
- io.out("launchpad.yaml already selects Workers dispatch. Run `launchpad deploy`.");
11937
- return 0;
11938
- }
11939
- const converted = dispatchManifest(manifest);
11940
- assertNoCustomRuntime(cwd, converted);
11941
- const slug = converted.metadata.slug;
11942
- await assertUnregistered(slug);
11943
- if (args.includes("--dry-run")) {
11944
- io.out(`${slug}: eligible for v2 Workers dispatch with gateway authentication. No files changed.`);
11945
- return 0;
11946
- }
11947
- saveMigration(file, original, converted);
11948
- io.out(`${slug}: launchpad.yaml now uses v2 Workers dispatch with gateway authentication.`);
11949
- io.out("Audience, owner and application settings are preserved. Original: .launchpad/manifest-before-dispatch.yaml");
11950
- io.out("Run `launchpad deploy`. No Pages project is needed.");
11951
- return 0;
11952
- } catch (error) {
11953
- io.err(`launchpad migrate: ${error instanceof Error ? error.message : String(error)}`);
11954
- return 1;
11955
- }
11956
- }
11957
- async function assertUnregistered(slug) {
11958
- try {
11959
- await apiRaw(loadConfig(), { path: `/apps/${encodeURIComponent(slug)}/manifest/state` });
11960
- } catch (error) {
11961
- if (error instanceof NotFoundError && error.serverCode === "not_found")
11962
- return;
11963
- throw error;
11964
- }
11965
- throw new Error("This app is already registered. This command only converts apps that have never provisioned; ask the platform team to migrate an existing app.");
11966
- }
11967
- function assertNoCustomRuntime(cwd, manifest) {
11968
- const appRoot = path12.resolve(cwd, manifest.spec.app?.root ?? ".");
11969
- const buildRoot = path12.resolve(appRoot, manifest.spec.build?.root_dir ?? ".");
11970
- const roots = new Set([
11971
- cwd,
11972
- appRoot,
11973
- buildRoot,
11974
- path12.resolve(cwd, manifest.spec.build?.root_dir ?? "."),
11975
- path12.resolve(buildRoot, manifest.spec.build?.destination_dir ?? "dist")
11976
- ]);
11977
- for (const root of roots) {
11978
- for (const name of ["functions", "_worker.js", "wrangler.toml", "wrangler.json", "wrangler.jsonc"]) {
11979
- if (existsSync11(path12.join(root, name))) {
11980
- throw new Error(`${name} needs a runtime review before conversion. Pages Functions and custom Worker bindings must not be dropped. Ask the platform team to review this app.`);
11981
- }
11982
- }
11983
- }
11984
- }
11985
- function saveMigration(file, original, converted) {
11986
- if (readFileSync16(file, "utf8") !== original)
11987
- throw new Error("launchpad.yaml changed during the check. Retry migration.");
11988
- const backupDir = path12.join(path12.dirname(file), ".launchpad");
11989
- mkdirSync4(backupDir, { recursive: true });
11990
- const backup = path12.join(backupDir, "manifest-before-dispatch.yaml");
11991
- const attemptDir = mkdtempSync(path12.join(backupDir, "migration-"));
11992
- let backupCreated = false;
11993
- try {
11994
- const fd = openSync(backup, "wx", 384);
11995
- backupCreated = true;
11996
- try {
11997
- writeFileSync4(fd, original);
11998
- } finally {
11999
- closeSync(fd);
12000
- }
12001
- const temporary = path12.join(attemptDir, "launchpad.yaml");
12002
- writeFileSync4(temporary, stringify(converted), { flag: "wx", mode: 384 });
12003
- if (readFileSync16(file, "utf8") !== original)
12004
- throw new Error("launchpad.yaml changed during migration. Retry migration.");
12005
- renameSync(temporary, file);
12006
- } catch (error) {
12007
- if (backupCreated)
12008
- rmSync2(backup);
12009
- throw error;
12010
- } finally {
12011
- rmSync2(attemptDir, { recursive: true, force: true });
12012
- }
12013
- }
12014
-
12015
12403
  // src/commands/plan.ts
12016
12404
  import { resolve as resolve12 } from "node:path";
12017
12405
  var planCommand = {
@@ -13892,6 +14280,22 @@ function parseEnv(text) {
13892
14280
  return { entries, invalidNames, malformedLines };
13893
14281
  }
13894
14282
 
14283
+ // src/secrets/declared.ts
14284
+ function declaredSecrets(loaded) {
14285
+ if (loaded.schema === "launchpad.m-kopa.us/v1alpha1") {
14286
+ const m2 = loaded.manifest;
14287
+ return { slug: m2.metadata.name, apiVersion: m2.apiVersion, bindings: m2.secrets?.bindings ?? [] };
14288
+ }
14289
+ const m = loaded.manifest;
14290
+ const envVars = m.spec.appType === "container" ? {} : m.spec.env_vars ?? {};
14291
+ const fromEnvVars = Object.entries(envVars).filter(([, v]) => v.type === "secret_text").map(([name]) => ({ name, source: "env-file" }));
14292
+ return {
14293
+ slug: m.metadata.slug,
14294
+ apiVersion: m.apiVersion,
14295
+ bindings: [...m.spec.secrets?.bindings ?? [], ...fromEnvVars]
14296
+ };
14297
+ }
14298
+
13895
14299
  // src/secrets/push.ts
13896
14300
  async function runSecretsPush(opts, io, deps = {}) {
13897
14301
  const cfg = deps.config ?? loadConfig();
@@ -13901,14 +14305,10 @@ async function runSecretsPush(opts, io, deps = {}) {
13901
14305
  if (loaded.kind !== "ok") {
13902
14306
  return renderManifestError4(loaded, io);
13903
14307
  }
13904
- const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets push", io);
13905
- if (v2Refusal !== null)
13906
- return v2Refusal;
13907
- const manifest = loaded.manifest;
13908
- const bindings = manifest.secrets?.bindings ?? [];
14308
+ const { slug, bindings } = declaredSecrets(loaded);
13909
14309
  const envFileBindings = bindings.filter((b) => b.source === "env-file");
13910
14310
  if (envFileBindings.length === 0) {
13911
- io.out("No `env-file` secret bindings declared in the manifest.");
14311
+ io.out("No secrets to push: the manifest declares no `env-file` bindings or `secret_text` env vars.");
13912
14312
  io.out("(platform-managed bindings are pushed via the platform-team ops path.)");
13913
14313
  return 0;
13914
14314
  }
@@ -13987,11 +14387,11 @@ async function runSecretsPush(opts, io, deps = {}) {
13987
14387
  io.out("Re-run without --dry-run to push.");
13988
14388
  return 0;
13989
14389
  }
13990
- const slug = manifest.metadata.name;
13991
14390
  let pushed = 0;
13992
14391
  let partial = 0;
13993
14392
  let anyPagesUpdated = false;
13994
14393
  let anyWorkerUpdated = false;
14394
+ let anyDispatchUpdated = false;
13995
14395
  for (const b of envFileBindings) {
13996
14396
  const value = parsed.entries.get(b.name);
13997
14397
  io.out(`Pushing ${b.name} …`);
@@ -14012,6 +14412,11 @@ async function runSecretsPush(opts, io, deps = {}) {
14012
14412
  io.err(`✗ secrets push ${b.name}: invalid bot response (results must be an array)`);
14013
14413
  return 2;
14014
14414
  }
14415
+ if (resp.error === "dispatch_script_not_delivered") {
14416
+ io.err(`✗ secrets push ${b.name}: ${resp.message ?? "the dispatch script is not delivered yet"}`);
14417
+ io.err(`${pushed} of ${envFileBindings.length} pushed before failure.`);
14418
+ return 1;
14419
+ }
14015
14420
  const results = resp.results ?? [];
14016
14421
  for (const r of results) {
14017
14422
  if (r.status !== "updated")
@@ -14020,6 +14425,8 @@ async function runSecretsPush(opts, io, deps = {}) {
14020
14425
  anyPagesUpdated = true;
14021
14426
  else if (r.kind === "worker")
14022
14427
  anyWorkerUpdated = true;
14428
+ else if (r.kind === "dispatch")
14429
+ anyDispatchUpdated = true;
14023
14430
  }
14024
14431
  const notUpdated = results.filter((r) => r.status !== "updated");
14025
14432
  if (resp.updated === true && notUpdated.length === 0) {
@@ -14043,7 +14450,9 @@ async function runSecretsPush(opts, io, deps = {}) {
14043
14450
  if (anyPagesUpdated) {
14044
14451
  return offerRedeploy(slug, opts, io, deps);
14045
14452
  }
14046
- if (anyWorkerUpdated) {
14453
+ if (anyDispatchUpdated) {
14454
+ io.out("Set on the app's dispatch Worker and live now - no redeploy needed.");
14455
+ } else if (anyWorkerUpdated) {
14047
14456
  io.out("These secrets bind on the app's next invocation — no redeploy needed.");
14048
14457
  }
14049
14458
  return 0;
@@ -14112,20 +14521,6 @@ function mapPushError(e, slug, name, io) {
14112
14521
  function describe30(e) {
14113
14522
  return e instanceof Error ? e.message : String(e);
14114
14523
  }
14115
- function refuseV2Manifest(loaded, command, io) {
14116
- if (loaded.schema !== "launchpad.m-kopa.us/v2")
14117
- return null;
14118
- const origin = loaded.manifest.spec?.origin;
14119
- if (origin === "dispatch") {
14120
- io.err(`${command}: this is a dispatch-origin (v2) app.`);
14121
- const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
14122
- io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
14123
- return 1;
14124
- }
14125
- io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
14126
- io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
14127
- return 1;
14128
- }
14129
14524
  function renderManifestError4(result, io) {
14130
14525
  switch (result.kind) {
14131
14526
  case "ok":
@@ -14166,17 +14561,12 @@ async function runSecretsStatus(opts, io, deps = {}) {
14166
14561
  const loaded = loadManifestAnyVersion(manifestPath);
14167
14562
  if (loaded.kind !== "ok")
14168
14563
  return renderManifestError5(loaded, io);
14169
- const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets status", io);
14170
- if (v2Refusal !== null)
14171
- return v2Refusal;
14172
- const manifest = loaded.manifest;
14173
- const slug = manifest.metadata.name;
14564
+ const { slug, bindings } = declaredSecrets(loaded);
14174
14565
  if (opts.slug != null && opts.slug !== slug) {
14175
14566
  io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
14176
14567
  io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
14177
14568
  return 2;
14178
14569
  }
14179
- const bindings = manifest.secrets?.bindings ?? [];
14180
14570
  if (bindings.length === 0) {
14181
14571
  io.out("No secret bindings declared in the manifest.");
14182
14572
  return 0;
@@ -14526,12 +14916,12 @@ async function runSecretsTemplate(args, io) {
14526
14916
  return 64;
14527
14917
  }
14528
14918
  const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
14529
- const result = loadManifest(manifestPath);
14530
- const renderResult = renderManifest(result, io);
14919
+ const renderResult = renderManifest(loadManifestAnyVersion(manifestPath), io);
14531
14920
  if (renderResult.kind !== "ok") {
14532
14921
  return renderResult.code;
14533
14922
  }
14534
- const body = buildTemplate(renderResult.manifest, flags.includePlatformManaged);
14923
+ const declared = renderResult.declared;
14924
+ const body = buildTemplate(declared, flags.includePlatformManaged);
14535
14925
  if (flags.toStdout) {
14536
14926
  for (const line of body.split(`
14537
14927
  `)) {
@@ -14554,14 +14944,14 @@ async function runSecretsTemplate(args, io) {
14554
14944
  return 2;
14555
14945
  }
14556
14946
  io.out(`✓ wrote ${outPath}`);
14557
- const bindings = result.kind === "ok" ? countBindings(result.manifest) : { total: 0, written: 0 };
14558
- io.out(` ${bindings.written} binding${bindings.written === 1 ? "" : "s"} scaffolded`);
14947
+ const written = declared.bindings.filter((b) => b.source === "env-file").length;
14948
+ io.out(` ${written} binding${written === 1 ? "" : "s"} scaffolded`);
14559
14949
  return 0;
14560
14950
  }
14561
14951
  function renderManifest(result, io) {
14562
14952
  switch (result.kind) {
14563
14953
  case "ok":
14564
- return { kind: "ok", manifest: result.manifest };
14954
+ return { kind: "ok", declared: declaredSecrets(result) };
14565
14955
  case "file-not-found":
14566
14956
  io.err(`launchpad secrets template: ${result.path}`);
14567
14957
  io.err(" manifest not found");
@@ -14583,19 +14973,19 @@ function renderManifest(result, io) {
14583
14973
  return { kind: "fail", code: 1 };
14584
14974
  }
14585
14975
  }
14586
- function buildTemplate(manifest, includePlatformManaged) {
14587
- const bindings = manifest.secrets?.bindings ?? [];
14976
+ function buildTemplate(declared, includePlatformManaged) {
14977
+ const { bindings } = declared;
14588
14978
  const lines = [];
14589
14979
  lines.push("# Auto-generated by `launchpad secrets template` — do not commit values.");
14590
- lines.push(`# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})`);
14591
- lines.push(`# App: ${manifest.metadata.name}`);
14980
+ lines.push(`# Source: launchpad.yaml (apiVersion: ${declared.apiVersion})`);
14981
+ lines.push(`# App: ${declared.slug}`);
14592
14982
  lines.push("#");
14593
14983
  lines.push("# Fill in values, then run `launchpad secrets push`.");
14594
14984
  lines.push("# This file is meant to be committed; the real `.env` must be gitignored.");
14595
14985
  lines.push("");
14596
14986
  if (bindings.length === 0) {
14597
14987
  lines.push("# No secret bindings declared in launchpad.yaml.");
14598
- lines.push("# Add entries under `secrets.bindings[]` and re-run this command.");
14988
+ lines.push("# Add entries under `secrets.bindings[]` (v1alpha1) or `spec.env_vars` with " + "`type: secret_text` (v2) and re-run this command.");
14599
14989
  lines.push("");
14600
14990
  return lines.join(`
14601
14991
  `);
@@ -14629,11 +15019,6 @@ function buildTemplate(manifest, includePlatformManaged) {
14629
15019
  return lines.join(`
14630
15020
  `);
14631
15021
  }
14632
- function countBindings(manifest) {
14633
- const bindings = manifest.secrets?.bindings ?? [];
14634
- const written = bindings.filter((b) => b.source === "env-file").length;
14635
- return { total: bindings.length, written };
14636
- }
14637
15022
  function parseFlags4(args) {
14638
15023
  let file;
14639
15024
  let out = ".env.example";
@@ -14842,7 +15227,7 @@ import { promises as fs7, existsSync as existsSync16 } from "node:fs";
14842
15227
  // src/skills-bundle.ts
14843
15228
  import { homedir as homedir2 } from "node:os";
14844
15229
  import { join as join15 } from "node:path";
14845
- import { readFileSync as readFileSync20, readdirSync as readdirSync2 } from "node:fs";
15230
+ import { readFileSync as readFileSync20, readdirSync as readdirSync3 } from "node:fs";
14846
15231
  var SKILL_PREFIX = "launchpad-";
14847
15232
  function skillsTargetDir() {
14848
15233
  return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
@@ -14850,7 +15235,7 @@ function skillsTargetDir() {
14850
15235
  function readInstalledSkills() {
14851
15236
  try {
14852
15237
  const dir = skillsTargetDir();
14853
- const bundle = readdirSync2(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
15238
+ const bundle = readdirSync3(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
14854
15239
  if (!bundle)
14855
15240
  return { present: false, version: null };
14856
15241
  return {
@@ -15540,7 +15925,7 @@ function printHelp5(io) {
15540
15925
  }
15541
15926
 
15542
15927
  // src/commands/validate.ts
15543
- import { existsSync as existsSync17, readFileSync as readFileSync22, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
15928
+ import { existsSync as existsSync17, readFileSync as readFileSync22, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
15544
15929
  import { dirname as dirname12, resolve as resolve17 } from "node:path";
15545
15930
  var validateCommand = {
15546
15931
  name: "validate",
@@ -15631,7 +16016,7 @@ function checkStaticServeDir(manifestPath, manifest) {
15631
16016
  }
15632
16017
  let entries;
15633
16018
  try {
15634
- entries = readdirSync3(abs);
16019
+ entries = readdirSync4(abs);
15635
16020
  } catch (e) {
15636
16021
  return [`failed to read static served directory '${serveDir}/': ${describe32(e)}`];
15637
16022
  }