@m-kopa/launchpad-cli 0.54.1 → 0.56.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bundle/upload.d.ts +4 -0
  3. package/dist/bundle/upload.d.ts.map +1 -1
  4. package/dist/cli.js +1272 -591
  5. package/dist/commands/admin.d.ts +10 -0
  6. package/dist/commands/admin.d.ts.map +1 -0
  7. package/dist/commands/deploy-flags.d.ts +2 -0
  8. package/dist/commands/deploy-flags.d.ts.map +1 -1
  9. package/dist/commands/deploy.d.ts.map +1 -1
  10. package/dist/commands/generate.d.ts.map +1 -1
  11. package/dist/commands/plan.d.ts.map +1 -1
  12. package/dist/commands/status.d.ts +5 -0
  13. package/dist/commands/status.d.ts.map +1 -1
  14. package/dist/commands/validate.d.ts.map +1 -1
  15. package/dist/config.d.ts +6 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/deploy/access-drift-offer.d.ts +2 -0
  18. package/dist/deploy/access-drift-offer.d.ts.map +1 -1
  19. package/dist/deploy/apply.d.ts +2 -0
  20. package/dist/deploy/apply.d.ts.map +1 -1
  21. package/dist/deploy/deployment-status.d.ts.map +1 -1
  22. package/dist/deploy/manifest-state.d.ts +2 -1
  23. package/dist/deploy/manifest-state.d.ts.map +1 -1
  24. package/dist/deploy/redact-diagnostic.d.ts.map +1 -1
  25. package/dist/dispatcher.d.ts.map +1 -1
  26. package/dist/manifest/container-filesystem.d.ts +19 -0
  27. package/dist/manifest/container-filesystem.d.ts.map +1 -0
  28. package/dist/manifest/runtime-capability.d.ts +22 -0
  29. package/dist/manifest/runtime-capability.d.ts.map +1 -0
  30. package/dist/status-page-output.d.ts +12 -0
  31. package/dist/status-page-output.d.ts.map +1 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/watch/index.d.ts +3 -0
  34. package/dist/watch/index.d.ts.map +1 -1
  35. package/dist/watch/model.d.ts +1 -0
  36. package/dist/watch/model.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/skills/launchpad-content-pr/SKILL.md +1 -1
  39. package/skills/launchpad-deploy/SKILL.md +1 -1
  40. package/skills/launchpad-deploy-status/SKILL.md +1 -1
  41. package/skills/launchpad-destroy/SKILL.md +1 -1
  42. package/skills/launchpad-identity/SKILL.md +1 -1
  43. package/skills/launchpad-onboard/SKILL.md +1 -1
  44. package/skills/launchpad-report/SKILL.md +1 -1
  45. package/skills/launchpad-status/SKILL.md +2 -2
package/dist/cli.js CHANGED
@@ -19,34 +19,49 @@ 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.54.1";
22
+ var CLI_VERSION = "0.56.0";
23
23
 
24
24
  // src/config.ts
25
+ import { readFileSync } from "node:fs";
25
26
  import * as os from "node:os";
26
27
  import * as path from "node:path";
27
28
  var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
28
29
  var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
30
+ var DEFAULT_STATUS_PAGE_HOST = "status.launchpad.m-kopa.us";
29
31
  function loadConfig(env = process.env) {
30
32
  const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
31
33
  const botUrl = rawBotUrl.replace(/\/+$/, "");
32
34
  const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
33
35
  const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
36
+ const statusPageHost = (env.LAUNCHPAD_STATUS_PAGE_HOST ?? DEFAULT_STATUS_PAGE_HOST).trim().toLowerCase();
34
37
  const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
35
38
  const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
36
39
  const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
37
40
  const stateDir = env.LAUNCHPAD_STATE_DIR ?? path.join(os.homedir(), ".launchpad", "state");
38
41
  const rawPlatformRepo = env.LAUNCHPAD_PLATFORM_REPO;
39
42
  const platformRepoPath = rawPlatformRepo !== undefined && rawPlatformRepo.length > 0 ? path.resolve(rawPlatformRepo) : null;
43
+ const configPath = env.LAUNCHPAD_CONFIG_PATH ?? path.join(os.homedir(), ".launchpad", "config.json");
44
+ const statusPageAutoOpen = readStatusPageAutoOpen(configPath);
40
45
  return {
41
46
  botUrl,
42
47
  authGatewayUrl,
48
+ statusPageHost,
43
49
  authLegacy,
44
50
  sessionPath,
45
51
  cacheDir,
46
52
  stateDir,
47
- platformRepoPath
53
+ platformRepoPath,
54
+ statusPageAutoOpen
48
55
  };
49
56
  }
57
+ function readStatusPageAutoOpen(configPath) {
58
+ try {
59
+ const value = JSON.parse(readFileSync(configPath, "utf8"));
60
+ return !(typeof value === "object" && value !== null && value.statusPageAutoOpen === false);
61
+ } catch (error) {
62
+ return error.code === "ENOENT";
63
+ }
64
+ }
50
65
 
51
66
  // src/http/error-envelope.ts
52
67
  var MAX_TEXT_CHARS = 16384;
@@ -1871,7 +1886,7 @@ import { existsSync as existsSync7 } from "node:fs";
1871
1886
  import * as path8 from "node:path";
1872
1887
 
1873
1888
  // src/bundle/orchestrate.ts
1874
- import { readFileSync as readFileSync4 } from "node:fs";
1889
+ import { readFileSync as readFileSync5 } from "node:fs";
1875
1890
  import { join as join7 } from "node:path";
1876
1891
 
1877
1892
  // src/deploy/tar-pack.ts
@@ -2183,7 +2198,8 @@ function deployBundleError(envelope, body) {
2183
2198
  ...paths === undefined ? {} : { paths },
2184
2199
  ...typeof body.reason === "string" ? { reason: body.reason } : {},
2185
2200
  ...typeof body.minimum === "string" ? { minimum: body.minimum } : {},
2186
- ...typeof body.yours === "string" ? { yours: body.yours } : {}
2201
+ ...typeof body.yours === "string" ? { yours: body.yours } : {},
2202
+ ...typeof body.statusPageUrl === "string" ? { statusPageUrl: body.statusPageUrl } : {}
2187
2203
  };
2188
2204
  }
2189
2205
  function readStringArray(value) {
@@ -2207,7 +2223,7 @@ function isRecord2(value) {
2207
2223
  }
2208
2224
 
2209
2225
  // src/bundle/cwd-walker.ts
2210
- import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2226
+ import { existsSync, lstatSync, readFileSync as readFileSync2, readdirSync } from "node:fs";
2211
2227
  import { join as join4, relative as relative2, sep } from "node:path";
2212
2228
  var DEFAULT_IGNORE = [
2213
2229
  ".git",
@@ -2313,7 +2329,7 @@ function loadIgnore(cwd) {
2313
2329
  }
2314
2330
  let content;
2315
2331
  try {
2316
- content = readFileSync(file, "utf8");
2332
+ content = readFileSync2(file, "utf8");
2317
2333
  } catch {
2318
2334
  return { matches: () => false };
2319
2335
  }
@@ -2387,7 +2403,7 @@ function globToRegExp(glob) {
2387
2403
  }
2388
2404
 
2389
2405
  // src/bundle/cron-bundle.ts
2390
- import { readFileSync as readFileSync2 } from "node:fs";
2406
+ import { readFileSync as readFileSync3 } from "node:fs";
2391
2407
  import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
2392
2408
  import { parse as parseYaml } from "yaml";
2393
2409
  import { build as esbuild } from "esbuild";
@@ -2467,7 +2483,7 @@ function locateWorkerConfig(cwd, walkFiles, script) {
2467
2483
  const abs = resolve5(cwd, rel);
2468
2484
  let text;
2469
2485
  try {
2470
- text = readFileSync2(abs, "utf8");
2486
+ text = readFileSync3(abs, "utf8");
2471
2487
  } catch {
2472
2488
  continue;
2473
2489
  }
@@ -2546,7 +2562,7 @@ async function buildWorkerArtifact(cwd, manifestYaml, walkFiles) {
2546
2562
  }
2547
2563
 
2548
2564
  // src/bundle/boundary.ts
2549
- import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
2565
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync4 } from "node:fs";
2550
2566
  import { join as join6 } from "node:path";
2551
2567
  import { parse as parseYaml2 } from "yaml";
2552
2568
 
@@ -3469,6 +3485,19 @@ var ManifestV2Schema = z2.object({
3469
3485
  spec: SpecSchema,
3470
3486
  status: StatusSchema.optional()
3471
3487
  }).strict();
3488
+ function parseManifestV2(input) {
3489
+ const result = ManifestV2Schema.safeParse(input);
3490
+ if (result.success) {
3491
+ return { kind: "ok", manifest: result.data };
3492
+ }
3493
+ return {
3494
+ kind: "schema-error",
3495
+ issues: result.error.issues.map((issue) => ({
3496
+ path: issue.path.map(String).join(".") || "(root)",
3497
+ message: issue.message
3498
+ }))
3499
+ };
3500
+ }
3472
3501
  // ../launchpad-engine/dist/secrets-audit.js
3473
3502
  function auditSecretPresence(bindings, surfaces) {
3474
3503
  const byId = new Map(surfaces.map((s) => [s.id, s]));
@@ -3588,298 +3617,569 @@ function deriveResources(m) {
3588
3617
  }
3589
3618
  return items;
3590
3619
  }
3591
- // ../launchpad-engine/dist/wrangler.js
3592
- var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
3593
- var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
3594
- function generateWranglerToml(manifest) {
3595
- if (manifest.deployment.type !== "container") {
3620
+ // ../launchpad-engine/dist/container-runtime-capability.js
3621
+ var MAX_PATH_BYTES = 256;
3622
+ var MAX_ENV_NAMES = 64;
3623
+ var MAX_ENV_NAME_BYTES = 64;
3624
+ var CONTAINER_NAME = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3625
+ var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
3626
+ var IMAGE_SEGMENT = /^[A-Za-z0-9._-]+$/;
3627
+ var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
3628
+ var RESERVED_ENV_PREFIXES = ["LAUNCHPAD_", "CF_ACCESS_", "CLOUDFLARE_"];
3629
+ var RESERVED_ENV_NAMES = new Set([
3630
+ "BINDINGS_PROXY_SECRET",
3631
+ "BINDINGS_PROXY_URL"
3632
+ ]);
3633
+ function validateDeploymentCapability(manifest) {
3634
+ const view = deploymentView(manifest);
3635
+ if (view.type !== "container") {
3596
3636
  return {
3597
- kind: "not-applicable",
3598
- reason: `deployment.type '${manifest.deployment.type}' does not use wrangler.toml`
3637
+ kind: "supported",
3638
+ manifestValid: true,
3639
+ runtimeSupported: true,
3640
+ runtime: "pages"
3599
3641
  };
3600
3642
  }
3601
- const containers = manifest.containers ?? [];
3602
- if (containers.length === 0) {
3643
+ if (view.containers.length !== 1) {
3644
+ return unsupported("unsupported_container_topology", "containers", `the container runtime supports exactly one container; found ${view.containers.length}`);
3645
+ }
3646
+ const container = view.containers[0];
3647
+ if (byteLength(container.name) > 32 || container.name.length < 2 || !CONTAINER_NAME.test(container.name)) {
3648
+ return unsupported("invalid_container_name", "containers.0.name", "container name must be a 2-32 character lowercase slug");
3649
+ }
3650
+ if (container.port < 1024 || container.port > 65535) {
3651
+ return unsupported("unsupported_container_port", "containers.0.port", "container port must be between 1024 and 65535");
3652
+ }
3653
+ if (!validImagePath(container.image)) {
3654
+ return unsupported("invalid_container_image", "containers.0.image", "container image must be a contained repository-relative Dockerfile path beginning ./");
3655
+ }
3656
+ if (!validHealthcheck(container.healthcheck)) {
3657
+ return unsupported("invalid_container_healthcheck", "containers.0.healthcheck", "container healthcheck must be a simple absolute path without traversal, query, or fragment");
3658
+ }
3659
+ const environmentNames = container.env ?? [];
3660
+ if (environmentNames.length > MAX_ENV_NAMES) {
3661
+ return unsupported("too_many_container_environment_names", "containers.0.env", `container env may contain at most ${MAX_ENV_NAMES} names`);
3662
+ }
3663
+ const seen = new Set;
3664
+ const bindingName = containerBindingName(container.name);
3665
+ for (let index = 0;index < environmentNames.length; index += 1) {
3666
+ const name = environmentNames[index];
3667
+ const path7 = `containers.0.env.${index}`;
3668
+ if (byteLength(name) > MAX_ENV_NAME_BYTES || !ENV_NAME.test(name)) {
3669
+ return unsupported("invalid_container_environment_name", path7, "container environment names must be at most 64 bytes and use UPPER_SNAKE_CASE");
3670
+ }
3671
+ if (seen.has(name)) {
3672
+ return unsupported("duplicate_container_environment_name", path7, `container environment name '${name}' is duplicated`);
3673
+ }
3674
+ seen.add(name);
3675
+ if (RESERVED_ENV_NAMES.has(name) || RESERVED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)) || name === bindingName) {
3676
+ return unsupported("reserved_container_environment_name", path7, `container environment name '${name}' is owned by the Launchpad runtime`);
3677
+ }
3678
+ }
3679
+ return {
3680
+ kind: "supported",
3681
+ manifestValid: true,
3682
+ runtimeSupported: true,
3683
+ runtime: "container",
3684
+ capability: {
3685
+ appName: view.appName,
3686
+ apiVersion: manifest.apiVersion,
3687
+ container,
3688
+ className: containerClassName(view.appName),
3689
+ bindingName,
3690
+ dockerfilePathFromContainerDirectory: `../${container.image.slice(2)}`,
3691
+ environmentNames
3692
+ }
3693
+ };
3694
+ }
3695
+ function containerClassName(containerName) {
3696
+ return `AppContainer${containerName.split("-").map(capitalise).join("")}`;
3697
+ }
3698
+ function containerBindingName(containerName) {
3699
+ return `APP_CONTAINER_${containerName.split("-").map((part) => part.toUpperCase()).join("_")}`;
3700
+ }
3701
+ function deploymentView(manifest) {
3702
+ if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
3603
3703
  return {
3604
- kind: "ok",
3605
- content: emit(manifest, []),
3606
- warnings: ["container manifest has no containers[] — emitting empty TOML"]
3704
+ type: manifest.spec.appType,
3705
+ appName: manifest.metadata.slug,
3706
+ containers: manifest.spec.containers ?? []
3607
3707
  };
3608
3708
  }
3609
- return { kind: "ok", content: emit(manifest, containers), warnings: [] };
3709
+ return {
3710
+ type: manifest.deployment.type,
3711
+ appName: manifest.metadata.name,
3712
+ containers: manifest.containers ?? []
3713
+ };
3610
3714
  }
3611
- function emit(manifest, containers) {
3612
- const lines = [];
3613
- const header = `# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.
3614
- # Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})
3615
- # App: ${manifest.metadata.name}
3616
- #
3617
- # Regenerate with \`launchpad generate\` (\`launchpad deploy\` also
3618
- # re-emits before applying). Manual edits are overwritten on the
3619
- # next regeneration. Manifest is the source of truth — change
3620
- # ${manifest.metadata.name}/launchpad.yaml instead.`;
3621
- lines.push(header);
3622
- lines.push("");
3623
- lines.push(`name = "${escapeTomlString(manifest.metadata.name)}"`);
3624
- lines.push(`main = "worker/index.ts"`);
3625
- lines.push(`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`);
3626
- lines.push(`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((f) => `"${escapeTomlString(f)}"`).join(", ")}]`);
3627
- lines.push("");
3628
- lines.push("# Workers Logs (PATTERNS.md: non-optional for container apps).");
3629
- lines.push("[observability]");
3630
- lines.push("enabled = true");
3631
- lines.push("head_sampling_rate = 1.0");
3632
- lines.push("");
3633
- lines.push("# Platform-managed runtime config. Populated by the deploy");
3634
- lines.push("# workflow via `wrangler deploy --var KEY:VALUE`. Secrets");
3635
- lines.push("# (declared in launchpad.yaml secrets.bindings[]) go through");
3636
- lines.push("# `wrangler secret put` and are NOT here.");
3637
- lines.push("[vars]");
3638
- lines.push(`CF_ACCESS_AUD = ""`);
3639
- lines.push(`CF_ACCESS_TEAM_DOMAIN = ""`);
3640
- lines.push("");
3641
- for (const c of containers) {
3642
- const className = containerClassName(c.name);
3643
- const maxInstances = c.max_instances ?? 1;
3644
- lines.push("[[containers]]");
3645
- lines.push(`class_name = "${className}"`);
3646
- lines.push(`image = "${escapeTomlString(c.image)}"`);
3647
- lines.push(`max_instances = ${maxInstances}`);
3648
- lines.push("");
3715
+ function validImagePath(path7) {
3716
+ if (byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("./") || path7.includes("\\") || CONTROL_CHARACTER.test(path7)) {
3717
+ return false;
3649
3718
  }
3650
- for (const c of containers) {
3651
- const className = containerClassName(c.name);
3652
- const bindingName = containerBindingName(c.name);
3653
- lines.push("[[durable_objects.bindings]]");
3654
- lines.push(`name = "${bindingName}"`);
3655
- lines.push(`class_name = "${className}"`);
3656
- lines.push("");
3719
+ const segments = path7.slice(2).split("/");
3720
+ return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== ".." && IMAGE_SEGMENT.test(segment));
3721
+ }
3722
+ function validHealthcheck(path7) {
3723
+ if (byteLength(path7) === 0 || byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("/") || path7.includes("\\") || path7.includes("?") || path7.includes("#") || CONTROL_CHARACTER.test(path7)) {
3724
+ return false;
3657
3725
  }
3658
- lines.push("# Fresh-deploy migration. Container-enabled DOs MUST be");
3659
- lines.push("# SQLite-backed (ADR 0015 #4/#5 — non-sqlite fails [code: 10074]).");
3660
- lines.push("# Re-shaping containers on a live app needs a v2 migration");
3661
- lines.push("# block; the engine's TF-state-aware migration generator lands");
3662
- lines.push("# with Scope 8 drift detection.");
3663
- lines.push("[[migrations]]");
3664
- lines.push(`tag = "v1"`);
3665
- const classNames = containers.map((c) => `"${containerClassName(c.name)}"`);
3666
- lines.push(`new_sqlite_classes = [${classNames.join(", ")}]`);
3667
- lines.push("");
3726
+ const segments = path7.slice(1).split("/");
3727
+ return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
3728
+ }
3729
+ function byteLength(value) {
3730
+ return new TextEncoder().encode(value).byteLength;
3731
+ }
3732
+ function capitalise(value) {
3733
+ if (value.length === 0)
3734
+ return value;
3735
+ return value[0].toUpperCase() + value.slice(1);
3736
+ }
3737
+ function unsupported(reason, path7, message) {
3738
+ return {
3739
+ kind: "unsupported",
3740
+ manifestValid: true,
3741
+ runtimeSupported: false,
3742
+ reason,
3743
+ path: path7,
3744
+ message
3745
+ };
3746
+ }
3747
+ // ../launchpad-engine/dist/container-worker.js
3748
+ function generateContainerWorker(manifest) {
3749
+ const result = validateDeploymentCapability(manifest);
3750
+ if (result.kind === "unsupported") {
3751
+ return {
3752
+ kind: "unsupported",
3753
+ reason: result.reason,
3754
+ message: result.message
3755
+ };
3756
+ }
3757
+ if (result.runtime !== "container") {
3758
+ return {
3759
+ kind: "not-applicable",
3760
+ reason: "pages runtimes do not use the generated container Worker"
3761
+ };
3762
+ }
3763
+ return { kind: "ok", content: emit(result.capability) };
3764
+ }
3765
+ function emit(capability) {
3766
+ const app = JSON.stringify(capability.appName);
3767
+ const className = capability.className;
3768
+ const bindingName = capability.bindingName;
3769
+ const environmentFields = capability.environmentNames.map((name) => ` ${name}: string;`);
3770
+ const environmentValues = capability.environmentNames.map((name) => ` ${name}: this.env.${name} ?? "",`);
3771
+ const lines = [
3772
+ "/**",
3773
+ " * Auto-generated by @m-kopa/launchpad-engine.",
3774
+ " * Source: launchpad.yaml. Do not edit by hand.",
3775
+ " */",
3776
+ 'import { Container } from "@cloudflare/containers";',
3777
+ "import {",
3778
+ " AccessJwtVerificationError,",
3779
+ " createJwksCache,",
3780
+ " parseAccessJwtFromHeaders,",
3781
+ " verifyAccessJwt,",
3782
+ " type User,",
3783
+ '} from "@m-kopa/platform-auth";',
3784
+ "",
3785
+ "type Env = {",
3786
+ ` ${bindingName}: DurableObjectNamespace<${className}>;`,
3787
+ " BINDINGS_PROXY_SECRET: string;",
3788
+ " CF_ACCESS_AUD: string;",
3789
+ " CF_ACCESS_TEAM_DOMAIN: string;",
3790
+ " LAUNCHPAD_DEPLOYMENT_SHA: string;",
3791
+ ...environmentFields,
3792
+ "};",
3793
+ "",
3794
+ `export class ${className} extends Container<Env> {`,
3795
+ ` defaultPort = ${capability.container.port};`,
3796
+ ' sleepAfter = "5m";',
3797
+ ...environmentValues.length === 0 ? [] : ["", " envVars = {", ...environmentValues, " };"],
3798
+ "}",
3799
+ "",
3800
+ "let jwksCache: ReturnType<typeof createJwksCache> | null = null;",
3801
+ "",
3802
+ "function jwks(env: Env): ReturnType<typeof createJwksCache> {",
3803
+ " if (jwksCache === null) {",
3804
+ " jwksCache = createJwksCache(",
3805
+ " `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
3806
+ " );",
3807
+ " }",
3808
+ " return jwksCache;",
3809
+ "}",
3810
+ "",
3811
+ "function unauthenticated(code: string): Response {",
3812
+ " return Response.json(",
3813
+ ' { error: "unauthenticated", code },',
3814
+ ' { status: 401, headers: { "cache-control": "no-store" } },',
3815
+ " );",
3816
+ "}",
3817
+ "",
3818
+ "async function secretsEqual(left: string, right: string): Promise<boolean> {",
3819
+ " const encoder = new TextEncoder();",
3820
+ " const [leftDigest, rightDigest] = await Promise.all([",
3821
+ ' crypto.subtle.digest("SHA-256", encoder.encode(left)),',
3822
+ ' crypto.subtle.digest("SHA-256", encoder.encode(right)),',
3823
+ " ]);",
3824
+ " const leftBytes = new Uint8Array(leftDigest);",
3825
+ " const rightBytes = new Uint8Array(rightDigest);",
3826
+ " let difference = 0;",
3827
+ " for (let index = 0; index < leftBytes.length; index += 1) {",
3828
+ " difference |= leftBytes[index]! ^ rightBytes[index]!;",
3829
+ " }",
3830
+ " return difference === 0;",
3831
+ "}",
3832
+ "",
3833
+ "async function authenticate(request: Request, env: Env): Promise<User | Response> {",
3834
+ " if (!env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
3835
+ ' return unauthenticated("auth_unconfigured");',
3836
+ " }",
3837
+ " const token = parseAccessJwtFromHeaders(request.headers);",
3838
+ " if (token === null) {",
3839
+ ' return unauthenticated("access_jwt_missing");',
3840
+ " }",
3841
+ " try {",
3842
+ " return await verifyAccessJwt(token, {",
3843
+ " teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
3844
+ " audience: env.CF_ACCESS_AUD,",
3845
+ " jwks: jwks(env),",
3846
+ " clockTolerance: 30,",
3847
+ " });",
3848
+ " } catch (error) {",
3849
+ " if (error instanceof AccessJwtVerificationError) {",
3850
+ " return unauthenticated(error.code);",
3851
+ " }",
3852
+ ' console.error(JSON.stringify({ event: "container_runtime_error", kind: "access_jwt_verification_failed" }));',
3853
+ ' return unauthenticated("access_jwt_verification_failed");',
3854
+ " }",
3855
+ "}",
3856
+ "",
3857
+ "function forwardedRequest(",
3858
+ " request: Request,",
3859
+ " user: User,",
3860
+ " env: Env,",
3861
+ " requestId: string,",
3862
+ "): Request {",
3863
+ " const url = new URL(request.url);",
3864
+ " const headers = new Headers(request.headers);",
3865
+ " for (const name of Array.from(headers.keys())) {",
3866
+ " const lower = name.toLowerCase();",
3867
+ " if (",
3868
+ ' lower.startsWith("cf-access-") ||',
3869
+ ' lower.startsWith("x-user-") ||',
3870
+ ' lower.startsWith("x-bindings-proxy-") ||',
3871
+ ' lower === "x-request-id"',
3872
+ " ) {",
3873
+ " headers.delete(name);",
3874
+ " }",
3875
+ " }",
3876
+ ' headers.set("X-User-Sub", user.sub);',
3877
+ ' headers.set("X-User-Email", user.preferredUsername);',
3878
+ " if (user.groups.length > 0) {",
3879
+ ' headers.set("X-User-Groups", user.groups.join(","));',
3880
+ " }",
3881
+ ' headers.set("X-Request-ID", requestId);',
3882
+ ' headers.set("X-Bindings-Proxy-Secret", env.BINDINGS_PROXY_SECRET);',
3883
+ ' headers.set("X-Bindings-Proxy-Url", `${url.origin}/__platform/bindings`);',
3884
+ " return new Request(url.toString(), {",
3885
+ " method: request.method,",
3886
+ " headers,",
3887
+ ' body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,',
3888
+ " redirect: request.redirect,",
3889
+ " });",
3890
+ "}",
3891
+ "",
3892
+ "function runtimeEvent(",
3893
+ " env: Env,",
3894
+ " requestId: string,",
3895
+ " status: number,",
3896
+ " kind: string,",
3897
+ "): void {",
3898
+ " console.log(JSON.stringify({",
3899
+ ' event: "container_runtime_dispatch",',
3900
+ ` slug: ${app},`,
3901
+ " deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
3902
+ " request_id: requestId,",
3903
+ ` class_name: ${JSON.stringify(className)},`,
3904
+ ` binding_name: ${JSON.stringify(bindingName)},`,
3905
+ " status,",
3906
+ " kind,",
3907
+ " }));",
3908
+ "}",
3909
+ "",
3910
+ "export default {",
3911
+ " async fetch(request: Request, env: Env): Promise<Response> {",
3912
+ " const url = new URL(request.url);",
3913
+ " const requestId = crypto.randomUUID();",
3914
+ ' if (request.method === "GET" && url.pathname === "/health") {',
3915
+ ' const response = Response.json({ ok: true, role: "worker" });',
3916
+ ' runtimeEvent(env, requestId, response.status, "worker_health");',
3917
+ " return response;",
3918
+ " }",
3919
+ ' if (url.pathname === "/__platform/bindings" || url.pathname.startsWith("/__platform/bindings/")) {',
3920
+ ' const presented = request.headers.get("x-bindings-proxy-secret");',
3921
+ " if (!env.BINDINGS_PROXY_SECRET || !presented || !(await secretsEqual(presented, env.BINDINGS_PROXY_SECRET))) {",
3922
+ ' const response = Response.json({ error: "forbidden" }, { status: 403 });',
3923
+ ' runtimeEvent(env, requestId, response.status, "bindings_forbidden");',
3924
+ " return response;",
3925
+ " }",
3926
+ ' const response = Response.json({ error: "unknown_binding_endpoint" }, { status: 404 });',
3927
+ ' runtimeEvent(env, requestId, response.status, "bindings_unknown");',
3928
+ " return response;",
3929
+ " }",
3930
+ " const identity = await authenticate(request, env);",
3931
+ " if (identity instanceof Response) {",
3932
+ ' runtimeEvent(env, requestId, identity.status, "authentication_failed");',
3933
+ " return identity;",
3934
+ " }",
3935
+ ` const id = env.${bindingName}.idFromName("singleton");`,
3936
+ ` const response = await env.${bindingName}.get(id).fetch(`,
3937
+ " forwardedRequest(request, identity, env, requestId),",
3938
+ " );",
3939
+ ' runtimeEvent(env, requestId, response.status, "container_response");',
3940
+ " return response;",
3941
+ " },",
3942
+ "};",
3943
+ ""
3944
+ ];
3668
3945
  return lines.join(`
3669
3946
  `);
3670
3947
  }
3671
- function containerClassName(manifestName) {
3672
- return "AppContainer" + manifestName.split("-").map(capitalise).join("");
3673
- }
3674
- function containerBindingName(manifestName) {
3675
- return "APP_CONTAINER_" + manifestName.split("-").map((p) => p.toUpperCase()).join("_");
3948
+ // ../launchpad-engine/dist/wrangler.js
3949
+ var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
3950
+ var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
3951
+ function generateWranglerToml(manifest) {
3952
+ const result = validateDeploymentCapability(manifest);
3953
+ if (result.kind === "unsupported") {
3954
+ return {
3955
+ kind: "unsupported",
3956
+ reason: result.reason,
3957
+ message: result.message
3958
+ };
3959
+ }
3960
+ if (result.runtime !== "container") {
3961
+ return {
3962
+ kind: "not-applicable",
3963
+ reason: "pages runtimes do not use container/wrangler.toml"
3964
+ };
3965
+ }
3966
+ return { kind: "ok", content: emit2(result.capability), warnings: [] };
3676
3967
  }
3677
- function capitalise(s) {
3678
- if (s.length === 0)
3679
- return s;
3680
- return s[0].toUpperCase() + s.slice(1);
3968
+ function emit2(capability) {
3969
+ const maxInstances = capability.container.max_instances ?? 1;
3970
+ return [
3971
+ "# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
3972
+ `# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
3973
+ `# App: ${capability.appName}`,
3974
+ "",
3975
+ `name = "${escapeTomlString(capability.appName)}"`,
3976
+ 'main = "worker/index.ts"',
3977
+ `compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`,
3978
+ `compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => `"${flag}"`).join(", ")}]`,
3979
+ "",
3980
+ "[observability]",
3981
+ "enabled = true",
3982
+ "head_sampling_rate = 1.0",
3983
+ "",
3984
+ "[vars]",
3985
+ 'CF_ACCESS_AUD = ""',
3986
+ 'CF_ACCESS_TEAM_DOMAIN = ""',
3987
+ 'LAUNCHPAD_DEPLOYMENT_SHA = ""',
3988
+ "",
3989
+ "[[containers]]",
3990
+ `class_name = "${capability.className}"`,
3991
+ `image = "${escapeTomlString(capability.dockerfilePathFromContainerDirectory)}"`,
3992
+ `max_instances = ${maxInstances}`,
3993
+ "",
3994
+ "[[durable_objects.bindings]]",
3995
+ `name = "${capability.bindingName}"`,
3996
+ `class_name = "${capability.className}"`,
3997
+ "",
3998
+ "[[migrations]]",
3999
+ 'tag = "v1"',
4000
+ `new_sqlite_classes = ["${capability.className}"]`,
4001
+ ""
4002
+ ].join(`
4003
+ `);
3681
4004
  }
3682
- function escapeTomlString(s) {
3683
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4005
+ function escapeTomlString(value) {
4006
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
3684
4007
  }
3685
4008
  // ../launchpad-engine/dist/github-workflow.js
3686
- var DEFAULT_WORKERS_SUBDOMAIN = "mkopa-launchpad.workers.dev";
3687
- var DEFAULT_BUN_VERSION = "1.3.4";
4009
+ var DEFAULT_NODE_VERSION = "22";
3688
4010
  var ACTIONS_CHECKOUT_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
3689
- var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
4011
+ var ACTIONS_SETUP_NODE_SHA = "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
3690
4012
  function generateGithubDeployWorkflow(manifest) {
3691
- if (manifest.deployment.type !== "container") {
4013
+ const result = validateDeploymentCapability(manifest);
4014
+ if (result.kind === "unsupported") {
3692
4015
  return {
3693
- kind: "not-applicable",
3694
- reason: `deployment.type '${manifest.deployment.type}' has no engine-generated deploy workflow yet`
4016
+ kind: "unsupported",
4017
+ reason: result.reason,
4018
+ message: result.message
3695
4019
  };
3696
4020
  }
3697
- const bindings = manifest.secrets?.bindings ?? [];
3698
- const warnings = [];
3699
- if ((manifest.containers ?? []).length === 0) {
3700
- warnings.push("container manifest has no containers[] — workflow will deploy an empty Worker");
4021
+ if (result.runtime !== "container") {
4022
+ return {
4023
+ kind: "not-applicable",
4024
+ reason: "pages runtimes do not use the container deploy workflow"
4025
+ };
3701
4026
  }
3702
- return {
3703
- kind: "ok",
3704
- content: emit2(manifest, bindings),
3705
- warnings
3706
- };
4027
+ return { kind: "ok", content: emit3(result.capability), warnings: [] };
3707
4028
  }
3708
- function emit2(manifest, bindings) {
3709
- const appName = manifest.metadata.name;
3710
- const lines = [];
3711
- lines.push(`# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.`);
3712
- lines.push(`# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})`);
3713
- lines.push(`# App: ${appName}`);
3714
- lines.push(`#`);
3715
- lines.push(`# Regenerate with \`launchpad generate\` (\`launchpad deploy\` also`);
3716
- lines.push(`# re-emits before applying). Manual edits are overwritten on the`);
3717
- lines.push(`# next regeneration. Manifest is the source of truth — change`);
3718
- lines.push(`# ${appName}/launchpad.yaml instead.`);
3719
- lines.push("");
3720
- lines.push(`name: deploy`);
3721
- lines.push("");
3722
- lines.push(`on:`);
3723
- lines.push(` push:`);
3724
- lines.push(` branches:`);
3725
- lines.push(` - main`);
3726
- lines.push(` workflow_dispatch:`);
3727
- lines.push("");
3728
- lines.push(`permissions:`);
3729
- lines.push(` contents: read`);
3730
- lines.push(` id-token: write`);
3731
- lines.push(` packages: read`);
3732
- lines.push("");
3733
- lines.push(`concurrency:`);
3734
- lines.push(` group: deploy-\${{ github.workflow }}-\${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3735
- lines.push(` cancel-in-progress: false`);
3736
- lines.push("");
3737
- lines.push(`defaults:`);
3738
- lines.push(` run:`);
3739
- lines.push(` # Container-shape apps keep wrangler.toml + Worker source +`);
3740
- lines.push(` # per-container Dockerfiles under container/. Engine convention.`);
3741
- lines.push(` working-directory: container`);
3742
- lines.push("");
3743
- lines.push(`jobs:`);
3744
- lines.push(` deploy:`);
3745
- lines.push(` name: deploy`);
3746
- lines.push(` runs-on: ubuntu-latest`);
3747
- lines.push(` environment: \${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3748
- lines.push(` steps:`);
3749
- lines.push(` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`);
3750
- lines.push("");
3751
- lines.push(` - uses: oven-sh/setup-bun@${SETUP_BUN_SHA} # v2`);
3752
- lines.push(` with:`);
3753
- lines.push(` bun-version: "${DEFAULT_BUN_VERSION}"`);
3754
- lines.push("");
3755
- lines.push(` - name: Install dependencies`);
3756
- lines.push(` env:`);
3757
- lines.push(` NODE_AUTH_TOKEN: \${{ secrets.NODE_AUTH_TOKEN }}`);
3758
- lines.push(` run: bun install --ignore-scripts`);
3759
- lines.push("");
3760
- lines.push(` - name: Deploy Worker + container images`);
3761
- lines.push(` # --var KEY:VALUE overrides the placeholder [vars] block in`);
3762
- lines.push(` # wrangler.toml. Missing either CF_ACCESS_* var trips the`);
3763
- lines.push(` # Worker's auth_unconfigured fail-closed branch on every`);
3764
- lines.push(` # request. Multi-container deploys build + push every`);
3765
- lines.push(` # [[containers]] block; total time scales linearly.`);
3766
- lines.push(` env:`);
3767
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3768
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3769
- lines.push(` run: |`);
3770
- lines.push(` bunx wrangler deploy \\`);
3771
- lines.push(` --var "CF_ACCESS_AUD:\${{ vars.CF_ACCESS_AUD }}" \\`);
3772
- lines.push(` --var "CF_ACCESS_TEAM_DOMAIN:\${{ vars.CF_ACCESS_TEAM_DOMAIN }}"`);
3773
- lines.push("");
3774
- for (const b of bindings) {
3775
- if (b.source !== "env-file")
3776
- continue;
3777
- emitSecretStep(lines, b);
3778
- }
3779
- const workerUrl = `https://${appName}.${DEFAULT_WORKERS_SUBDOMAIN}`;
3780
- lines.push(` - name: Smoke probe — Worker liveness (via .well-known bypass)`);
3781
- lines.push(` # In workers_dev_only mode CF Access intercepts every path`);
3782
- lines.push(` # (including /health) with HTTP 302 to the IdP. The only`);
3783
- lines.push(` # path that reaches the Worker without an Access JWT is`);
3784
- lines.push(` # /.well-known/openid-configuration, gated by a separate`);
3785
- lines.push(` # Bypass-policy Access app.`);
3786
- lines.push(` #`);
3787
- lines.push(` # Outcomes:`);
3788
- lines.push(` # 302 → bypass policy missing or misconfigured.`);
3789
- lines.push(` # 5xx → Worker is down (deploy bug).`);
3790
- lines.push(` # 401 + auth_unconfigured → CF_ACCESS_* vars didn't wire in.`);
3791
- lines.push(` # 401 + access_jwt_missing (any other 4xx) → HAPPY PATH.`);
3792
- lines.push(` run: |`);
3793
- lines.push(` set -euo pipefail`);
3794
- lines.push(` URL="${workerUrl}/.well-known/openid-configuration"`);
3795
- lines.push(` for i in 1 2 3 4 5; do`);
3796
- lines.push(` body_file="$(mktemp)"`);
3797
- lines.push(` code=$(curl -sS -o "$body_file" -w "%{http_code}" "$URL" || echo "000")`);
3798
- lines.push(` body=$(cat "$body_file")`);
3799
- lines.push(` rm -f "$body_file"`);
3800
- lines.push("");
3801
- lines.push(` if [ "$code" = "302" ]; then`);
3802
- lines.push(` echo "liveness-probe: bypass policy missing — Access redirected /.well-known/. Attempt $i."`);
3803
- lines.push(` sleep 5`);
3804
- lines.push(` continue`);
3805
- lines.push(` fi`);
3806
- lines.push(` if [ "\${code:0:1}" = "5" ] || [ "$code" = "000" ]; then`);
3807
- lines.push(` echo "liveness-probe: Worker down (got $code). Attempt $i."`);
3808
- lines.push(` sleep 5`);
3809
- lines.push(` continue`);
3810
- lines.push(` fi`);
3811
- lines.push(` if printf '%s' "$body" | grep -q "auth_unconfigured"; then`);
3812
- lines.push(` echo "liveness-probe FAIL: Worker is up but in fail-closed misconfig branch — CF_ACCESS_AUD / CF_ACCESS_TEAM_DOMAIN not wired in." >&2`);
3813
- lines.push(` echo "body: $body" >&2`);
3814
- lines.push(` exit 1`);
3815
- lines.push(` fi`);
3816
- lines.push(` echo "liveness-probe ok: Worker reachable (HTTP $code), env wired in (no auth_unconfigured)."`);
3817
- lines.push(` exit 0`);
3818
- lines.push(` done`);
3819
- lines.push(` echo "liveness-probe failed after 5 attempts" >&2`);
3820
- lines.push(` curl -sSI "$URL" >&2 || true`);
3821
- lines.push(` exit 1`);
3822
- lines.push("");
3823
- lines.push(` - name: Smoke probe — frontend route is auth-gated`);
3824
- lines.push(` # 302 or 401 prove unauthenticated traffic is blocked.`);
3825
- lines.push(` # HTTP 200 here would mean the SPA bundle is publicly`);
3826
- lines.push(` # readable — the regression we're catching.`);
3827
- lines.push(` run: |`);
3828
- lines.push(` set -euo pipefail`);
3829
- lines.push(` URL="${workerUrl}/"`);
3830
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3831
- lines.push(` case "$code" in`);
3832
- lines.push(` 302|401)`);
3833
- lines.push(` echo "frontend auth-gate ok: HTTP $code on unauthenticated request to /."`);
3834
- lines.push(` ;;`);
3835
- lines.push(` *)`);
3836
- lines.push(` echo "frontend auth-gate regression: expected 302 or 401 on /, got $code" >&2`);
3837
- lines.push(` curl -sSI "$URL" >&2 || true`);
3838
- lines.push(` exit 1`);
3839
- lines.push(` ;;`);
3840
- lines.push(` esac`);
3841
- lines.push("");
3842
- lines.push(` - name: Smoke probe — backend route is auth-gated`);
3843
- lines.push(` # Same logic as the frontend probe; confirms /api/* surface`);
3844
- lines.push(` # is gated. A 200 here would be the worse regression:`);
3845
- lines.push(` # backend data leaked unauthenticated.`);
3846
- lines.push(` run: |`);
3847
- lines.push(` set -euo pipefail`);
3848
- lines.push(` URL="${workerUrl}/api/health"`);
3849
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3850
- lines.push(` case "$code" in`);
3851
- lines.push(` 302|401)`);
3852
- lines.push(` echo "backend auth-gate ok: HTTP $code on unauthenticated request to /api/health."`);
3853
- lines.push(` ;;`);
3854
- lines.push(` *)`);
3855
- lines.push(` echo "backend auth-gate regression: expected 302 or 401 on /api/health, got $code" >&2`);
3856
- lines.push(` curl -sSI "$URL" >&2 || true`);
3857
- lines.push(` exit 1`);
3858
- lines.push(` ;;`);
3859
- lines.push(` esac`);
3860
- lines.push("");
4029
+ function emit3(capability) {
4030
+ const lines = [
4031
+ "# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
4032
+ `# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
4033
+ `# App: ${capability.appName}`,
4034
+ "",
4035
+ "name: deploy",
4036
+ "",
4037
+ "on:",
4038
+ " push:",
4039
+ " branches:",
4040
+ " - main",
4041
+ " workflow_dispatch:",
4042
+ "",
4043
+ "permissions:",
4044
+ " contents: read",
4045
+ "",
4046
+ "concurrency:",
4047
+ " group: deploy-${{ github.workflow }}-${{ github.sha }}",
4048
+ " cancel-in-progress: false",
4049
+ "",
4050
+ "jobs:",
4051
+ " authorize:",
4052
+ " name: authorize exact run attempt",
4053
+ " runs-on: ubuntu-latest",
4054
+ " steps:",
4055
+ ` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
4056
+ " with:",
4057
+ " persist-credentials: false",
4058
+ "",
4059
+ " - name: Verify protected runtime authorization",
4060
+ " env:",
4061
+ " GH_TOKEN: ${{ github.token }}",
4062
+ " run: |",
4063
+ " set -euo pipefail",
4064
+ " record=$(mktemp)",
4065
+ ` trap 'rm -f "$record"' EXIT`,
4066
+ ' api="${{ github.api_url }}/repos/${{ github.repository }}/contents/.launchpad/runtime-control/${GITHUB_SHA}.json?ref=launchpad-runtime-control"',
4067
+ " for attempt in $(seq 1 60); do",
4068
+ " if curl --fail --silent --show-error \\",
4069
+ ' -H "Authorization: Bearer ${GH_TOKEN}" \\',
4070
+ ' -H "Accept: application/vnd.github.raw+json" \\',
4071
+ ' -H "X-GitHub-Api-Version: 2022-11-28" \\',
4072
+ ' "$api" > "$record"; then',
4073
+ " break",
4074
+ " fi",
4075
+ ' if [ "$attempt" -eq 60 ]; then',
4076
+ ' echo "No exact-run authorization was published" >&2',
4077
+ " exit 1",
4078
+ " fi",
4079
+ " sleep 5",
4080
+ " done",
4081
+ " jq -e '",
4082
+ ' (.schema == "launchpad-runtime-control/v1") and',
4083
+ ' (.attempt_id | type == "string" and length > 0) and',
4084
+ ' (.lease_digest | test("^[0-9a-f]{64}$")) and',
4085
+ " (.commit_sha == env.GITHUB_SHA) and",
4086
+ " (.run_id == env.GITHUB_RUN_ID) and",
4087
+ " (.run_attempt == env.GITHUB_RUN_ATTEMPT) and",
4088
+ ' (.expires_at_epoch | type == "number" and . > now)',
4089
+ ` ' "$record" >/dev/null`,
4090
+ " wrangler_sha=$(sha256sum container/wrangler.toml | cut -d' ' -f1)",
4091
+ " worker_sha=$(sha256sum container/worker/index.ts | cut -d' ' -f1)",
4092
+ " workflow_sha=$(sha256sum .github/workflows/deploy.yml | cut -d' ' -f1)",
4093
+ " jq -e \\",
4094
+ ' --arg wrangler "$wrangler_sha" \\',
4095
+ ' --arg worker "$worker_sha" \\',
4096
+ ' --arg workflow "$workflow_sha" \\',
4097
+ ` '.artifact_digests["container/wrangler.toml"] == $wrangler and`,
4098
+ ' .artifact_digests["container/worker/index.ts"] == $worker and',
4099
+ ` .artifact_digests[".github/workflows/deploy.yml"] == $workflow' \\`,
4100
+ ' "$record" >/dev/null',
4101
+ "",
4102
+ " deploy:",
4103
+ " name: deploy admitted runtime",
4104
+ " needs: authorize",
4105
+ " runs-on: ubuntu-latest",
4106
+ " environment: ${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}",
4107
+ " steps:",
4108
+ ` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
4109
+ " with:",
4110
+ " persist-credentials: false",
4111
+ "",
4112
+ ` - uses: actions/setup-node@${ACTIONS_SETUP_NODE_SHA} # v6.4.0`,
4113
+ " with:",
4114
+ ` node-version: "${DEFAULT_NODE_VERSION}"`,
4115
+ " cache: npm",
4116
+ " cache-dependency-path: |",
4117
+ " container/package-lock.json",
4118
+ " container/backend/package-lock.json",
4119
+ "",
4120
+ " - name: Install frozen Worker dependencies",
4121
+ " working-directory: container",
4122
+ " run: npm ci --ignore-scripts",
4123
+ "",
4124
+ " - name: Install frozen workload dependencies",
4125
+ " working-directory: container/backend",
4126
+ " run: npm ci --ignore-scripts",
4127
+ "",
4128
+ " - name: Verify required deployment inputs",
4129
+ " env:",
4130
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4131
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4132
+ " CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
4133
+ " CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
4134
+ " BINDINGS_PROXY_SECRET: ${{ secrets.BINDINGS_PROXY_SECRET }}",
4135
+ " CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
4136
+ " CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
4137
+ ...capability.environmentNames.map((name) => ` ${name}: \${{ secrets.${name} }}`),
4138
+ " run: |",
4139
+ " set -euo pipefail",
4140
+ " for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID BINDINGS_PROXY_SECRET CF_ACCESS_AUD CF_ACCESS_TEAM_DOMAIN; do",
4141
+ ' if [ -z "${!name:-}" ]; then',
4142
+ ' echo "Required deployment input $name is empty" >&2',
4143
+ " exit 1",
4144
+ " fi",
4145
+ " done",
4146
+ ...capability.environmentNames.flatMap((name) => [
4147
+ ` if [ -z "\${${name}:-}" ]; then`,
4148
+ ` echo "Required container secret ${name} is empty" >&2`,
4149
+ " exit 1",
4150
+ " fi"
4151
+ ]),
4152
+ "",
4153
+ " - name: Deploy Worker and container image",
4154
+ " working-directory: container",
4155
+ " env:",
4156
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4157
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4158
+ " run: |",
4159
+ " npx --no-install wrangler deploy \\",
4160
+ ' --message "launchpad:${GITHUB_SHA}" \\',
4161
+ ' --tag "launchpad-deployment-sha:${GITHUB_SHA}" \\',
4162
+ ' --var "CF_ACCESS_AUD:${CF_ACCESS_AUD}" \\',
4163
+ ' --var "CF_ACCESS_TEAM_DOMAIN:${CF_ACCESS_TEAM_DOMAIN}" \\',
4164
+ ' --var "LAUNCHPAD_DEPLOYMENT_SHA:${GITHUB_SHA}"',
4165
+ "",
4166
+ ...secretSteps(capability)
4167
+ ];
3861
4168
  return lines.join(`
3862
4169
  `);
3863
4170
  }
3864
- function emitSecretStep(lines, binding) {
3865
- const name = binding.name;
3866
- const sourceNote = `env-file binding from launchpad.yaml — operator sets via \`launchpad secrets push\` or \`gh secret set\`.`;
3867
- lines.push(` - name: Push ${name} to the Worker`);
3868
- lines.push(` # ${sourceNote}`);
3869
- lines.push(` # Gated on the GH Actions secret existing so the first deploy`);
3870
- lines.push(` # on a fresh repo doesn't fail before secrets are provisioned.`);
3871
- lines.push(` # Skipping logs a ::warning:: so the gap shows in the deploy log.`);
3872
- lines.push(` env:`);
3873
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3874
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3875
- lines.push(` ${name}: \${{ secrets.${name} }}`);
3876
- lines.push(` run: |`);
3877
- lines.push(` if [ -z "\${${name}:-}" ]; then`);
3878
- lines.push(` echo "::warning::${name} GH Actions secret is unset — Worker will be missing this binding until the secret is provisioned + this workflow is rerun."`);
3879
- lines.push(` exit 0`);
3880
- lines.push(` fi`);
3881
- lines.push(` printf '%s' "$${name}" | bunx wrangler secret put ${name}`);
3882
- lines.push("");
4171
+ function secretSteps(capability) {
4172
+ return ["BINDINGS_PROXY_SECRET", ...capability.environmentNames].flatMap((name) => [
4173
+ ` - name: Push ${name}`,
4174
+ " working-directory: container",
4175
+ " env:",
4176
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4177
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4178
+ ` ${name}: \${{ secrets.${name} }}`,
4179
+ " run: |",
4180
+ ` printf '%s' "$${name}" | npx --no-install wrangler secret put ${name}`,
4181
+ ""
4182
+ ]);
3883
4183
  }
3884
4184
  // ../launchpad-engine/dist/per-app-workspace.js
3885
4185
  var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
@@ -4103,7 +4403,7 @@ function applyBoundaryToFiles(args) {
4103
4403
  const ignorePath = join6(cwd, ".launchpadignore");
4104
4404
  if (existsSync2(ignorePath)) {
4105
4405
  try {
4106
- launchpadIgnore = parseLaunchpadIgnore(readFileSync3(ignorePath, "utf8"));
4406
+ launchpadIgnore = parseLaunchpadIgnore(readFileSync4(ignorePath, "utf8"));
4107
4407
  } catch {}
4108
4408
  }
4109
4409
  const result = applyAppBoundary(contract, files, {
@@ -4114,7 +4414,7 @@ function applyBoundaryToFiles(args) {
4114
4414
  const stat = lstatSync2(abs);
4115
4415
  if (!stat.isFile() || stat.size > MAX_CONTENT_PROBE_BYTES)
4116
4416
  return;
4117
- return readFileSync3(abs, "utf8");
4417
+ return readFileSync4(abs, "utf8");
4118
4418
  } catch {
4119
4419
  return;
4120
4420
  }
@@ -4166,7 +4466,7 @@ async function bundleAndDeploy(args) {
4166
4466
  const manifestPath = join7(cwd, "launchpad.yaml");
4167
4467
  let manifestYaml;
4168
4468
  try {
4169
- manifestYaml = readFileSync4(manifestPath, "utf8");
4469
+ manifestYaml = readFileSync5(manifestPath, "utf8");
4170
4470
  } catch (e) {
4171
4471
  return {
4172
4472
  kind: "no-manifest",
@@ -4228,9 +4528,24 @@ async function bundleAndDeploy(args) {
4228
4528
  }
4229
4529
 
4230
4530
  // src/deploy/verify-deploy.ts
4231
- import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
4531
+ import { readFileSync as readFileSync9, existsSync as existsSync4 } from "node:fs";
4232
4532
  import { join as join9 } from "node:path";
4233
4533
 
4534
+ // src/deploy/redact-diagnostic.ts
4535
+ var MAX_DIAGNOSTIC_LEN = 600;
4536
+ var SECRET_KV = /(["']?)(\b[A-Za-z0-9_-]*(?:secrets?|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session)\b)\1(\s*[=:]\s*|\s+)(?:"[^"]*"|'[^']*'|\S+)/gi;
4537
+ var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
4538
+ function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
4539
+ if (text === undefined || text === null)
4540
+ return "";
4541
+ let t = text.replace(SECRET_KV, (_match, quote, key, separator) => `${quote}${key}${quote}${separator}***`);
4542
+ t = t.replace(LONG_TOKEN, "***");
4543
+ t = t.trim();
4544
+ if (t.length > maxLen)
4545
+ t = `${t.slice(0, maxLen - 1)}…`;
4546
+ return t;
4547
+ }
4548
+
4234
4549
  // src/deploy/deployment-status.ts
4235
4550
  async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
4236
4551
  let raw;
@@ -4259,11 +4574,39 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4259
4574
  return {
4260
4575
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4261
4576
  supported: raw.supported,
4262
- liveDeployment: raw.liveDeployment ?? null,
4263
- lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null,
4577
+ liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4578
+ lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4264
4579
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4265
4580
  };
4266
4581
  }
4582
+ function projectLastSuccessful(value) {
4583
+ if (value === null || typeof value !== "object")
4584
+ return null;
4585
+ const raw = value;
4586
+ if (typeof raw.id !== "string" || typeof raw.createdOn !== "string") {
4587
+ return null;
4588
+ }
4589
+ return {
4590
+ id: raw.id,
4591
+ createdOn: raw.createdOn,
4592
+ url: typeof raw.url === "string" ? raw.url : null
4593
+ };
4594
+ }
4595
+ function scrubLiveDeployment(deployment) {
4596
+ if (deployment === null)
4597
+ return null;
4598
+ return {
4599
+ ...deployment,
4600
+ ...deployment.commit === undefined ? {} : {
4601
+ commit: {
4602
+ ...deployment.commit,
4603
+ message: redactDiagnostic(deployment.commit.message)
4604
+ }
4605
+ },
4606
+ ...deployment.failedStage === undefined ? {} : { failedStage: redactDiagnostic(deployment.failedStage) },
4607
+ ...deployment.logExcerpt === undefined ? {} : { logExcerpt: redactDiagnostic(deployment.logExcerpt) }
4608
+ };
4609
+ }
4267
4610
 
4268
4611
  // src/deploy/commit-match-settle.ts
4269
4612
  var DEFAULT_REGISTRATION_BOUND_MS = 120000;
@@ -4429,23 +4772,8 @@ async function verifyServed(args, deps = { fetch }) {
4429
4772
  };
4430
4773
  }
4431
4774
 
4432
- // src/deploy/redact-diagnostic.ts
4433
- var MAX_DIAGNOSTIC_LEN = 600;
4434
- var SECRET_KV = /\b(secret|secrets?|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session)\b(\s*[=:]\s*|\s+)(\S+)/gi;
4435
- var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
4436
- function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
4437
- if (text === undefined || text === null)
4438
- return "";
4439
- let t = text.replace(SECRET_KV, (_m, key, sep2) => `${key}${sep2}***`);
4440
- t = t.replace(LONG_TOKEN, "***");
4441
- t = t.trim();
4442
- if (t.length > maxLen)
4443
- t = `${t.slice(0, maxLen - 1)}…`;
4444
- return t;
4445
- }
4446
-
4447
4775
  // src/commands/status.ts
4448
- import { readFileSync as readFileSync7 } from "node:fs";
4776
+ import { readFileSync as readFileSync8 } from "node:fs";
4449
4777
 
4450
4778
  // src/deploy/manifest-state.ts
4451
4779
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
@@ -4455,6 +4783,7 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
4455
4783
  slug: raw.slug,
4456
4784
  hasAppFile: raw.hasAppFile,
4457
4785
  lastAppliedManifestSha: raw.lastAppliedManifestSha,
4786
+ deployedManifestSha: raw.deployedManifestSha ?? raw.lastAppliedManifestSha,
4458
4787
  appRepoHeadSha: raw.appRepoHeadSha ?? null,
4459
4788
  openPr: raw.openPr,
4460
4789
  enforcedGroups: raw.enforcedGroups ?? null,
@@ -4464,7 +4793,7 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
4464
4793
 
4465
4794
  // src/commands/infer-slug.ts
4466
4795
  import * as path7 from "node:path";
4467
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
4796
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
4468
4797
  import { parse as parseYaml3 } from "yaml";
4469
4798
  var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4470
4799
  function inferSlugFromCwd(cwd) {
@@ -4487,7 +4816,7 @@ function inferSlugFromManifestFile(manifestPath) {
4487
4816
  if (!existsSync3(manifestPath))
4488
4817
  return null;
4489
4818
  try {
4490
- return resolveManifestSlug(parseYaml3(readFileSync5(manifestPath, "utf8")));
4819
+ return resolveManifestSlug(parseYaml3(readFileSync6(manifestPath, "utf8")));
4491
4820
  } catch {
4492
4821
  return null;
4493
4822
  }
@@ -4503,7 +4832,7 @@ function inferSlug(opts) {
4503
4832
  }
4504
4833
 
4505
4834
  // src/groups/client.ts
4506
- import { mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
4835
+ import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
4507
4836
  import { dirname as dirname5, join as join8 } from "node:path";
4508
4837
  var CACHE_TTL_MS = 60 * 60 * 1000;
4509
4838
  var CACHE_FILENAME = "groups.json";
@@ -4547,7 +4876,7 @@ async function fetchGroups(cfg, opts = {}) {
4547
4876
  function readCache(path8) {
4548
4877
  let raw;
4549
4878
  try {
4550
- raw = readFileSync6(path8, "utf8");
4879
+ raw = readFileSync7(path8, "utf8");
4551
4880
  } catch {
4552
4881
  return null;
4553
4882
  }
@@ -5183,9 +5512,10 @@ async function runWatch(slug, io, deps = {}) {
5183
5512
  const cfg = loadConfig();
5184
5513
  const now = deps.now ?? (() => Date.now());
5185
5514
  const stdout = deps.stdout ?? process.stdout;
5515
+ const readLifecycle = deps.fetchLifecycle ?? (() => fetchLifecycleView(cfg, slug));
5186
5516
  let first;
5187
5517
  try {
5188
- first = await fetchLifecycleView(cfg, slug);
5518
+ first = await readLifecycle();
5189
5519
  } catch (e) {
5190
5520
  return mapWatchError(e, slug, io);
5191
5521
  }
@@ -5193,6 +5523,9 @@ async function runWatch(slug, io, deps = {}) {
5193
5523
  io.err(`launchpad watch: no app "${slug}" to watch (unknown slug, or first deploy not started)`);
5194
5524
  return 1;
5195
5525
  }
5526
+ if (first.statusPageUrl !== undefined) {
5527
+ io.out(`Status page: ${first.statusPageUrl}`);
5528
+ }
5196
5529
  if (first.state === "destroyed" || first.state === "destroying" || first.state === "destroy_failed") {
5197
5530
  io.out(`${slug}: ${first.state} — nothing to watch.`);
5198
5531
  return 0;
@@ -5219,7 +5552,7 @@ async function runWatch(slug, io, deps = {}) {
5219
5552
  };
5220
5553
  try {
5221
5554
  const res = await runWatchLoop(slug, {
5222
- fetchLifecycle: () => fetchLifecycleView(cfg, slug),
5555
+ fetchLifecycle: readLifecycle,
5223
5556
  now,
5224
5557
  sleep: makeRealSleep(),
5225
5558
  signal: controller.signal,
@@ -5256,6 +5589,19 @@ var statusCommand = {
5256
5589
  run: runStatus
5257
5590
  };
5258
5591
  var SLUG_RE3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
5592
+ function statusTruthFields(args) {
5593
+ return {
5594
+ slug: args.slug,
5595
+ deployedSha: args.deployedSha,
5596
+ headSha: args.headSha,
5597
+ hasOpenPr: args.openPrNumber !== null,
5598
+ openPrNumber: args.openPrNumber,
5599
+ driftFields: [],
5600
+ driftDetails: [],
5601
+ ...args.deployment !== undefined ? { deployment: args.deployment } : {},
5602
+ ...args.standingExceptions !== undefined ? { standingExceptions: args.standingExceptions } : {}
5603
+ };
5604
+ }
5259
5605
  async function fetchLifecycle(cfg, slug) {
5260
5606
  try {
5261
5607
  return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
@@ -5304,12 +5650,34 @@ async function runStatus(args, io) {
5304
5650
  return mapBotError(e, parsed.slug, io);
5305
5651
  }
5306
5652
  if (lifecycle !== null && lifecycle.state !== "live") {
5307
- emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
5653
+ let output = lifecycleOutput(parsed.slug, lifecycle);
5654
+ if (lifecycle.state === "failed") {
5655
+ try {
5656
+ const deployment2 = await fetchDeploymentStatus(cfg, parsed.slug);
5657
+ output = {
5658
+ ...output,
5659
+ deployment: deployment2,
5660
+ ...deployment2 === null ? {
5661
+ deploymentUnavailableReason: "the deployment-status endpoint is unavailable or returned an unsupported response"
5662
+ } : {}
5663
+ };
5664
+ } catch (e) {
5665
+ if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5666
+ return mapBotError(e, parsed.slug, io);
5667
+ }
5668
+ output = {
5669
+ ...output,
5670
+ deployment: null,
5671
+ deploymentUnavailableReason: redactDiagnostic(describe12(e))
5672
+ };
5673
+ }
5674
+ }
5675
+ emit4(output, parsed.json, io);
5308
5676
  return 0;
5309
5677
  }
5310
5678
  let localYaml = null;
5311
5679
  try {
5312
- localYaml = readFileSync7(parsed.file, "utf8");
5680
+ localYaml = readFileSync8(parsed.file, "utf8");
5313
5681
  } catch (e) {
5314
5682
  if (!isEnoent(e)) {
5315
5683
  io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
@@ -5352,7 +5720,7 @@ async function runStatus(args, io) {
5352
5720
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5353
5721
  return mapBotError(e, parsed.slug, io);
5354
5722
  }
5355
- io.err(`launchpad status: live deployment state unavailable (${describe12(e)}) — ` + `the report below is from the platform manifest view only.`);
5723
+ io.err(`launchpad status: live deployment state unavailable (${redactDiagnostic(describe12(e))}) — ` + `the report below is from the platform manifest view only.`);
5356
5724
  }
5357
5725
  let standingExceptions = null;
5358
5726
  try {
@@ -5370,35 +5738,35 @@ async function runStatus(args, io) {
5370
5738
  const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
5371
5739
  const out = {
5372
5740
  state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
5373
- slug: parsed.slug,
5374
5741
  ...local === null ? { drift: null } : {},
5375
- deployedSha: state.lastAppliedManifestSha,
5376
- headSha: state.appRepoHeadSha,
5377
- hasOpenPr: state.openPr !== null,
5378
- openPrNumber: state.openPr?.number ?? null,
5379
- driftFields: [],
5380
- driftDetails: [],
5381
- ...deploymentKnown ? { deployment } : {},
5382
- ...standingExceptions !== null ? { standingExceptions } : {}
5742
+ ...statusTruthFields({
5743
+ slug: parsed.slug,
5744
+ deployedSha: state.deployedManifestSha,
5745
+ headSha: state.appRepoHeadSha,
5746
+ openPrNumber: state.openPr?.number ?? null,
5747
+ ...deploymentKnown ? { deployment } : {},
5748
+ ...standingExceptions !== null ? { standingExceptions } : {}
5749
+ }),
5750
+ ...lifecycle?.statusPageUrl !== undefined ? { statusPageUrl: lifecycle.statusPageUrl } : {}
5383
5751
  };
5384
- emit3(out, parsed.json, io);
5752
+ emit4(out, parsed.json, io);
5385
5753
  return 0;
5386
5754
  }
5387
5755
  if (local === null) {
5388
5756
  const out = {
5389
5757
  state: "live_drift_unknown",
5390
- slug: parsed.slug,
5391
5758
  drift: null,
5392
- deployedSha: state.lastAppliedManifestSha,
5393
- headSha: state.appRepoHeadSha,
5394
- hasOpenPr: state.openPr !== null,
5395
- openPrNumber: state.openPr?.number ?? null,
5396
- driftFields: [],
5397
- driftDetails: [],
5398
- ...deploymentKnown ? { deployment } : {},
5399
- ...standingExceptions !== null ? { standingExceptions } : {}
5759
+ ...statusTruthFields({
5760
+ slug: parsed.slug,
5761
+ deployedSha: state.deployedManifestSha,
5762
+ headSha: state.appRepoHeadSha,
5763
+ openPrNumber: state.openPr?.number ?? null,
5764
+ ...deploymentKnown ? { deployment } : {},
5765
+ ...standingExceptions !== null ? { standingExceptions } : {}
5766
+ }),
5767
+ ...lifecycle?.statusPageUrl !== undefined ? { statusPageUrl: lifecycle.statusPageUrl } : {}
5400
5768
  };
5401
- emit3(out, parsed.json, io);
5769
+ emit4(out, parsed.json, io);
5402
5770
  if (parsed.strict) {
5403
5771
  if (deployment?.liveDeployment?.buildStatus === "failure") {
5404
5772
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
@@ -5413,12 +5781,12 @@ async function runStatus(args, io) {
5413
5781
  const { parse: parseYaml4 } = await import("yaml");
5414
5782
  deployedObj = parseYaml4(state.manifestYaml);
5415
5783
  } catch (e) {
5416
- io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe12(e)}`);
5784
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
5417
5785
  return 2;
5418
5786
  }
5419
5787
  const deployedParse = parseManifest(deployedObj);
5420
5788
  if (deployedParse.kind !== "ok") {
5421
- io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} failed schema validation:`);
5789
+ io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation:`);
5422
5790
  for (const issue of deployedParse.issues) {
5423
5791
  io.err(` - ${issue.path}: ${issue.message}`);
5424
5792
  }
@@ -5438,7 +5806,7 @@ async function runStatus(args, io) {
5438
5806
  cfg,
5439
5807
  refs: allowedEntraGroups(deployed.access),
5440
5808
  source: "deployed manifest",
5441
- manifestSha: state.lastAppliedManifestSha
5809
+ manifestSha: state.deployedManifestSha
5442
5810
  });
5443
5811
  if (deployedGroups.kind === "error") {
5444
5812
  return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
@@ -5453,7 +5821,7 @@ async function runStatus(args, io) {
5453
5821
  cfg,
5454
5822
  refs: state.enforcedGroups,
5455
5823
  source: "enforced access",
5456
- manifestSha: state.lastAppliedManifestSha
5824
+ manifestSha: state.deployedManifestSha
5457
5825
  });
5458
5826
  if (enforcedGroups.kind === "error") {
5459
5827
  return renderStatusGroupError(enforcedGroups.envelope, parsed.json, io);
@@ -5462,7 +5830,7 @@ async function runStatus(args, io) {
5462
5830
  }
5463
5831
  const localAssignmentGroups = (local.auth ?? "access") === "access" ? localGroups : null;
5464
5832
  const deployedAssignmentGroups = (deployed.auth ?? "access") === "access" ? deployedGroups : null;
5465
- const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.lastAppliedManifestSha);
5833
+ const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
5466
5834
  if (assignmentCheck.kind === "error") {
5467
5835
  return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
5468
5836
  }
@@ -5470,7 +5838,7 @@ async function runStatus(args, io) {
5470
5838
  const result = {
5471
5839
  state: drift.length === 0 ? "in_sync" : "drift",
5472
5840
  slug: parsed.slug,
5473
- deployedSha: state.lastAppliedManifestSha,
5841
+ deployedSha: state.deployedManifestSha,
5474
5842
  headSha: state.appRepoHeadSha,
5475
5843
  hasOpenPr: state.openPr !== null,
5476
5844
  openPrNumber: state.openPr?.number ?? null,
@@ -5479,9 +5847,10 @@ async function runStatus(args, io) {
5479
5847
  ...deploymentKnown ? { deployment } : {},
5480
5848
  ...standingExceptions !== null ? { standingExceptions } : {},
5481
5849
  ...accessDrift !== undefined ? { accessDrift } : {},
5482
- ...groupIssues.length === 0 ? {} : { groupIssues }
5850
+ ...groupIssues.length === 0 ? {} : { groupIssues },
5851
+ ...lifecycle?.statusPageUrl !== undefined ? { statusPageUrl: lifecycle.statusPageUrl } : {}
5483
5852
  };
5484
- emit3(result, parsed.json, io);
5853
+ emit4(result, parsed.json, io);
5485
5854
  if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
5486
5855
  return 1;
5487
5856
  }
@@ -5674,14 +6043,18 @@ function lifecycleOutput(slug, lc) {
5674
6043
  hasOpenPr: false,
5675
6044
  openPrNumber: null,
5676
6045
  driftFields: [],
5677
- driftDetails: []
6046
+ driftDetails: [],
6047
+ ...lc.statusPageUrl !== undefined ? { statusPageUrl: lc.statusPageUrl } : {}
5678
6048
  };
5679
6049
  }
5680
- function emit3(out, asJson, io) {
6050
+ function emit4(out, asJson, io) {
5681
6051
  if (asJson) {
5682
6052
  io.out(JSON.stringify(out, null, 2));
5683
6053
  return;
5684
6054
  }
6055
+ if (out.statusPageUrl !== undefined) {
6056
+ io.out(`Status page: ${out.statusPageUrl}`);
6057
+ }
5685
6058
  switch (out.state) {
5686
6059
  case "provisioning":
5687
6060
  io.out(`${out.slug}: provisioning (stage: ${out.stage ?? "starting"})` + (out.submissionId ? ` — submission ${out.submissionId}` : ""));
@@ -5689,12 +6062,16 @@ function emit3(out, asJson, io) {
5689
6062
  return;
5690
6063
  case "provisioning_failed":
5691
6064
  io.out(`${out.slug}: provisioning FAILED at stage ${out.stage ?? "unknown"}` + (out.failedReason ? `: ${out.failedReason}` : ""));
5692
- if (isLikelyLiveFailureStage(out.stage)) {
6065
+ if (out.deployment?.reconciledToLive === true) {
6066
+ io.out(` that lifecycle state is stale: this read verified the live build and reconciled the record failed→live.`);
6067
+ io.out(` no action needed — re-run \`launchpad status ${out.slug}\` to see the reconciled state.`);
6068
+ } else if (isLikelyLiveFailureStage(out.stage)) {
5693
6069
  io.out(` the app may already be live — run \`launchpad recover ${out.slug}\` to reconcile the record;`);
5694
6070
  io.out(` if it is not serving, the same command starts the stage-dependent recovery.`);
5695
6071
  } else {
5696
6072
  io.out(` run \`launchpad recover ${out.slug}\` to start the stage-dependent recovery.`);
5697
6073
  }
6074
+ surfaceFailedLifecycleDeployment(out, io);
5698
6075
  return;
5699
6076
  case "destroying":
5700
6077
  io.out(`${out.slug}: destroying…`);
@@ -5791,6 +6168,20 @@ function triggerLabel(trigger) {
5791
6168
  return "launchpad deploy (bot)";
5792
6169
  return "an unknown mechanism";
5793
6170
  }
6171
+ function surfaceFailedLifecycleDeployment(out, io) {
6172
+ if (out.deploymentUnavailableReason !== undefined) {
6173
+ io.out(` latest deployment evidence unavailable: ${out.deploymentUnavailableReason}`);
6174
+ return;
6175
+ }
6176
+ if (out.deployment === undefined || out.deployment === null)
6177
+ return;
6178
+ if (!out.deployment.supported) {
6179
+ io.out(" latest deployment evidence: not applicable because this app has no Pages deployment surface.");
6180
+ return;
6181
+ }
6182
+ io.out(" latest deployment evidence (may not be the cause of this lifecycle failure):");
6183
+ surfaceDeployment(out, io);
6184
+ }
5794
6185
  function surfaceDeployment(out, io) {
5795
6186
  const dep = out.deployment;
5796
6187
  if (dep === undefined || dep === null || !dep.supported)
@@ -5807,15 +6198,15 @@ function surfaceDeployment(out, io) {
5807
6198
  const via = triggerLabel(live.trigger);
5808
6199
  switch (live.buildStatus) {
5809
6200
  case "success":
5810
- io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
6201
+ io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
5811
6202
  return;
5812
6203
  case "in_progress":
5813
- io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
6204
+ io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn}) [deployment ${live.id}]`);
5814
6205
  io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
5815
6206
  return;
5816
6207
  case "failure": {
5817
6208
  const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
5818
- io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
6209
+ io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
5819
6210
  if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
5820
6211
  io.out(" build log (excerpt):");
5821
6212
  for (const line of live.logExcerpt.split(`
@@ -5988,7 +6379,7 @@ function readLocalEntrypoint(cwd) {
5988
6379
  for (const rel of ["index.html", join9("static", "index.html")]) {
5989
6380
  const p = join9(cwd, rel);
5990
6381
  if (existsSync4(p))
5991
- return new Uint8Array(readFileSync8(p));
6382
+ return new Uint8Array(readFileSync9(p));
5992
6383
  }
5993
6384
  return null;
5994
6385
  }
@@ -6222,7 +6613,7 @@ async function verifyFirstDeployServed(args, liveUrl, io, deps) {
6222
6613
 
6223
6614
  // src/commands/deploy.ts
6224
6615
  import { parse as parseYaml6 } from "yaml";
6225
- import { readFileSync as readFileSync10 } from "node:fs";
6616
+ import { readFileSync as readFileSync11 } from "node:fs";
6226
6617
 
6227
6618
  // src/deploy/git-files.ts
6228
6619
  import { spawn as spawn3 } from "node:child_process";
@@ -6479,6 +6870,7 @@ function parseDeployFlags(args) {
6479
6870
  let rebaseOntoHead = false;
6480
6871
  let overrideStale = null;
6481
6872
  let allowUnguarded = false;
6873
+ let noOpen = false;
6482
6874
  let i = 0;
6483
6875
  while (i < args.length) {
6484
6876
  const a = args[i] ?? "";
@@ -6683,6 +7075,11 @@ function parseDeployFlags(args) {
6683
7075
  i += 1;
6684
7076
  continue;
6685
7077
  }
7078
+ if (a === "--no-open") {
7079
+ noOpen = true;
7080
+ i += 1;
7081
+ continue;
7082
+ }
6686
7083
  if (a.startsWith("--verify-timeout=")) {
6687
7084
  const v = a.slice("--verify-timeout=".length);
6688
7085
  const n = Number(v);
@@ -6703,6 +7100,9 @@ function parseDeployFlags(args) {
6703
7100
  if (rebaseOntoHead && overrideStale !== null) {
6704
7101
  return "--rebase-onto-head and --override-stale are mutually exclusive — re-stamp to head, or override in place, not both";
6705
7102
  }
7103
+ if (noOpen && mode !== "content" && mode !== "apply") {
7104
+ return "--no-open is only valid for a content deploy or --apply";
7105
+ }
6706
7106
  if (mode === "dry-run") {
6707
7107
  if (slug !== null)
6708
7108
  return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
@@ -6759,7 +7159,8 @@ function parseDeployFlags(args) {
6759
7159
  timeoutMinutes
6760
7160
  },
6761
7161
  message: null,
6762
- timeoutSeconds: null
7162
+ timeoutSeconds: null,
7163
+ ...noOpen ? { noOpen: true } : {}
6763
7164
  };
6764
7165
  }
6765
7166
  if (resumePrNumber !== null) {
@@ -6800,7 +7201,8 @@ function parseDeployFlags(args) {
6800
7201
  allowUnguarded
6801
7202
  },
6802
7203
  message,
6803
- timeoutSeconds
7204
+ timeoutSeconds,
7205
+ ...noOpen ? { noOpen: true } : {}
6804
7206
  };
6805
7207
  }
6806
7208
  if (mode === "resume" || mode === "abandon") {
@@ -6854,12 +7256,12 @@ import { existsSync as existsSync6, rmSync } from "node:fs";
6854
7256
  import { resolve as resolvePath, join as join10 } from "node:path";
6855
7257
 
6856
7258
  // src/manifest/load.ts
6857
- import { readFileSync as readFileSync9 } from "node:fs";
7259
+ import { readFileSync as readFileSync10 } from "node:fs";
6858
7260
  import { parse as parseYaml5, YAMLParseError } from "yaml";
6859
7261
  function loadManifest(path8) {
6860
7262
  let raw;
6861
7263
  try {
6862
- raw = readFileSync9(path8, "utf8");
7264
+ raw = readFileSync10(path8, "utf8");
6863
7265
  } catch (err) {
6864
7266
  const e = err;
6865
7267
  if (e.code === "ENOENT") {
@@ -6894,6 +7296,38 @@ function formatIssues(error) {
6894
7296
  }));
6895
7297
  }
6896
7298
 
7299
+ // src/status-page-output.ts
7300
+ async function surfaceStatusPageUrl(url, io, options) {
7301
+ if (typeof url !== "string" || url.length === 0)
7302
+ return;
7303
+ io.out(`Status page: ${url}`);
7304
+ const env = options.env ?? process.env;
7305
+ const isTty = options.isTty ?? process.stdout.isTTY === true;
7306
+ if (options.noOpen || !options.autoOpenEnabled || !isTty || env.CI !== undefined) {
7307
+ return;
7308
+ }
7309
+ if (!isAllowedStatusPageUrl(url, options.allowedHost)) {
7310
+ io.err("warning: refused to open an untrusted status page URL");
7311
+ return;
7312
+ }
7313
+ try {
7314
+ await (options.open ?? openBrowser)(url);
7315
+ } catch (error) {
7316
+ io.err(`warning: couldn't open the status page automatically: ${describe13(error)}`);
7317
+ }
7318
+ }
7319
+ function isAllowedStatusPageUrl(url, allowedHost) {
7320
+ try {
7321
+ const parsed = new URL(url);
7322
+ return parsed.protocol === "https:" && (allowedHost === undefined || parsed.hostname === allowedHost);
7323
+ } catch {
7324
+ return false;
7325
+ }
7326
+ }
7327
+ function describe13(error) {
7328
+ return error instanceof Error ? error.message : String(error);
7329
+ }
7330
+
6897
7331
  // src/deploy/apply.ts
6898
7332
  var DEFAULT_TIMEOUT_MINUTES = 90;
6899
7333
  var DEFAULT_POLL_INTERVAL_SECONDS = 10;
@@ -7028,6 +7462,11 @@ async function runDeployApply(opts, io, deps = {}) {
7028
7462
  io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/apply: ` + `requested ${manifestSha}, received ${String(apply.manifestSha)}`);
7029
7463
  return 2;
7030
7464
  }
7465
+ await surfaceStatusPageUrl(apply.statusPageUrl, io, {
7466
+ noOpen: opts.noOpen === true,
7467
+ autoOpenEnabled: cfg.statusPageAutoOpen,
7468
+ allowedHost: cfg.statusPageHost
7469
+ });
7031
7470
  io.out("");
7032
7471
  io.out(`PR opened: ${apply.prUrl}`);
7033
7472
  io.out(`Branch: ${apply.branch}`);
@@ -7102,7 +7541,7 @@ async function pollUntilApplied(args) {
7102
7541
  breakDots();
7103
7542
  return { kind: "failed", reason: e.message };
7104
7543
  }
7105
- args.io.err(`! apply-status fetch failed (will retry): ${describe13(e)}`);
7544
+ args.io.err(`! apply-status fetch failed (will retry): ${describe14(e)}`);
7106
7545
  await sleep(args.pollIntervalSec * 1000);
7107
7546
  continue;
7108
7547
  }
@@ -7149,7 +7588,7 @@ async function pollUntilApplied(args) {
7149
7588
  path: `/apps/${args.slug}/manifest/state`
7150
7589
  }, args.fetcher);
7151
7590
  } catch (e) {
7152
- args.io.err(`! state fetch failed (will retry): ${describe13(e)}`);
7591
+ args.io.err(`! state fetch failed (will retry): ${describe14(e)}`);
7153
7592
  await sleep(args.pollIntervalSec * 1000);
7154
7593
  continue;
7155
7594
  }
@@ -7223,7 +7662,7 @@ function deletePinIfPresent(cfg, slug, io) {
7223
7662
  rmSync(pinPath, { force: true });
7224
7663
  io.out(` (cleaned up obsolete pin file ${pinPath})`);
7225
7664
  } catch (e) {
7226
- io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe13(e)}`);
7665
+ io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe14(e)}`);
7227
7666
  }
7228
7667
  }
7229
7668
  async function loadSlugForResume(opts, io) {
@@ -7247,7 +7686,7 @@ async function defaultPrompt(question) {
7247
7686
  rl.close();
7248
7687
  }
7249
7688
  }
7250
- function describe13(e) {
7689
+ function describe14(e) {
7251
7690
  return e instanceof Error ? e.message : String(e);
7252
7691
  }
7253
7692
  function mapHttpError(e, slug, io) {
@@ -7271,7 +7710,7 @@ function mapHttpError(e, slug, io) {
7271
7710
  io.err(`launchpad deploy --apply: ${e.message}`);
7272
7711
  return 1;
7273
7712
  }
7274
- io.err(`launchpad deploy --apply failed: ${describe13(e)}`);
7713
+ io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
7275
7714
  return 2;
7276
7715
  }
7277
7716
  function renderManifestError(loaded, io) {
@@ -7314,7 +7753,7 @@ function renderAccessDriftSummary(slug, body) {
7314
7753
  return lines;
7315
7754
  }
7316
7755
  async function handleAccessDrift(params) {
7317
- const { slug, manifestPath, body, io, deps, outcome } = params;
7756
+ const { slug, manifestPath, body, io, deps, noOpen, outcome } = params;
7318
7757
  if (outcome === undefined) {
7319
7758
  for (const line of renderAccessDriftSummary(slug, body))
7320
7759
  io.err(line);
@@ -7330,6 +7769,7 @@ async function handleAccessDrift(params) {
7330
7769
  platformRepo: null,
7331
7770
  rePin: false,
7332
7771
  yes: true,
7772
+ noOpen,
7333
7773
  ...outcome === undefined ? {} : { atSha: outcome.manifest_sha },
7334
7774
  ...outcome?.pr === undefined ? {} : {
7335
7775
  preopenedPr: {
@@ -7370,7 +7810,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
7370
7810
  try {
7371
7811
  manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
7372
7812
  } catch (e) {
7373
- const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe14(e)}`;
7813
+ const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe15(e)}`;
7374
7814
  if (opts.json) {
7375
7815
  io.out(JSON.stringify({ ok: false, kind: "git-error", message: msg }));
7376
7816
  } else {
@@ -7419,7 +7859,7 @@ function defaultGitHeadSha(cwd) {
7419
7859
  });
7420
7860
  return out.trim();
7421
7861
  }
7422
- function describe14(e) {
7862
+ function describe15(e) {
7423
7863
  return e instanceof Error ? e.message : String(e);
7424
7864
  }
7425
7865
  function mapHttpError2(e, slug, json, io) {
@@ -7718,6 +8158,81 @@ function handleNetworkError(e, io, slug, verb) {
7718
8158
  return 1;
7719
8159
  }
7720
8160
 
8161
+ // src/manifest/container-filesystem.ts
8162
+ import { lstatSync as lstatSync3, realpathSync } from "node:fs";
8163
+ import { dirname as dirname6, join as join11, relative as relative3, resolve as resolve9, sep as sep2 } from "node:path";
8164
+ function validateContainerFilesystem(manifestPath, capability) {
8165
+ const root = realpathSync(dirname6(manifestPath));
8166
+ const segments = capability.container.image.slice(2).split("/");
8167
+ let current = root;
8168
+ for (const segment of segments) {
8169
+ current = join11(current, segment);
8170
+ let stat;
8171
+ try {
8172
+ stat = lstatSync3(current);
8173
+ } catch (error) {
8174
+ return failure("container_image_unreadable", relative3(root, current), `cannot inspect container image path: ${describe16(error)}`);
8175
+ }
8176
+ if (stat.isSymbolicLink()) {
8177
+ return failure("container_image_symlink", relative3(root, current), "container image path must not contain symbolic links");
8178
+ }
8179
+ }
8180
+ let finalPath;
8181
+ try {
8182
+ finalPath = realpathSync(current);
8183
+ } catch (error) {
8184
+ return failure("container_image_unreadable", relative3(root, current), `cannot resolve container image path: ${describe16(error)}`);
8185
+ }
8186
+ const rootPrefix = resolve9(root) + sep2;
8187
+ if (!resolve9(finalPath).startsWith(rootPrefix)) {
8188
+ return failure("container_image_outside_repository", capability.container.image, "container image resolves outside the app repository");
8189
+ }
8190
+ if (!lstatSync3(finalPath).isFile()) {
8191
+ return failure("container_image_not_regular_file", capability.container.image, "container image must resolve to a regular Dockerfile");
8192
+ }
8193
+ return { kind: "ok", dockerfilePath: finalPath };
8194
+ }
8195
+ function failure(reason, path8, message) {
8196
+ return { kind: "error", reason, path: path8, message };
8197
+ }
8198
+ function describe16(error) {
8199
+ return error instanceof Error ? error.message : String(error);
8200
+ }
8201
+
8202
+ // src/manifest/runtime-capability.ts
8203
+ function validateCliRuntimeCapability(manifestPath, manifest) {
8204
+ const result = validateDeploymentCapability(manifest);
8205
+ if (result.kind === "unsupported" || result.runtime === "pages") {
8206
+ return result;
8207
+ }
8208
+ const filesystem = validateContainerFilesystem(manifestPath, result.capability);
8209
+ return filesystem.kind === "error" ? filesystem : result;
8210
+ }
8211
+ function rejectUnsupportedCliRuntime(args) {
8212
+ const result = validateCliRuntimeCapability(args.manifestPath, args.manifest);
8213
+ if (result.kind !== "unsupported" && result.kind !== "error")
8214
+ return null;
8215
+ if (args.json) {
8216
+ args.io.out(JSON.stringify({
8217
+ ok: false,
8218
+ manifest_valid: true,
8219
+ runtime_supported: false,
8220
+ reason: result.reason,
8221
+ path: result.path,
8222
+ message: result.message
8223
+ }));
8224
+ return 1;
8225
+ }
8226
+ if (args.structurallyValidName !== undefined) {
8227
+ args.io.out(`✓ ${args.structurallyValidName} - manifest is structurally valid`);
8228
+ args.io.err(`✗ runtime unsupported: ${result.reason}`);
8229
+ } else {
8230
+ args.io.err(`launchpad ${args.command}: runtime unsupported (${result.reason})`);
8231
+ }
8232
+ args.io.err(` ${result.path}: ${result.message}`);
8233
+ return 1;
8234
+ }
8235
+
7721
8236
  // src/commands/deploy.ts
7722
8237
  var deployCommand = {
7723
8238
  name: "deploy",
@@ -7749,7 +8264,8 @@ async function runDeploy(args, io) {
7749
8264
  yes: flags.mode.yes,
7750
8265
  ...flags.mode.atSha !== null ? { atSha: flags.mode.atSha } : {},
7751
8266
  ...flags.mode.resumePrNumber !== null ? { resumePrNumber: flags.mode.resumePrNumber } : {},
7752
- ...flags.mode.timeoutMinutes !== null ? { timeoutMinutes: flags.mode.timeoutMinutes } : {}
8267
+ ...flags.mode.timeoutMinutes !== null ? { timeoutMinutes: flags.mode.timeoutMinutes } : {},
8268
+ noOpen: flags.noOpen === true
7753
8269
  }, io);
7754
8270
  }
7755
8271
  if (flags.mode.kind === "abandon") {
@@ -7785,7 +8301,8 @@ async function runDeploy(args, io) {
7785
8301
  allowStale: contentMode.allowStale,
7786
8302
  rebaseOntoHead: contentMode.rebaseOntoHead,
7787
8303
  overrideStale: contentMode.overrideStale,
7788
- allowUnguarded: contentMode.allowUnguarded
8304
+ allowUnguarded: contentMode.allowUnguarded,
8305
+ noOpen: flags.noOpen === true
7789
8306
  });
7790
8307
  }
7791
8308
  if (flags.mode.kind === "content" && flags.mode.allowUnguarded) {
@@ -7826,7 +8343,7 @@ async function runDeploy(args, io) {
7826
8343
  const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
7827
8344
  if (existsSync7(contentManifestPath)) {
7828
8345
  try {
7829
- contentManifestYaml = readFileSync10(contentManifestPath, "utf8");
8346
+ contentManifestYaml = readFileSync11(contentManifestPath, "utf8");
7830
8347
  } catch {
7831
8348
  contentManifestYaml = null;
7832
8349
  }
@@ -7859,6 +8376,11 @@ async function runDeploy(args, io) {
7859
8376
  headers,
7860
8377
  jsonBody: undefined
7861
8378
  }, makeBytesFetcher(packed.bytes, `/apps/${slug}/deploy-pr`));
8379
+ await surfaceStatusPageUrl(response.statusPageUrl, io, {
8380
+ noOpen: flags.noOpen === true,
8381
+ autoOpenEnabled: cfg.statusPageAutoOpen,
8382
+ allowedHost: cfg.statusPageHost
8383
+ });
7862
8384
  io.out("");
7863
8385
  io.out(`PR opened: ${response.reviewUrl}`);
7864
8386
  io.out(`Branch: ${response.branch}`);
@@ -7904,12 +8426,12 @@ async function runDeploy(args, io) {
7904
8426
  io.err(`launchpad deploy: ${e.message}`);
7905
8427
  return 1;
7906
8428
  }
7907
- io.err(`launchpad deploy failed: ${describe15(e)}`);
8429
+ io.err(`launchpad deploy failed: ${describe17(e)}`);
7908
8430
  return 1;
7909
8431
  }
7910
8432
  }
7911
8433
  function deployUsage() {
7912
- return `usage: launchpad deploy [--message <text>] [--slug <slug>] [--allow-unguarded]
8434
+ return `usage: launchpad deploy [--message <text>] [--slug <slug>] [--allow-unguarded] [--no-open]
7913
8435
  ` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
7914
8436
  ` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
7915
8437
  ` + `provisioning modes:
@@ -7947,6 +8469,10 @@ function parseArgs3(args) {
7947
8469
  i += 2;
7948
8470
  continue;
7949
8471
  }
8472
+ if (a === "--no-open") {
8473
+ i += 1;
8474
+ continue;
8475
+ }
7950
8476
  return null;
7951
8477
  }
7952
8478
  return { slug, message };
@@ -7975,7 +8501,7 @@ function formatBytes2(n) {
7975
8501
  return `${(n / 1024).toFixed(1)}KB`;
7976
8502
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
7977
8503
  }
7978
- function describe15(e) {
8504
+ function describe17(e) {
7979
8505
  return e instanceof Error ? e.message : String(e);
7980
8506
  }
7981
8507
  function surfaceDeployExtras(body, io, slug, allowStale = false) {
@@ -8047,11 +8573,11 @@ function surfaceStaleBlock(body, io) {
8047
8573
  }
8048
8574
  var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
8049
8575
  function withTimeout(p, ms) {
8050
- return new Promise((resolve9, reject) => {
8576
+ return new Promise((resolve10, reject) => {
8051
8577
  const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
8052
8578
  p.then((v) => {
8053
8579
  clearTimeout(timer);
8054
- resolve9(v);
8580
+ resolve10(v);
8055
8581
  }, (e) => {
8056
8582
  clearTimeout(timer);
8057
8583
  reject(e instanceof Error ? e : new Error(String(e)));
@@ -8061,7 +8587,7 @@ function withTimeout(p, ms) {
8061
8587
  async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
8062
8588
  let localYaml;
8063
8589
  try {
8064
- localYaml = readFileSync10(manifestPath, "utf8");
8590
+ localYaml = readFileSync11(manifestPath, "utf8");
8065
8591
  } catch {
8066
8592
  return;
8067
8593
  }
@@ -8098,7 +8624,7 @@ async function advanceBaseShaMarker(cwd, sha, io) {
8098
8624
  return;
8099
8625
  await writeBaseShaMarker(cwd, sha);
8100
8626
  } catch (e) {
8101
- io.err(`warning: couldn't update the local base-sha marker (${describe15(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
8627
+ io.err(`warning: couldn't update the local base-sha marker (${describe17(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
8102
8628
  }
8103
8629
  }
8104
8630
  async function runModelADeploy(args) {
@@ -8106,19 +8632,37 @@ async function runModelADeploy(args) {
8106
8632
  let slug;
8107
8633
  let appType = "static";
8108
8634
  try {
8109
- const manifestObj = parseYaml6(readFileSync10(manifestPath, "utf8"));
8635
+ const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
8110
8636
  const metaSlug = resolveManifestSlug(manifestObj);
8111
8637
  if (metaSlug === null) {
8112
8638
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
8113
8639
  return 64;
8114
8640
  }
8115
8641
  slug = metaSlug;
8116
- const dtype = manifestObj?.deployment?.type;
8117
- if (dtype === "static" || dtype === "react" || dtype === "react+api" || dtype === "container") {
8118
- appType = dtype;
8642
+ const raw = manifestObj;
8643
+ const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
8644
+ const deploymentType = isV2 ? raw?.spec?.appType : raw?.deployment?.type;
8645
+ if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
8646
+ appType = deploymentType;
8647
+ }
8648
+ if (deploymentType === "container") {
8649
+ const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
8650
+ if (parsed.kind !== "ok") {
8651
+ io.err("launchpad deploy: manifest is structurally invalid:");
8652
+ for (const issue of parsed.issues) {
8653
+ io.err(` ${issue.path}: ${issue.message}`);
8654
+ }
8655
+ return 1;
8656
+ }
8657
+ const capability = validateCliRuntimeCapability(manifestPath, parsed.manifest);
8658
+ if (capability.kind === "unsupported" || capability.kind === "error") {
8659
+ io.err(`launchpad deploy: runtime unsupported (${capability.reason})`);
8660
+ io.err(` ${capability.path}: ${capability.message}`);
8661
+ return 1;
8662
+ }
8119
8663
  }
8120
8664
  } catch (e) {
8121
- io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe15(e)}`);
8665
+ io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe17(e)}`);
8122
8666
  return 1;
8123
8667
  }
8124
8668
  const noVerify = args.argv.includes("--no-verify");
@@ -8148,7 +8692,7 @@ async function runModelADeploy(args) {
8148
8692
  try {
8149
8693
  cfg = loadConfig();
8150
8694
  } catch (e) {
8151
- io.err(`launchpad deploy: ${describe15(e)}`);
8695
+ io.err(`launchpad deploy: ${describe17(e)}`);
8152
8696
  return 1;
8153
8697
  }
8154
8698
  try {
@@ -8198,7 +8742,7 @@ async function runModelADeploy(args) {
8198
8742
  io.err(" run `launchpad login` to refresh your session.");
8199
8743
  return 1;
8200
8744
  }
8201
- io.err(`launchpad deploy: unexpected error: ${describe15(e)}`);
8745
+ io.err(`launchpad deploy: unexpected error: ${describe17(e)}`);
8202
8746
  return 1;
8203
8747
  }
8204
8748
  switch (result.kind) {
@@ -8219,6 +8763,11 @@ async function runModelADeploy(args) {
8219
8763
  return 1;
8220
8764
  case "upload-error": {
8221
8765
  const body = result.body;
8766
+ await surfaceStatusPageUrl(body.statusPageUrl, io, {
8767
+ noOpen: args.noOpen,
8768
+ autoOpenEnabled: cfg.statusPageAutoOpen,
8769
+ allowedHost: cfg.statusPageHost
8770
+ });
8222
8771
  const errorCode = body.error;
8223
8772
  if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
8224
8773
  io.err(upgradeRequiredMessage(body.minimum, body.yours));
@@ -8255,6 +8804,7 @@ async function runModelADeploy(args) {
8255
8804
  manifestPath,
8256
8805
  body,
8257
8806
  io,
8807
+ noOpen: args.noOpen,
8258
8808
  deps: { runApply: runDeployApply }
8259
8809
  });
8260
8810
  }
@@ -8272,6 +8822,7 @@ async function runModelADeploy(args) {
8272
8822
  body,
8273
8823
  outcome,
8274
8824
  io,
8825
+ noOpen: args.noOpen,
8275
8826
  deps: { runApply: runDeployApply }
8276
8827
  });
8277
8828
  if (accessExit !== 0)
@@ -8315,6 +8866,11 @@ async function runModelADeploy(args) {
8315
8866
  io.err(`warning: ${w}`);
8316
8867
  }
8317
8868
  const success = result.result;
8869
+ await surfaceStatusPageUrl(success.statusPageUrl, io, {
8870
+ noOpen: args.noOpen,
8871
+ autoOpenEnabled: cfg.statusPageAutoOpen,
8872
+ allowedHost: cfg.statusPageHost
8873
+ });
8318
8874
  if ("outcome" in success) {
8319
8875
  io.out("Nothing to deploy — your app is already live at this content.");
8320
8876
  if (typeof success.head_sha === "string") {
@@ -8359,6 +8915,7 @@ async function runModelADeploy(args) {
8359
8915
  body: {},
8360
8916
  outcome: success.reconcile,
8361
8917
  io,
8918
+ noOpen: args.noOpen,
8362
8919
  deps: { runApply: runDeployApply }
8363
8920
  });
8364
8921
  if (accessExit !== 0)
@@ -8410,7 +8967,7 @@ function pendingReconcileOutcome(body) {
8410
8967
  }
8411
8968
 
8412
8969
  // src/commands/redeploy.ts
8413
- import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
8970
+ import { existsSync as existsSync8, readFileSync as readFileSync12 } from "node:fs";
8414
8971
  import { resolve as resolvePath2 } from "node:path";
8415
8972
  import { parse as parseYaml7 } from "yaml";
8416
8973
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
@@ -8491,7 +9048,7 @@ function readLocalAppType(cwd, file) {
8491
9048
  if (!existsSync8(manifestPath))
8492
9049
  return null;
8493
9050
  try {
8494
- const obj = parseYaml7(readFileSync11(manifestPath, "utf8"));
9051
+ const obj = parseYaml7(readFileSync12(manifestPath, "utf8"));
8495
9052
  return coerceAppType(obj?.deployment?.type);
8496
9053
  } catch {
8497
9054
  return null;
@@ -8673,7 +9230,7 @@ async function runEnvvars(args, io) {
8673
9230
  io.err(`launchpad envvars: ${e.message}`);
8674
9231
  return 1;
8675
9232
  }
8676
- io.err(`launchpad envvars failed: ${describe16(e)}`);
9233
+ io.err(`launchpad envvars failed: ${describe18(e)}`);
8677
9234
  return 1;
8678
9235
  }
8679
9236
  }
@@ -8770,16 +9327,16 @@ function renderList(envVars, io) {
8770
9327
  io.out(fmt(row));
8771
9328
  }
8772
9329
  }
8773
- function describe16(e) {
9330
+ function describe18(e) {
8774
9331
  return e instanceof Error ? e.message : String(e);
8775
9332
  }
8776
9333
 
8777
9334
  // src/commands/generate.ts
8778
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
8779
- import { dirname as dirname6, resolve as resolve9, relative as relative3 } from "node:path";
9335
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
9336
+ import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
8780
9337
  var generateCommand = {
8781
9338
  name: "generate",
8782
- summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
9339
+ summary: "emit derived runtime artefacts from launchpad.yaml",
8783
9340
  run: runGenerate
8784
9341
  };
8785
9342
  async function runGenerate(args, io) {
@@ -8789,33 +9346,50 @@ async function runGenerate(args, io) {
8789
9346
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
8790
9347
  return 64;
8791
9348
  }
8792
- const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
9349
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
8793
9350
  const result = loadManifest(manifestPath);
8794
9351
  if (result.kind !== "ok") {
8795
9352
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
8796
9353
  }
8797
- const appRoot = dirname6(manifestPath);
8798
- const wranglerPath = resolve9(appRoot, "container", "wrangler.toml");
8799
- const workflowPath = resolve9(appRoot, ".github", "workflows", "deploy.yml");
9354
+ const unsupported2 = rejectUnsupportedCliRuntime({
9355
+ command: "generate",
9356
+ manifestPath,
9357
+ manifest: result.manifest,
9358
+ io,
9359
+ json: flags.json
9360
+ });
9361
+ if (unsupported2 !== null)
9362
+ return unsupported2;
9363
+ const appRoot = dirname7(manifestPath);
9364
+ const wranglerPath = resolve10(appRoot, "container", "wrangler.toml");
9365
+ const workerPath = resolve10(appRoot, "container", "worker", "index.ts");
9366
+ const workflowPath = resolve10(appRoot, ".github", "workflows", "deploy.yml");
8800
9367
  const wranglerOut = generateWranglerToml(result.manifest);
9368
+ const workerRaw = generateContainerWorker(result.manifest);
9369
+ const workerOut = workerRaw.kind === "ok" ? { kind: "ok", content: workerRaw.content, warnings: [] } : workerRaw;
8801
9370
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
8802
9371
  if (flags.dryRun) {
8803
9372
  return renderDryRun([
8804
9373
  { artefact: "wrangler.toml", path: wranglerPath, result: wranglerOut },
9374
+ { artefact: "worker/index.ts", path: workerPath, result: workerOut },
8805
9375
  { artefact: "deploy.yml", path: workflowPath, result: workflowOut }
8806
9376
  ], appRoot, flags.json, io);
8807
9377
  }
8808
9378
  const reports = [
8809
9379
  applyOne("wrangler.toml", wranglerPath, wranglerOut, flags.force),
9380
+ applyOne("worker/index.ts", workerPath, workerOut, flags.force),
8810
9381
  applyOne("deploy.yml", workflowPath, workflowOut, flags.force)
8811
9382
  ];
8812
9383
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
8813
9384
  }
8814
9385
  function applyOne(artefact, path10, out, force) {
8815
- if (out.kind === "not-applicable") {
9386
+ if (out.kind === "not-applicable" || out.kind === "unsupported") {
8816
9387
  return {
8817
9388
  artefact,
8818
- action: { kind: "not-applicable", reason: out.reason },
9389
+ action: {
9390
+ kind: "not-applicable",
9391
+ reason: out.kind === "unsupported" ? `${out.reason}: ${out.message}` : out.reason
9392
+ },
8819
9393
  warnings: []
8820
9394
  };
8821
9395
  }
@@ -8838,7 +9412,7 @@ function applyOne(artefact, path10, out, force) {
8838
9412
  };
8839
9413
  }
8840
9414
  try {
8841
- mkdirSync2(dirname6(path10), { recursive: true });
9415
+ mkdirSync2(dirname7(path10), { recursive: true });
8842
9416
  writeFileSync2(path10, out.content, "utf8");
8843
9417
  return {
8844
9418
  artefact,
@@ -8863,7 +9437,7 @@ function applyOne(artefact, path10, out, force) {
8863
9437
  }
8864
9438
  function readIfExists(path10) {
8865
9439
  try {
8866
- return { kind: "ok", content: readFileSync12(path10, "utf8") };
9440
+ return { kind: "ok", content: readFileSync13(path10, "utf8") };
8867
9441
  } catch (err) {
8868
9442
  const e = err;
8869
9443
  if (e.code === "ENOENT")
@@ -8874,12 +9448,12 @@ function readIfExists(path10) {
8874
9448
  function renderDryRun(inputs, appRoot, json, io) {
8875
9449
  if (json) {
8876
9450
  const files = inputs.map((i) => {
8877
- const previewPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
8878
- if (i.result.kind === "not-applicable") {
9451
+ const previewPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
9452
+ if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
8879
9453
  return {
8880
9454
  artefact: i.artefact,
8881
9455
  kind: "not-applicable",
8882
- reason: i.result.reason,
9456
+ reason: i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason,
8883
9457
  ...i.preview ? { preview: true, destination: i.preview.destination } : {}
8884
9458
  };
8885
9459
  }
@@ -8896,12 +9470,13 @@ function renderDryRun(inputs, appRoot, json, io) {
8896
9470
  return 0;
8897
9471
  }
8898
9472
  for (const i of inputs) {
8899
- if (i.result.kind === "not-applicable") {
8900
- io.out(`# ${i.artefact}: skipped — ${i.result.reason}`);
9473
+ if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
9474
+ const reason = i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason;
9475
+ io.out(`# ${i.artefact}: skipped - ${reason}`);
8901
9476
  io.out("");
8902
9477
  continue;
8903
9478
  }
8904
- const headerPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
9479
+ const headerPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
8905
9480
  const header = i.preview === undefined ? `--- ${headerPath} ---` : `--- [preview, destination = ${i.preview.destination}] ${headerPath} ---`;
8906
9481
  io.out(header);
8907
9482
  io.out(i.result.content);
@@ -8920,20 +9495,20 @@ function renderApplyHuman(reports, appRoot, io) {
8920
9495
  const a = r.action;
8921
9496
  switch (a.kind) {
8922
9497
  case "written":
8923
- io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative3(appRoot, a.path)}`);
9498
+ io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative4(appRoot, a.path)}`);
8924
9499
  break;
8925
9500
  case "unchanged":
8926
- io.out(`= ${r.artefact} — unchanged (${relative3(appRoot, a.path)})`);
9501
+ io.out(`= ${r.artefact} — unchanged (${relative4(appRoot, a.path)})`);
8927
9502
  break;
8928
9503
  case "would-overwrite":
8929
- io.err(`! ${r.artefact} — exists with different content (${relative3(appRoot, a.path)}); re-run with --force to overwrite`);
9504
+ io.err(`! ${r.artefact} — exists with different content (${relative4(appRoot, a.path)}); re-run with --force to overwrite`);
8930
9505
  wouldOverwrite++;
8931
9506
  break;
8932
9507
  case "not-applicable":
8933
9508
  io.out(`- ${r.artefact} — skipped (${a.reason})`);
8934
9509
  break;
8935
9510
  case "write-error":
8936
- io.err(`✗ ${r.artefact} — ${a.message} (${relative3(appRoot, a.path)})`);
9511
+ io.err(`✗ ${r.artefact} — ${a.message} (${relative4(appRoot, a.path)})`);
8937
9512
  writeError++;
8938
9513
  break;
8939
9514
  }
@@ -8952,17 +9527,17 @@ function renderApplyJson(reports, appRoot, io) {
8952
9527
  const base = { artefact: r.artefact, warnings: r.warnings };
8953
9528
  switch (a.kind) {
8954
9529
  case "written":
8955
- return { ...base, kind: a.kind, path: relative3(appRoot, a.path), bytes: a.bytes };
9530
+ return { ...base, kind: a.kind, path: relative4(appRoot, a.path), bytes: a.bytes };
8956
9531
  case "unchanged":
8957
9532
  case "would-overwrite":
8958
- return { ...base, kind: a.kind, path: relative3(appRoot, a.path) };
9533
+ return { ...base, kind: a.kind, path: relative4(appRoot, a.path) };
8959
9534
  case "not-applicable":
8960
9535
  return { ...base, kind: a.kind, reason: a.reason };
8961
9536
  case "write-error":
8962
9537
  return {
8963
9538
  ...base,
8964
9539
  kind: a.kind,
8965
- path: relative3(appRoot, a.path),
9540
+ path: relative4(appRoot, a.path),
8966
9541
  message: a.message
8967
9542
  };
8968
9543
  }
@@ -9086,13 +9661,13 @@ function decodeJwtPayload(token) {
9086
9661
  try {
9087
9662
  json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
9088
9663
  } catch (e) {
9089
- throw new JwtParseError(`could not base64-decode JWT payload: ${describe17(e)}`);
9664
+ throw new JwtParseError(`could not base64-decode JWT payload: ${describe19(e)}`);
9090
9665
  }
9091
9666
  let parsed;
9092
9667
  try {
9093
9668
  parsed = JSON.parse(json);
9094
9669
  } catch (e) {
9095
- throw new JwtParseError(`JWT payload is not JSON: ${describe17(e)}`);
9670
+ throw new JwtParseError(`JWT payload is not JSON: ${describe19(e)}`);
9096
9671
  }
9097
9672
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9098
9673
  throw new JwtParseError(`JWT payload is not an object`);
@@ -9103,7 +9678,7 @@ function b64UrlToB64(s) {
9103
9678
  const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
9104
9679
  return padded.replace(/-/g, "+").replace(/_/g, "/");
9105
9680
  }
9106
- function describe17(e) {
9681
+ function describe19(e) {
9107
9682
  return e instanceof Error ? e.message : String(e);
9108
9683
  }
9109
9684
 
@@ -9130,7 +9705,7 @@ async function runGroupsWhoami(args, io) {
9130
9705
  try {
9131
9706
  payload = decodeJwtPayload(session.accessToken);
9132
9707
  } catch (e) {
9133
- const message = e instanceof JwtParseError ? e.message : describe18(e);
9708
+ const message = e instanceof JwtParseError ? e.message : describe20(e);
9134
9709
  if (json) {
9135
9710
  io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
9136
9711
  } else {
@@ -9211,7 +9786,7 @@ function parseFlags2(args) {
9211
9786
  }
9212
9787
  return { kind: "ok", json };
9213
9788
  }
9214
- function describe18(e) {
9789
+ function describe20(e) {
9215
9790
  return e instanceof Error ? e.message : String(e);
9216
9791
  }
9217
9792
 
@@ -9314,7 +9889,7 @@ async function loadGroups(io, refresh, json) {
9314
9889
  }
9315
9890
  return { kind: "error", code: 3 };
9316
9891
  }
9317
- renderFetchError(io, describe19(e), json);
9892
+ renderFetchError(io, describe21(e), json);
9318
9893
  return { kind: "error", code: 2 };
9319
9894
  }
9320
9895
  }
@@ -9351,7 +9926,7 @@ async function lookupTenantWide(key) {
9351
9926
  return { kind: "unauthenticated", message: e.message };
9352
9927
  if (e instanceof ForbiddenError)
9353
9928
  return { kind: "forbidden", message: e.message };
9354
- return { kind: "error", message: describe19(e) };
9929
+ return { kind: "error", message: describe21(e) };
9355
9930
  }
9356
9931
  }
9357
9932
  function renderLookupFailure(io, json, hit) {
@@ -9497,9 +10072,9 @@ async function runShow(args, io) {
9497
10072
  }
9498
10073
  const key = parsed.flags.positional[0];
9499
10074
  const hit = await lookupTenantWide(key);
9500
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9501
- if (failure !== null)
9502
- return failure;
10075
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
10076
+ if (failure2 !== null)
10077
+ return failure2;
9503
10078
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9504
10079
  return 2;
9505
10080
  }
@@ -9546,9 +10121,9 @@ async function runResolve(args, io) {
9546
10121
  }
9547
10122
  const key = parsed.flags.positional[0];
9548
10123
  const hit = await lookupTenantWide(key);
9549
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9550
- if (failure !== null)
9551
- return failure;
10124
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
10125
+ if (failure2 !== null)
10126
+ return failure2;
9552
10127
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9553
10128
  return 2;
9554
10129
  }
@@ -9595,23 +10170,23 @@ function renderTable2(io, groups) {
9595
10170
  for (const r of rows)
9596
10171
  io.out(fmt(r));
9597
10172
  }
9598
- function describe19(e) {
10173
+ function describe21(e) {
9599
10174
  return e instanceof Error ? e.message : String(e);
9600
10175
  }
9601
10176
 
9602
10177
  // src/commands/init.ts
9603
10178
  import { createInterface } from "node:readline/promises";
9604
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9605
- import { dirname as dirname7, resolve as resolve10 } from "node:path";
10179
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync3 } from "node:fs";
10180
+ import { dirname as dirname8, resolve as resolve11 } from "node:path";
9606
10181
  import { stringify as yamlStringify } from "yaml";
9607
10182
 
9608
10183
  // src/detect/index.ts
9609
- import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
9610
- import { join as join12 } from "node:path";
10184
+ import { existsSync as existsSync9, readFileSync as readFileSync14, statSync } from "node:fs";
10185
+ import { join as join13 } from "node:path";
9611
10186
  function detectAppShape(cwd) {
9612
- const hasPackageJson = existsSync9(join12(cwd, "package.json"));
9613
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join12(cwd, file)));
9614
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join12(cwd, n)));
10187
+ const hasPackageJson = existsSync9(join13(cwd, "package.json"));
10188
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join13(cwd, file)));
10189
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join13(cwd, n)));
9615
10190
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
9616
10191
  return {
9617
10192
  kind: "not-applicable",
@@ -9657,7 +10232,7 @@ var LOCKFILES = [
9657
10232
  { file: "yarn.lock", pm: "yarn" }
9658
10233
  ];
9659
10234
  function detectPackageManager(cwd) {
9660
- const present = LOCKFILES.filter(({ file }) => existsSync9(join12(cwd, file)));
10235
+ const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
9661
10236
  if (present.length === 0) {
9662
10237
  return {
9663
10238
  kind: "ambiguous",
@@ -9679,7 +10254,7 @@ function detectPackageManager(cwd) {
9679
10254
  }
9680
10255
  function detectVitePresence(cwd) {
9681
10256
  for (const name of VITE_CONFIG_NAMES) {
9682
- const p = join12(cwd, name);
10257
+ const p = join13(cwd, name);
9683
10258
  if (existsSync9(p)) {
9684
10259
  return { kind: "ok", value: { path: p } };
9685
10260
  }
@@ -9690,7 +10265,7 @@ function detectVitePresence(cwd) {
9690
10265
  };
9691
10266
  }
9692
10267
  function detectAppType(cwd) {
9693
- const fnDir = join12(cwd, "functions");
10268
+ const fnDir = join13(cwd, "functions");
9694
10269
  let hasFunctionsDir = false;
9695
10270
  if (existsSync9(fnDir)) {
9696
10271
  try {
@@ -9702,7 +10277,7 @@ function detectAppType(cwd) {
9702
10277
  return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
9703
10278
  }
9704
10279
  function detectBuildCommand(cwd, pm) {
9705
- const pkgJsonPath = join12(cwd, "package.json");
10280
+ const pkgJsonPath = join13(cwd, "package.json");
9706
10281
  if (!existsSync9(pkgJsonPath)) {
9707
10282
  return {
9708
10283
  kind: "ambiguous",
@@ -9711,7 +10286,7 @@ function detectBuildCommand(cwd, pm) {
9711
10286
  }
9712
10287
  let pkgJson;
9713
10288
  try {
9714
- pkgJson = JSON.parse(readFileSync13(pkgJsonPath, "utf8"));
10289
+ pkgJson = JSON.parse(readFileSync14(pkgJsonPath, "utf8"));
9715
10290
  } catch (e) {
9716
10291
  return {
9717
10292
  kind: "ambiguous",
@@ -9746,7 +10321,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
9746
10321
  function detectDestinationDir(cwd, vite) {
9747
10322
  let text;
9748
10323
  try {
9749
- text = readFileSync13(vite.path, "utf8");
10324
+ text = readFileSync14(vite.path, "utf8");
9750
10325
  } catch (e) {
9751
10326
  return {
9752
10327
  kind: "ambiguous",
@@ -9775,7 +10350,7 @@ async function runInit(args, io, prompt) {
9775
10350
  return 64;
9776
10351
  }
9777
10352
  const { inputs, options } = parsed;
9778
- const outPath = resolve10(process.cwd(), options.out);
10353
+ const outPath = resolve11(process.cwd(), options.out);
9779
10354
  if (existsSync10(outPath) && !options.force) {
9780
10355
  io.err(`launchpad init: ${outPath} already exists`);
9781
10356
  io.err("Pass --force to overwrite.");
@@ -9823,7 +10398,7 @@ async function runInit(args, io, prompt) {
9823
10398
  }
9824
10399
  io.out(`✓ wrote ${outPath}`);
9825
10400
  if (options.gitignore) {
9826
- const gitignorePath = resolve10(process.cwd(), ".gitignore");
10401
+ const gitignorePath = resolve11(process.cwd(), ".gitignore");
9827
10402
  try {
9828
10403
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
9829
10404
  if (changed.length > 0) {
@@ -9838,8 +10413,8 @@ async function runInit(args, io, prompt) {
9838
10413
  }
9839
10414
  }
9840
10415
  if (fillResult.inputs.type === "static") {
9841
- const serveDir = resolve10(dirname7(outPath), STATIC_SERVE_DIR);
9842
- const indexPath = resolve10(serveDir, "index.html");
10416
+ const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
10417
+ const indexPath = resolve11(serveDir, "index.html");
9843
10418
  if (!existsSync10(indexPath)) {
9844
10419
  try {
9845
10420
  mkdirSync3(serveDir, { recursive: true });
@@ -10233,7 +10808,7 @@ function renderYaml(manifest) {
10233
10808
  function ensureGitignoreEntries(path10, entries) {
10234
10809
  let current = "";
10235
10810
  if (existsSync10(path10)) {
10236
- current = readFileSync14(path10, "utf8");
10811
+ current = readFileSync15(path10, "utf8");
10237
10812
  }
10238
10813
  const lines = current.split(/\r?\n/);
10239
10814
  const present = new Set(lines.map((l) => l.trim()));
@@ -10306,13 +10881,13 @@ async function runLogin(args, io, deps) {
10306
10881
  io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
10307
10882
  return 0;
10308
10883
  } catch (e) {
10309
- io.err(`Gateway login failed: ${describe20(e)}`);
10884
+ io.err(`Gateway login failed: ${describe22(e)}`);
10310
10885
  io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
10311
10886
  io.err("");
10312
10887
  return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
10313
10888
  }
10314
10889
  } catch (e) {
10315
- io.err(`launchpad login failed: ${describe20(e)}`);
10890
+ io.err(`launchpad login failed: ${describe22(e)}`);
10316
10891
  return 1;
10317
10892
  }
10318
10893
  }
@@ -10354,7 +10929,7 @@ function printLoginHelp(io) {
10354
10929
  io.out(" 0 signed in / session stored");
10355
10930
  io.out(" 1 login failed (network, browser, or gateway error)");
10356
10931
  }
10357
- function describe20(e) {
10932
+ function describe22(e) {
10358
10933
  return e instanceof Error ? e.message : String(e);
10359
10934
  }
10360
10935
 
@@ -10391,14 +10966,14 @@ async function runLogout(args, io, deps) {
10391
10966
  });
10392
10967
  io.out("Server-side session revoked.");
10393
10968
  } catch (e) {
10394
- io.err(`warning: could not revoke the session server-side (${describe21(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
10969
+ io.err(`warning: could not revoke the session server-side (${describe23(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
10395
10970
  }
10396
10971
  }
10397
10972
  const had = await clearSession(cfg.sessionPath);
10398
10973
  io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
10399
10974
  return 0;
10400
10975
  } catch (e) {
10401
- io.err(`launchpad logout failed: ${describe21(e)}`);
10976
+ io.err(`launchpad logout failed: ${describe23(e)}`);
10402
10977
  return 1;
10403
10978
  }
10404
10979
  }
@@ -10418,7 +10993,7 @@ function printLogoutHelp(io) {
10418
10993
  io.out(" 0 logged out (or already logged out)");
10419
10994
  io.out(" 1 could not clear the local session (file IO / config error)");
10420
10995
  }
10421
- function describe21(e) {
10996
+ function describe23(e) {
10422
10997
  return e instanceof Error ? e.message : String(e);
10423
10998
  }
10424
10999
 
@@ -10488,7 +11063,7 @@ async function runLogs(args, io) {
10488
11063
  io.err(`launchpad logs: ${e.message}`);
10489
11064
  return 1;
10490
11065
  }
10491
- io.err(`launchpad logs failed: ${describe22(e)}`);
11066
+ io.err(`launchpad logs failed: ${describe24(e)}`);
10492
11067
  return 1;
10493
11068
  }
10494
11069
  }
@@ -10568,7 +11143,7 @@ function formatRelative2(iso) {
10568
11143
  return `${hr}h ago`;
10569
11144
  return `${Math.floor(hr / 24)}d ago`;
10570
11145
  }
10571
- function describe22(e) {
11146
+ function describe24(e) {
10572
11147
  return e instanceof Error ? e.message : String(e);
10573
11148
  }
10574
11149
 
@@ -10617,7 +11192,7 @@ async function runMerge(args, io) {
10617
11192
  try {
10618
11193
  body = await res.json();
10619
11194
  } catch (e) {
10620
- io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe23(e)}`);
11195
+ io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe25(e)}`);
10621
11196
  return 1;
10622
11197
  }
10623
11198
  io.out("");
@@ -10657,7 +11232,7 @@ async function runMerge(args, io) {
10657
11232
  io.err(`launchpad merge: ${e.message}`);
10658
11233
  return 1;
10659
11234
  }
10660
- io.err(`launchpad merge failed: ${describe23(e)}`);
11235
+ io.err(`launchpad merge failed: ${describe25(e)}`);
10661
11236
  return 1;
10662
11237
  }
10663
11238
  }
@@ -10756,12 +11331,12 @@ function renderBotError(status, env, io, prNumber = null) {
10756
11331
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
10757
11332
  }
10758
11333
  }
10759
- function describe23(e) {
11334
+ function describe25(e) {
10760
11335
  return e instanceof Error ? e.message : String(e);
10761
11336
  }
10762
11337
 
10763
11338
  // src/commands/plan.ts
10764
- import { resolve as resolve11 } from "node:path";
11339
+ import { resolve as resolve12 } from "node:path";
10765
11340
  var planCommand = {
10766
11341
  name: "plan",
10767
11342
  summary: "summarise what the manifest would deploy (offline)",
@@ -10774,8 +11349,19 @@ async function runPlan(args, io) {
10774
11349
  io.err("Usage: launchpad plan [--file <path>] [--json]");
10775
11350
  return 64;
10776
11351
  }
10777
- const manifestPath = resolve11(process.cwd(), flags.file ?? "launchpad.yaml");
11352
+ const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
10778
11353
  const result = loadManifest(manifestPath);
11354
+ if (result.kind === "ok") {
11355
+ const unsupported2 = rejectUnsupportedCliRuntime({
11356
+ command: "plan",
11357
+ manifestPath,
11358
+ manifest: result.manifest,
11359
+ io,
11360
+ json: flags.json
11361
+ });
11362
+ if (unsupported2 !== null)
11363
+ return unsupported2;
11364
+ }
10779
11365
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
10780
11366
  }
10781
11367
  function renderJson(result, io) {
@@ -11008,7 +11594,7 @@ async function runDestroy(args, io, prompt, isTty) {
11008
11594
  io.err(`launchpad destroy: ${e.message}`);
11009
11595
  return 1;
11010
11596
  }
11011
- io.err(`launchpad destroy failed: ${describe24(e)}`);
11597
+ io.err(`launchpad destroy failed: ${describe26(e)}`);
11012
11598
  return 1;
11013
11599
  }
11014
11600
  }
@@ -11208,7 +11794,7 @@ function printUsage4(io) {
11208
11794
  ].join(`
11209
11795
  `));
11210
11796
  }
11211
- function describe24(e) {
11797
+ function describe26(e) {
11212
11798
  return e instanceof Error ? e.message : String(e);
11213
11799
  }
11214
11800
 
@@ -11340,22 +11926,22 @@ async function treesEqual(cwd, tracked, headDir, headPaths) {
11340
11926
  if (tracked.length !== headPaths.length)
11341
11927
  return false;
11342
11928
  const expected = new Set(headPaths);
11343
- for (const relative5 of tracked) {
11344
- if (!expected.has(relative5))
11929
+ for (const relative6 of tracked) {
11930
+ if (!expected.has(relative6))
11345
11931
  return false;
11346
- if (!equal(await readEntry(path12.resolve(cwd, relative5)), await readEntry(path12.resolve(headDir, relative5)))) {
11932
+ if (!equal(await readEntry(path12.resolve(cwd, relative6)), await readEntry(path12.resolve(headDir, relative6)))) {
11347
11933
  return false;
11348
11934
  }
11349
11935
  }
11350
11936
  return true;
11351
11937
  }
11352
- async function listFiles(root, relative5 = "") {
11938
+ async function listFiles(root, relative6 = "") {
11353
11939
  const files = [];
11354
- const entries = await fs6.readdir(path12.resolve(root, relative5), {
11940
+ const entries = await fs6.readdir(path12.resolve(root, relative6), {
11355
11941
  withFileTypes: true
11356
11942
  });
11357
11943
  for (const entry of entries) {
11358
- const child = relative5.length === 0 ? entry.name : `${relative5}/${entry.name}`;
11944
+ const child = relative6.length === 0 ? entry.name : `${relative6}/${entry.name}`;
11359
11945
  if (entry.isDirectory())
11360
11946
  files.push(...await listFiles(root, child));
11361
11947
  else if (entry.isFile())
@@ -11364,12 +11950,12 @@ async function listFiles(root, relative5 = "") {
11364
11950
  return files.sort();
11365
11951
  }
11366
11952
  function gitOutput(cwd, args) {
11367
- return new Promise((resolve13, reject) => {
11953
+ return new Promise((resolve14, reject) => {
11368
11954
  execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
11369
11955
  if (error !== null)
11370
11956
  reject(error);
11371
11957
  else
11372
- resolve13(stdout);
11958
+ resolve14(stdout);
11373
11959
  });
11374
11960
  });
11375
11961
  }
@@ -11378,7 +11964,7 @@ function splitNull(value) {
11378
11964
  }
11379
11965
  async function reconcile(args) {
11380
11966
  const removals = await cleanAncestorRemovals(args);
11381
- const planned = await Promise.all(args.paths.map((relative5) => planPath(args, relative5, removals)));
11967
+ const planned = await Promise.all(args.paths.map((relative6) => planPath(args, relative6, removals)));
11382
11968
  const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
11383
11969
  if (conflicts.length > 0) {
11384
11970
  const report = {
@@ -11422,55 +12008,55 @@ async function reconcile(args) {
11422
12008
  }
11423
12009
  async function cleanAncestorRemovals(args) {
11424
12010
  const removals = new Set;
11425
- for (const relative5 of args.compare.deleted) {
11426
- assertSafePath(relative5);
11427
- const checked = await checkParents(args.cwd, relative5, removals);
12011
+ for (const relative6 of args.compare.deleted) {
12012
+ assertSafePath(relative6);
12013
+ const checked = await checkParents(args.cwd, relative6, removals);
11428
12014
  if (checked.kind === "blocked")
11429
12015
  continue;
11430
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
12016
+ const base = await readEntry(path12.resolve(args.baseDir, relative6));
11431
12017
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11432
12018
  if (local.kind !== "missing" && cleanLocal(base, local))
11433
- removals.add(relative5);
12019
+ removals.add(relative6);
11434
12020
  }
11435
12021
  return removals;
11436
12022
  }
11437
- async function planPath(args, relative5, removals) {
11438
- assertSafePath(relative5);
11439
- const checked = await checkParents(args.cwd, relative5, removals);
11440
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
11441
- const upstreamDeleted = args.compare.deleted.includes(relative5);
11442
- const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative5));
11443
- const unsupported = (args.compare.unsupported ?? []).includes(relative5);
12023
+ async function planPath(args, relative6, removals) {
12024
+ assertSafePath(relative6);
12025
+ const checked = await checkParents(args.cwd, relative6, removals);
12026
+ const base = await readEntry(path12.resolve(args.baseDir, relative6));
12027
+ const upstreamDeleted = args.compare.deleted.includes(relative6);
12028
+ const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative6));
12029
+ const unsupported2 = (args.compare.unsupported ?? []).includes(relative6);
11444
12030
  if (checked.kind === "blocked") {
11445
- return conflictPlan(relative5, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
12031
+ return conflictPlan(relative6, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11446
12032
  }
11447
12033
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11448
- if (unsupported) {
11449
- return conflictPlan(relative5, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
12034
+ if (unsupported2) {
12035
+ return conflictPlan(relative6, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11450
12036
  }
11451
12037
  if (alreadyResolved(local, head, upstreamDeleted)) {
11452
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: false };
12038
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: false };
11453
12039
  }
11454
12040
  if (unsupportedHead(head, upstreamDeleted)) {
11455
- return conflictPlan(relative5, checked.target, local, head, true, "unsupported upstream file type");
12041
+ return conflictPlan(relative6, checked.target, local, head, true, "unsupported upstream file type");
11456
12042
  }
11457
12043
  if (cleanLocal(base, local)) {
11458
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: true };
12044
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: true };
11459
12045
  }
11460
- return conflictPlan(relative5, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
12046
+ return conflictPlan(relative6, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
11461
12047
  }
11462
- function conflictPlan(relative5, target, local, head, upstreamDeleted, reason) {
12048
+ function conflictPlan(relative6, target, local, head, upstreamDeleted, reason) {
11463
12049
  const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
11464
- const digest = createHash3("sha256").update(relative5).digest("hex");
12050
+ const digest = createHash3("sha256").update(relative6).digest("hex");
11465
12051
  return {
11466
- relative: relative5,
12052
+ relative: relative6,
11467
12053
  target,
11468
12054
  local,
11469
12055
  head,
11470
12056
  upstreamDeleted,
11471
12057
  apply: false,
11472
12058
  conflict: {
11473
- path: relative5,
12059
+ path: relative6,
11474
12060
  reason,
11475
12061
  artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
11476
12062
  upstreamDeleted
@@ -11571,13 +12157,13 @@ function equal(left, right) {
11571
12157
  return false;
11572
12158
  return left.bytes.every((byte, index) => byte === right.bytes[index]);
11573
12159
  }
11574
- async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11575
- const checked = await checkParents(cwd, relative5, new Set, allowMetadata);
12160
+ async function writeAtomic(cwd, relative6, bytes, mode, allowMetadata = false) {
12161
+ const checked = await checkParents(cwd, relative6, new Set, allowMetadata);
11576
12162
  if (checked.kind === "blocked")
11577
12163
  throw new Error(checked.reason);
11578
12164
  const file = checked.target;
11579
12165
  await fs6.mkdir(path12.dirname(file), { recursive: true });
11580
- const rechecked = await checkParents(cwd, relative5, new Set, allowMetadata);
12166
+ const rechecked = await checkParents(cwd, relative6, new Set, allowMetadata);
11581
12167
  if (rechecked.kind === "blocked")
11582
12168
  throw new Error(rechecked.reason);
11583
12169
  const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
@@ -11592,8 +12178,8 @@ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11592
12178
  throw error;
11593
12179
  }
11594
12180
  }
11595
- async function writeJsonAtomic(cwd, relative5, value) {
11596
- await writeAtomic(cwd, relative5, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
12181
+ async function writeJsonAtomic(cwd, relative6, value) {
12182
+ await writeAtomic(cwd, relative6, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
11597
12183
  `), 384, true);
11598
12184
  }
11599
12185
  async function resetConflictArtifacts(cwd) {
@@ -11609,37 +12195,37 @@ async function cleanupConflictState(cwd) {
11609
12195
  throw new Error(report.reason);
11610
12196
  await fs6.rm(report.target, { force: true });
11611
12197
  }
11612
- function assertSafePath(relative5) {
11613
- if (!isSafeSourcePath(relative5)) {
11614
- throw new Error(`unsafe source path returned by bot: ${relative5}`);
12198
+ function assertSafePath(relative6) {
12199
+ if (!isSafeSourcePath(relative6)) {
12200
+ throw new Error(`unsafe source path returned by bot: ${relative6}`);
11615
12201
  }
11616
12202
  }
11617
- function isSafeSourcePath(relative5) {
11618
- return isSafeRelativePath(relative5) && !relative5.startsWith(`${MARKER_DIR}/`);
12203
+ function isSafeSourcePath(relative6) {
12204
+ return isSafeRelativePath(relative6) && !relative6.startsWith(`${MARKER_DIR}/`);
11619
12205
  }
11620
- function isSafeRelativePath(relative5) {
11621
- const normalized = path12.posix.normalize(relative5);
11622
- return relative5.length > 0 && !relative5.includes("\\") && !path12.isAbsolute(relative5) && normalized === relative5 && normalized !== ".." && !normalized.startsWith("../");
12206
+ function isSafeRelativePath(relative6) {
12207
+ const normalized = path12.posix.normalize(relative6);
12208
+ return relative6.length > 0 && !relative6.includes("\\") && !path12.isAbsolute(relative6) && normalized === relative6 && normalized !== ".." && !normalized.startsWith("../");
11623
12209
  }
11624
- function resolveInsideClone(cwd, relative5) {
12210
+ function resolveInsideClone(cwd, relative6) {
11625
12211
  const root = path12.resolve(cwd);
11626
- const target = path12.resolve(root, relative5);
12212
+ const target = path12.resolve(root, relative6);
11627
12213
  const fromRoot = path12.relative(root, target);
11628
12214
  if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
11629
- throw new Error(`unsafe source path returned by bot: ${relative5}`);
12215
+ throw new Error(`unsafe source path returned by bot: ${relative6}`);
11630
12216
  }
11631
12217
  return target;
11632
12218
  }
11633
- async function checkParents(cwd, relative5, projectedRemovals, allowMetadata = false) {
12219
+ async function checkParents(cwd, relative6, projectedRemovals, allowMetadata = false) {
11634
12220
  if (allowMetadata) {
11635
- if (!isSafeRelativePath(relative5)) {
11636
- throw new Error(`unsafe metadata path: ${relative5}`);
12221
+ if (!isSafeRelativePath(relative6)) {
12222
+ throw new Error(`unsafe metadata path: ${relative6}`);
11637
12223
  }
11638
12224
  } else {
11639
- assertSafePath(relative5);
12225
+ assertSafePath(relative6);
11640
12226
  }
11641
- const target = resolveInsideClone(cwd, relative5);
11642
- const components = relative5.split("/");
12227
+ const target = resolveInsideClone(cwd, relative6);
12228
+ const components = relative6.split("/");
11643
12229
  let current = path12.resolve(cwd);
11644
12230
  for (let index = 0;index < components.length - 1; index += 1) {
11645
12231
  current = path12.join(current, components[index]);
@@ -11778,7 +12364,7 @@ async function runPull(args, io) {
11778
12364
  io.err(`launchpad pull: ${e.message}`);
11779
12365
  return 1;
11780
12366
  }
11781
- io.err(`launchpad pull failed: ${describe25(e)}`);
12367
+ io.err(`launchpad pull failed: ${describe27(e)}`);
11782
12368
  return 1;
11783
12369
  }
11784
12370
  }
@@ -11886,7 +12472,7 @@ function printUsage5(io) {
11886
12472
  ].join(`
11887
12473
  `));
11888
12474
  }
11889
- function describe25(e) {
12475
+ function describe27(e) {
11890
12476
  return e instanceof Error ? e.message : String(e);
11891
12477
  }
11892
12478
 
@@ -12221,7 +12807,7 @@ async function runReview(args, io) {
12221
12807
  io.err(`launchpad review: ${e.message}`);
12222
12808
  return 1;
12223
12809
  }
12224
- io.err(`launchpad review failed: ${describe26(e)}`);
12810
+ io.err(`launchpad review failed: ${describe28(e)}`);
12225
12811
  return 1;
12226
12812
  }
12227
12813
  }
@@ -12299,12 +12885,12 @@ function severityRank(s) {
12299
12885
  return 3;
12300
12886
  }
12301
12887
  }
12302
- function describe26(e) {
12888
+ function describe28(e) {
12303
12889
  return e instanceof Error ? e.message : String(e);
12304
12890
  }
12305
12891
 
12306
12892
  // src/deploy/rollback.ts
12307
- import { existsSync as existsSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
12893
+ import { existsSync as existsSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
12308
12894
  import { resolve as resolvePath3 } from "node:path";
12309
12895
  import { createInterface as createInterface3 } from "node:readline/promises";
12310
12896
 
@@ -12359,7 +12945,7 @@ async function runRollback(opts, io, deps = {}) {
12359
12945
  cwd: process.cwd()
12360
12946
  });
12361
12947
  } catch (e) {
12362
- io.err(`launchpad rollback: git rev-parse failed to start: ${describe27(e)}`);
12948
+ io.err(`launchpad rollback: git rev-parse failed to start: ${describe29(e)}`);
12363
12949
  return 2;
12364
12950
  }
12365
12951
  if (revParse.exitCode !== 0) {
@@ -12374,7 +12960,7 @@ async function runRollback(opts, io, deps = {}) {
12374
12960
  try {
12375
12961
  show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
12376
12962
  } catch (e) {
12377
- io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
12963
+ io.err(`launchpad rollback: git show failed to start: ${describe29(e)}`);
12378
12964
  return 2;
12379
12965
  }
12380
12966
  if (show.exitCode !== 0) {
@@ -12408,7 +12994,7 @@ async function runRollback(opts, io, deps = {}) {
12408
12994
  try {
12409
12995
  writeFileSync5(manifestPath, historicalYaml, "utf8");
12410
12996
  } catch (e) {
12411
- io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
12997
+ io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe29(e)}`);
12412
12998
  return 2;
12413
12999
  }
12414
13000
  io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
@@ -12433,7 +13019,7 @@ function readCurrentManifest(path14) {
12433
13019
  return null;
12434
13020
  let raw;
12435
13021
  try {
12436
- raw = readFileSync15(path14, "utf8");
13022
+ raw = readFileSync16(path14, "utf8");
12437
13023
  } catch {
12438
13024
  return null;
12439
13025
  }
@@ -12490,7 +13076,7 @@ async function defaultPrompt4(question) {
12490
13076
  rl.close();
12491
13077
  }
12492
13078
  }
12493
- function describe27(e) {
13079
+ function describe29(e) {
12494
13080
  return e instanceof Error ? e.message : String(e);
12495
13081
  }
12496
13082
  function renderManifestError3(result, io) {
@@ -12589,13 +13175,13 @@ function parseArgs12(args) {
12589
13175
  }
12590
13176
 
12591
13177
  // src/secrets/push.ts
12592
- import { existsSync as existsSync12, readFileSync as readFileSync16 } from "node:fs";
13178
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "node:fs";
12593
13179
  import { resolve as resolvePath4 } from "node:path";
12594
13180
  import { createInterface as createInterface4 } from "node:readline/promises";
12595
13181
  import { stdin as input2, stdout as output2 } from "node:process";
12596
13182
 
12597
13183
  // src/secrets/env-parse.ts
12598
- var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
13184
+ var ENV_NAME2 = /^[A-Z][A-Z0-9_]*$/;
12599
13185
  var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
12600
13186
  function parseEnv(text) {
12601
13187
  const entries = new Map;
@@ -12620,7 +13206,7 @@ function parseEnv(text) {
12620
13206
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
12621
13207
  value = value.slice(1, -1);
12622
13208
  }
12623
- if (!ENV_NAME.test(key)) {
13209
+ if (!ENV_NAME2.test(key)) {
12624
13210
  invalidNames.push(key);
12625
13211
  continue;
12626
13212
  }
@@ -12653,9 +13239,9 @@ async function runSecretsPush(opts, io, deps = {}) {
12653
13239
  }
12654
13240
  let envText;
12655
13241
  try {
12656
- envText = readFileSync16(envPath, "utf8");
13242
+ envText = readFileSync17(envPath, "utf8");
12657
13243
  } catch (e) {
12658
- io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
13244
+ io.err(`launchpad secrets push: failed to read ${envPath}: ${describe30(e)}`);
12659
13245
  return 2;
12660
13246
  }
12661
13247
  const parsed = parseEnv(envText);
@@ -12839,10 +13425,10 @@ function mapPushError(e, slug, name, io) {
12839
13425
  io.err(`✗ secrets push ${name}: ${e.message}`);
12840
13426
  return 1;
12841
13427
  }
12842
- io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
13428
+ io.err(`✗ secrets push ${name} failed: ${describe30(e)}`);
12843
13429
  return 2;
12844
13430
  }
12845
- function describe28(e) {
13431
+ function describe30(e) {
12846
13432
  return e instanceof Error ? e.message : String(e);
12847
13433
  }
12848
13434
  function renderManifestError4(result, io) {
@@ -12978,7 +13564,7 @@ function renderManifestError5(result, io) {
12978
13564
  }
12979
13565
 
12980
13566
  // src/secrets/set.ts
12981
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
13567
+ import { existsSync as existsSync13, readFileSync as readFileSync18 } from "node:fs";
12982
13568
  import { resolve as resolvePath6 } from "node:path";
12983
13569
  import { parse as parseYaml8 } from "yaml";
12984
13570
  var CELL_LABEL2 = {
@@ -12995,7 +13581,7 @@ function loadSet(fleetFile, setName, io) {
12995
13581
  }
12996
13582
  let obj;
12997
13583
  try {
12998
- obj = parseYaml8(readFileSync17(path14, "utf8"));
13584
+ obj = parseYaml8(readFileSync18(path14, "utf8"));
12999
13585
  } catch (e) {
13000
13586
  io.err(`✗ ${path14}`);
13001
13587
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
@@ -13087,7 +13673,7 @@ async function runSecretsPushSet(opts, io, deps = {}) {
13087
13673
  }
13088
13674
  let envText;
13089
13675
  try {
13090
- envText = readFileSync17(envPath, "utf8");
13676
+ envText = readFileSync18(envPath, "utf8");
13091
13677
  } catch (e) {
13092
13678
  io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
13093
13679
  return 2;
@@ -13232,7 +13818,7 @@ function setPushExit(e) {
13232
13818
 
13233
13819
  // src/commands/secrets-template.ts
13234
13820
  import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
13235
- import { resolve as resolve13 } from "node:path";
13821
+ import { resolve as resolve14 } from "node:path";
13236
13822
  async function runSecretsTemplate(args, io) {
13237
13823
  const flags = parseFlags4(args);
13238
13824
  if (flags.kind === "usage-error") {
@@ -13240,7 +13826,7 @@ async function runSecretsTemplate(args, io) {
13240
13826
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
13241
13827
  return 64;
13242
13828
  }
13243
- const manifestPath = resolve13(process.cwd(), flags.file ?? "launchpad.yaml");
13829
+ const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
13244
13830
  const result = loadManifest(manifestPath);
13245
13831
  const renderResult = renderManifest(result, io);
13246
13832
  if (renderResult.kind !== "ok") {
@@ -13254,7 +13840,7 @@ async function runSecretsTemplate(args, io) {
13254
13840
  }
13255
13841
  return 0;
13256
13842
  }
13257
- const outPath = resolve13(process.cwd(), flags.out);
13843
+ const outPath = resolve14(process.cwd(), flags.out);
13258
13844
  if (existsSync14(outPath) && !flags.force) {
13259
13845
  io.err(`launchpad secrets template: ${outPath} already exists`);
13260
13846
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
@@ -13551,16 +14137,16 @@ function printHelp3(io) {
13551
14137
 
13552
14138
  // src/commands/skills.ts
13553
14139
  import { fileURLToPath } from "node:url";
13554
- import { dirname as dirname9, join as join15, resolve as resolve14 } from "node:path";
14140
+ import { dirname as dirname10, join as join16, resolve as resolve15 } from "node:path";
13555
14141
  import { promises as fs7, existsSync as existsSync15 } from "node:fs";
13556
14142
 
13557
14143
  // src/skills-bundle.ts
13558
14144
  import { homedir as homedir2 } from "node:os";
13559
- import { join as join14 } from "node:path";
13560
- import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
14145
+ import { join as join15 } from "node:path";
14146
+ import { readFileSync as readFileSync19, readdirSync as readdirSync2 } from "node:fs";
13561
14147
  var SKILL_PREFIX = "launchpad-";
13562
14148
  function skillsTargetDir() {
13563
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
14149
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
13564
14150
  }
13565
14151
  function readInstalledSkills() {
13566
14152
  try {
@@ -13570,7 +14156,7 @@ function readInstalledSkills() {
13570
14156
  return { present: false, version: null };
13571
14157
  return {
13572
14158
  present: true,
13573
- version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
14159
+ version: readSkillVersion(join15(dir, bundle.name, "SKILL.md"))
13574
14160
  };
13575
14161
  } catch {
13576
14162
  return { present: false, version: null };
@@ -13578,7 +14164,7 @@ function readInstalledSkills() {
13578
14164
  }
13579
14165
  function readSkillVersion(path14) {
13580
14166
  try {
13581
- const text = readFileSync18(path14, "utf8");
14167
+ const text = readFileSync19(path14, "utf8");
13582
14168
  const fenceEnd = text.indexOf(`
13583
14169
  ---`, 4);
13584
14170
  const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
@@ -13632,7 +14218,7 @@ async function runSkills(args, io) {
13632
14218
  return 64;
13633
14219
  }
13634
14220
  } catch (e) {
13635
- io.err(`launchpad skills ${action}: ${describe29(e)}`);
14221
+ io.err(`launchpad skills ${action}: ${describe31(e)}`);
13636
14222
  return 1;
13637
14223
  }
13638
14224
  }
@@ -13655,13 +14241,13 @@ function resolveInstallEnv() {
13655
14241
  return { bundleDir, userSkillsDir };
13656
14242
  }
13657
14243
  function defaultBundleDir() {
13658
- const here = dirname9(fileURLToPath(import.meta.url));
14244
+ const here = dirname10(fileURLToPath(import.meta.url));
13659
14245
  const candidates = [
13660
- resolve14(here, "..", "skills"),
13661
- resolve14(here, "..", "..", "skills")
14246
+ resolve15(here, "..", "skills"),
14247
+ resolve15(here, "..", "..", "skills")
13662
14248
  ];
13663
14249
  for (const c of candidates) {
13664
- if (existsSync15(join15(c, "launchpad-onboard", "SKILL.md"))) {
14250
+ if (existsSync15(join16(c, "launchpad-onboard", "SKILL.md"))) {
13665
14251
  return c;
13666
14252
  }
13667
14253
  }
@@ -13682,12 +14268,12 @@ async function doInstall(io) {
13682
14268
  }
13683
14269
  let installed = 0;
13684
14270
  for (const skill of BUNDLED_SKILLS) {
13685
- const src = join15(env.bundleDir, skill);
14271
+ const src = join16(env.bundleDir, skill);
13686
14272
  if (!await isDir(src)) {
13687
14273
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
13688
14274
  return 1;
13689
14275
  }
13690
- const dest = join15(env.userSkillsDir, skill);
14276
+ const dest = join16(env.userSkillsDir, skill);
13691
14277
  await fs7.rm(dest, { recursive: true, force: true });
13692
14278
  await fs7.cp(src, dest, { recursive: true });
13693
14279
  installed++;
@@ -13712,7 +14298,7 @@ async function doUninstall(io) {
13712
14298
  continue;
13713
14299
  if (!isBundleManaged(entry.name))
13714
14300
  continue;
13715
- const target = join15(env.userSkillsDir, entry.name);
14301
+ const target = join16(env.userSkillsDir, entry.name);
13716
14302
  await fs7.rm(target, { recursive: true, force: true });
13717
14303
  removed++;
13718
14304
  io.out(`✗ removed ${target}`);
@@ -13735,7 +14321,7 @@ async function doList(io) {
13735
14321
  }
13736
14322
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
13737
14323
  for (const name of managedDirs) {
13738
- const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
14324
+ const skillFile = join16(env.userSkillsDir, name, "SKILL.md");
13739
14325
  const version = await readVersion(skillFile);
13740
14326
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
13741
14327
  }
@@ -13762,7 +14348,7 @@ async function isDir(path14) {
13762
14348
  return false;
13763
14349
  }
13764
14350
  }
13765
- function describe29(e) {
14351
+ function describe31(e) {
13766
14352
  return e instanceof Error ? e.message : String(e);
13767
14353
  }
13768
14354
 
@@ -13770,9 +14356,9 @@ function describe29(e) {
13770
14356
  import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
13771
14357
  import { promisify } from "node:util";
13772
14358
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13773
- import { dirname as dirname10, resolve as resolve15, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
14359
+ import { dirname as dirname11, resolve as resolve16, relative as relative6, isAbsolute as isAbsolute3, join as join17 } from "node:path";
13774
14360
  import { homedir as homedir3, tmpdir } from "node:os";
13775
- import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
14361
+ import { readFileSync as readFileSync20, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
13776
14362
 
13777
14363
  // src/commands/channel-auth.ts
13778
14364
  import { createServer as createServer2 } from "node:http";
@@ -13815,9 +14401,9 @@ async function startLoopback(state, timeoutMs) {
13815
14401
  resolveCode(code);
13816
14402
  }
13817
14403
  });
13818
- const bound = await new Promise((resolve15) => {
13819
- server.once("error", () => resolve15(false));
13820
- server.listen(0, "127.0.0.1", () => resolve15(true));
14404
+ const bound = await new Promise((resolve16) => {
14405
+ server.once("error", () => resolve16(false));
14406
+ server.listen(0, "127.0.0.1", () => resolve16(true));
13821
14407
  });
13822
14408
  if (!bound)
13823
14409
  return null;
@@ -13893,7 +14479,7 @@ var PKG = "@m-kopa/launchpad-cli";
13893
14479
  var REGISTRY = "https://registry.npmjs.org";
13894
14480
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
13895
14481
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
13896
- var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
14482
+ var CHANNEL_MARKER = join17(homedir3(), ".launchpad", "channel");
13897
14483
  var EXIT_UPDATE_AVAILABLE = 10;
13898
14484
  var UPGRADE_ARGS = {
13899
14485
  npm: ["install", "-g", `${PKG}@latest`],
@@ -14022,8 +14608,8 @@ async function openSystemBrowser(url) {
14022
14608
  await execFileAsync(opener, [url]);
14023
14609
  }
14024
14610
  async function runInstallerScript(script) {
14025
- const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
14026
- const file = join16(dir, "install.sh");
14611
+ const dir = mkdtempSync(join17(tmpdir(), "launchpad-update-"));
14612
+ const file = join17(dir, "install.sh");
14027
14613
  try {
14028
14614
  writeFileSync7(file, script, { mode: 448 });
14029
14615
  return await new Promise((resolvePromise) => {
@@ -14047,7 +14633,7 @@ function resolveLatestVersion() {
14047
14633
  }
14048
14634
  function detectInstallChannel() {
14049
14635
  try {
14050
- return readFileSync19(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
14636
+ return readFileSync20(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
14051
14637
  } catch {
14052
14638
  return "github";
14053
14639
  }
@@ -14110,7 +14696,7 @@ function errStderr(e) {
14110
14696
  return "";
14111
14697
  }
14112
14698
  async function detectPackageManager2() {
14113
- const pkgRoot = resolve15(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
14699
+ const pkgRoot = resolve16(dirname11(fileURLToPath2(import.meta.url)), "..", "..");
14114
14700
  const candidates = [];
14115
14701
  const npmRoot = await pmRoot("npm");
14116
14702
  if (npmRoot !== null)
@@ -14118,7 +14704,7 @@ async function detectPackageManager2() {
14118
14704
  const pnpmRoot = await pmRoot("pnpm");
14119
14705
  if (pnpmRoot !== null)
14120
14706
  candidates.push(["pnpm", pnpmRoot]);
14121
- candidates.push(["bun", resolve15(homedir3(), ".bun/install/global/node_modules")]);
14707
+ candidates.push(["bun", resolve16(homedir3(), ".bun/install/global/node_modules")]);
14122
14708
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
14123
14709
  return matches.length === 1 ? matches[0][0] : null;
14124
14710
  }
@@ -14132,7 +14718,7 @@ async function pmRoot(pm) {
14132
14718
  }
14133
14719
  }
14134
14720
  function pathContains(root, child) {
14135
- const rel = relative5(root, child);
14721
+ const rel = relative6(root, child);
14136
14722
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
14137
14723
  }
14138
14724
  function runUpgrade(pm) {
@@ -14255,8 +14841,8 @@ function printHelp5(io) {
14255
14841
  }
14256
14842
 
14257
14843
  // src/commands/validate.ts
14258
- import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14259
- import { dirname as dirname11, resolve as resolve16 } from "node:path";
14844
+ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14845
+ import { dirname as dirname12, resolve as resolve17 } from "node:path";
14260
14846
  var validateCommand = {
14261
14847
  name: "validate",
14262
14848
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -14270,11 +14856,21 @@ async function runValidate(args, io) {
14270
14856
  return 64;
14271
14857
  }
14272
14858
  const { file, json, strictGroups } = parseResult;
14273
- const path14 = resolve16(process.cwd(), file ?? "launchpad.yaml");
14859
+ const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14274
14860
  const result = loadManifest(path14);
14275
14861
  if (result.kind !== "ok") {
14276
14862
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14277
14863
  }
14864
+ const unsupported2 = rejectUnsupportedCliRuntime({
14865
+ command: "validate",
14866
+ manifestPath: path14,
14867
+ manifest: result.manifest,
14868
+ io,
14869
+ json,
14870
+ structurallyValidName: result.manifest.metadata.name
14871
+ });
14872
+ if (unsupported2 !== null)
14873
+ return unsupported2;
14278
14874
  const boundary = checkBoundary(path14, result.manifest.app !== undefined);
14279
14875
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14280
14876
  if (serveErrors.length > 0) {
@@ -14284,7 +14880,7 @@ async function runValidate(args, io) {
14284
14880
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14285
14881
  }
14286
14882
  function checkBoundary(manifestPath, declared) {
14287
- const dir = dirname11(manifestPath);
14883
+ const dir = dirname12(manifestPath);
14288
14884
  let files;
14289
14885
  try {
14290
14886
  files = walkCwd(dir).files;
@@ -14293,7 +14889,7 @@ function checkBoundary(manifestPath, declared) {
14293
14889
  }
14294
14890
  let manifestYaml;
14295
14891
  try {
14296
- manifestYaml = readFileSync20(manifestPath, "utf8");
14892
+ manifestYaml = readFileSync21(manifestPath, "utf8");
14297
14893
  } catch {
14298
14894
  manifestYaml = null;
14299
14895
  }
@@ -14316,8 +14912,8 @@ function checkStaticServeDir(manifestPath, manifest) {
14316
14912
  `static app destination_dir '${serveDir}' is unsafe — use a confined relative subdirectory (e.g. '${STATIC_SERVE_DIR}'). ` + `Serving '.' or an absolute/parent path would publish files outside your app's content.`
14317
14913
  ];
14318
14914
  }
14319
- const dir = dirname11(manifestPath);
14320
- const abs = resolve16(dir, serveDir);
14915
+ const dir = dirname12(manifestPath);
14916
+ const abs = resolve17(dir, serveDir);
14321
14917
  if (!existsSync16(abs)) {
14322
14918
  return [
14323
14919
  `static app serves '${serveDir}/' but that directory does not exist — nothing would be served. ` + `Create ${serveDir}/index.html (or run \`launchpad init\` to scaffold it).`
@@ -14327,7 +14923,7 @@ function checkStaticServeDir(manifestPath, manifest) {
14327
14923
  try {
14328
14924
  isDir2 = statSync2(abs).isDirectory();
14329
14925
  } catch (e) {
14330
- return [`failed to inspect static served directory '${serveDir}/': ${describe30(e)}`];
14926
+ return [`failed to inspect static served directory '${serveDir}/': ${describe32(e)}`];
14331
14927
  }
14332
14928
  if (!isDir2) {
14333
14929
  return [`static app serves '${serveDir}/' but that path is not a directory.`];
@@ -14336,14 +14932,14 @@ function checkStaticServeDir(manifestPath, manifest) {
14336
14932
  try {
14337
14933
  entries = readdirSync3(abs);
14338
14934
  } catch (e) {
14339
- return [`failed to read static served directory '${serveDir}/': ${describe30(e)}`];
14935
+ return [`failed to read static served directory '${serveDir}/': ${describe32(e)}`];
14340
14936
  }
14341
14937
  if (entries.length === 0) {
14342
14938
  return [
14343
14939
  `static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
14344
14940
  ];
14345
14941
  }
14346
- if (!existsSync16(resolve16(abs, "index.html"))) {
14942
+ if (!existsSync16(resolve17(abs, "index.html"))) {
14347
14943
  return [
14348
14944
  `static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
14349
14945
  ];
@@ -14391,7 +14987,7 @@ async function checkGroup(allowedGroup, cfg) {
14391
14987
  if (e instanceof ForbiddenError) {
14392
14988
  return { kind: "forbidden", message: e.message };
14393
14989
  }
14394
- return { kind: "network", message: describe30(e) };
14990
+ return { kind: "network", message: describe32(e) };
14395
14991
  }
14396
14992
  }
14397
14993
  async function checkGroups(groups) {
@@ -14488,6 +15084,8 @@ function renderHumanError(result, io) {
14488
15084
  }
14489
15085
  function renderJsonOk(result, group, boundary, io) {
14490
15086
  const base = {
15087
+ manifest_valid: true,
15088
+ runtime_supported: true,
14491
15089
  name: result.manifest.metadata.name,
14492
15090
  deploymentType: result.manifest.deployment.type,
14493
15091
  appBoundary: {
@@ -14604,7 +15202,7 @@ function parseArgs13(args) {
14604
15202
  }
14605
15203
  return { kind: "ok", file, json, strictGroups };
14606
15204
  }
14607
- function describe30(e) {
15205
+ function describe32(e) {
14608
15206
  return e instanceof Error ? e.message : String(e);
14609
15207
  }
14610
15208
 
@@ -14657,7 +15255,7 @@ async function runWhoami(args, io) {
14657
15255
  io.err(e.message);
14658
15256
  return 1;
14659
15257
  }
14660
- io.err(`launchpad whoami failed: ${describe31(e)}`);
15258
+ io.err(`launchpad whoami failed: ${describe33(e)}`);
14661
15259
  return 1;
14662
15260
  }
14663
15261
  }
@@ -14687,11 +15285,11 @@ async function runShare(args, io) {
14687
15285
  io.err(e.message);
14688
15286
  return 1;
14689
15287
  }
14690
- io.err(`launchpad whoami --share failed: ${describe31(e)}`);
15288
+ io.err(`launchpad whoami --share failed: ${describe33(e)}`);
14691
15289
  return 1;
14692
15290
  }
14693
15291
  }
14694
- function describe31(e) {
15292
+ function describe33(e) {
14695
15293
  return e instanceof Error ? e.message : String(e);
14696
15294
  }
14697
15295
 
@@ -14857,7 +15455,7 @@ async function runEditor(action, args, io, deps) {
14857
15455
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
14858
15456
  return 1;
14859
15457
  }
14860
- io.err(`launchpad ${verb} failed: ${describe32(e)}`);
15458
+ io.err(`launchpad ${verb} failed: ${describe34(e)}`);
14861
15459
  return 1;
14862
15460
  }
14863
15461
  }
@@ -14891,7 +15489,7 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
14891
15489
  nonThrowingStatuses: [409]
14892
15490
  });
14893
15491
  }
14894
- function describe32(e) {
15492
+ function describe34(e) {
14895
15493
  return e instanceof Error ? e.message : String(e);
14896
15494
  }
14897
15495
  var grantEditorCommand = makeEditorCommand("grant");
@@ -15097,7 +15695,7 @@ async function runOwner(action, args, io, deps) {
15097
15695
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
15098
15696
  return 1;
15099
15697
  }
15100
- io.err(`launchpad ${verb} failed: ${describe33(e)}`);
15698
+ io.err(`launchpad ${verb} failed: ${describe35(e)}`);
15101
15699
  return 1;
15102
15700
  }
15103
15701
  }
@@ -15131,12 +15729,92 @@ async function sendMutation2(action, slug, target, version, cfg, deps) {
15131
15729
  nonThrowingStatuses: [409]
15132
15730
  });
15133
15731
  }
15134
- function describe33(e) {
15732
+ function describe35(e) {
15135
15733
  return e instanceof Error ? e.message : String(e);
15136
15734
  }
15137
15735
  var addOwnerCommand = makeOwnerCommand("add");
15138
15736
  var removeOwnerCommand = makeOwnerCommand("remove");
15139
15737
 
15738
+ // src/commands/admin.ts
15739
+ var REAL_DEPS3 = { loadConfig, apiJson };
15740
+ var adminCommand = makeAdminCommand();
15741
+ function makeAdminCommand(deps = REAL_DEPS3) {
15742
+ return {
15743
+ name: "admin",
15744
+ summary: "run authenticated platform-operator actions",
15745
+ run: (args, io) => runAdmin(args, io, deps)
15746
+ };
15747
+ }
15748
+ function statusPageUrl(value, allowedHost) {
15749
+ try {
15750
+ const url = new URL(value);
15751
+ if (url.protocol !== "https:")
15752
+ return null;
15753
+ if (url.hostname !== allowedHost)
15754
+ return null;
15755
+ if (!/^\/d\/[^/]+$/u.test(url.pathname))
15756
+ return null;
15757
+ if (url.username !== "" || url.password !== "")
15758
+ return null;
15759
+ if (url.search !== "" || url.hash !== "")
15760
+ return null;
15761
+ return url.toString();
15762
+ } catch {
15763
+ return null;
15764
+ }
15765
+ }
15766
+ async function runAdmin(args, io, deps) {
15767
+ if (args.includes("--help") || args.includes("-h")) {
15768
+ printHelp6(io);
15769
+ return 0;
15770
+ }
15771
+ const config = deps.loadConfig();
15772
+ const parsed = parseStatusRevokeArgs(args, config.statusPageHost ?? DEFAULT_STATUS_PAGE_HOST);
15773
+ if (parsed.kind === "unknown") {
15774
+ io.err(`launchpad admin: unknown subcommand '${parsed.subcommand}'`);
15775
+ printHelp6(io);
15776
+ return 64;
15777
+ }
15778
+ if (parsed.kind === "invalid") {
15779
+ io.err("launchpad admin: expected `status-revoke <status-url>`");
15780
+ printHelp6(io);
15781
+ return 64;
15782
+ }
15783
+ return revokeStatusPage(parsed.url, config, io, deps);
15784
+ }
15785
+ function parseStatusRevokeArgs(args, allowedHost) {
15786
+ if (args[0] !== undefined && args[0] !== "status-revoke") {
15787
+ return { kind: "unknown", subcommand: args[0] };
15788
+ }
15789
+ if (args.length !== 2)
15790
+ return { kind: "invalid" };
15791
+ const url = statusPageUrl(args[1], allowedHost);
15792
+ return url === null ? { kind: "invalid" } : { kind: "ok", url };
15793
+ }
15794
+ async function revokeStatusPage(url, config, io, deps) {
15795
+ try {
15796
+ const response = await deps.apiJson(config, {
15797
+ method: "POST",
15798
+ path: "/status-page/revoke",
15799
+ jsonBody: { url }
15800
+ });
15801
+ io.out(statusRevokeMessage(response));
15802
+ return 0;
15803
+ } catch (error) {
15804
+ io.err(`launchpad admin status-revoke: ${error instanceof Error ? error.message : String(error)}`);
15805
+ return 1;
15806
+ }
15807
+ }
15808
+ function statusRevokeMessage(response) {
15809
+ return response.revoked ? "Status page revoked." : "Status page was already absent.";
15810
+ }
15811
+ function printHelp6(io) {
15812
+ io.out("launchpad admin - authenticated platform-operator actions.");
15813
+ io.out("");
15814
+ io.out("Usage:");
15815
+ io.out(" launchpad admin status-revoke <status-url>");
15816
+ }
15817
+
15140
15818
  // src/commands/list-owners.ts
15141
15819
  function realDeps4() {
15142
15820
  return { loadConfig, apiJson };
@@ -15212,20 +15890,20 @@ var listOwnersCommand = makeListOwnersCommand();
15212
15890
  // src/update-notifier.ts
15213
15891
  import { spawn as spawn6 } from "node:child_process";
15214
15892
  import { homedir as homedir4 } from "node:os";
15215
- import { join as join17 } from "node:path";
15893
+ import { join as join18 } from "node:path";
15216
15894
  import {
15217
15895
  existsSync as existsSync17,
15218
15896
  mkdirSync as mkdirSync4,
15219
- readFileSync as readFileSync21,
15897
+ readFileSync as readFileSync22,
15220
15898
  writeFileSync as writeFileSync8
15221
15899
  } from "node:fs";
15222
15900
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
15223
15901
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
15224
15902
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
15225
- var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
15903
+ var CACHE_FILE = join18(homedir4(), ".launchpad", "update-check.json");
15226
15904
  function readCache2() {
15227
15905
  try {
15228
- const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
15906
+ const raw = JSON.parse(readFileSync22(CACHE_FILE, "utf8"));
15229
15907
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
15230
15908
  const latest = raw.latest;
15231
15909
  return {
@@ -15238,12 +15916,12 @@ function readCache2() {
15238
15916
  }
15239
15917
  function writeCache2(state) {
15240
15918
  try {
15241
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15919
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15242
15920
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
15243
15921
  `, { mode: 384 });
15244
15922
  } catch {}
15245
15923
  }
15246
- var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
15924
+ var SKILLS_HINT_MARKER = join18(homedir4(), ".launchpad", "skills-hint-shown");
15247
15925
  function absentHintShown() {
15248
15926
  try {
15249
15927
  return existsSync17(SKILLS_HINT_MARKER);
@@ -15253,7 +15931,7 @@ function absentHintShown() {
15253
15931
  }
15254
15932
  function markAbsentHintShown() {
15255
15933
  try {
15256
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15934
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15257
15935
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
15258
15936
  `, { mode: 384 });
15259
15937
  } catch {}
@@ -15356,8 +16034,8 @@ var refreshUpdateCacheCommand = {
15356
16034
  // src/telemetry.ts
15357
16035
  import { spawn as spawn7 } from "node:child_process";
15358
16036
  import { homedir as homedir5 } from "node:os";
15359
- import { join as join18 } from "node:path";
15360
- import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
16037
+ import { join as join19 } from "node:path";
16038
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "node:fs";
15361
16039
  import { randomUUID } from "node:crypto";
15362
16040
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
15363
16041
  var POSTHOG_HOST = "https://us.i.posthog.com";
@@ -15365,8 +16043,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
15365
16043
  var CAPTURE_PATH = "/i/v0/e/";
15366
16044
  var SEND_TIMEOUT_MS = 3000;
15367
16045
  var APP = "launchpad-cli";
15368
- var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
15369
- var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
16046
+ var DEVICE_ID_FILE = join19(homedir5(), ".launchpad", "telemetry-id.json");
16047
+ var FIRST_RUN_MARKER = join19(homedir5(), ".launchpad", "telemetry-notice-shown");
15370
16048
  function buildEvent(ctx, identity, nowMs) {
15371
16049
  const properties = {
15372
16050
  app: APP,
@@ -15433,14 +16111,14 @@ async function resolveIdentityReal() {
15433
16111
  }
15434
16112
  function getOrCreateDeviceId() {
15435
16113
  try {
15436
- const raw = JSON.parse(readFileSync22(DEVICE_ID_FILE, "utf8"));
16114
+ const raw = JSON.parse(readFileSync23(DEVICE_ID_FILE, "utf8"));
15437
16115
  const id2 = raw?.deviceId;
15438
16116
  if (typeof id2 === "string" && id2)
15439
16117
  return id2;
15440
16118
  } catch {}
15441
16119
  const id = randomUUID();
15442
16120
  try {
15443
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16121
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15444
16122
  writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
15445
16123
  `, {
15446
16124
  mode: 384
@@ -15518,7 +16196,7 @@ function firstRunNoticeShown() {
15518
16196
  }
15519
16197
  function markFirstRunNotice() {
15520
16198
  try {
15521
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16199
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15522
16200
  writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
15523
16201
  `, { mode: 384 });
15524
16202
  } catch {}
@@ -15584,22 +16262,22 @@ import { createInterface as createInterface7 } from "node:readline/promises";
15584
16262
  import { platform } from "node:os";
15585
16263
 
15586
16264
  // src/report/breadcrumb.ts
15587
- import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
16265
+ import { writeFileSync as writeFileSync10, readFileSync as readFileSync24, mkdirSync as mkdirSync6 } from "node:fs";
15588
16266
  import { homedir as homedir6 } from "node:os";
15589
- import { join as join19, dirname as dirname12 } from "node:path";
16267
+ import { join as join20, dirname as dirname13 } from "node:path";
15590
16268
  function breadcrumbPath() {
15591
- return join19(homedir6(), ".launchpad", "last-run.json");
16269
+ return join20(homedir6(), ".launchpad", "last-run.json");
15592
16270
  }
15593
16271
  function writeBreadcrumb(verb, exit) {
15594
16272
  try {
15595
16273
  const p = breadcrumbPath();
15596
- mkdirSync6(dirname12(p), { recursive: true });
16274
+ mkdirSync6(dirname13(p), { recursive: true });
15597
16275
  writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
15598
16276
  } catch {}
15599
16277
  }
15600
16278
  function readBreadcrumb() {
15601
16279
  try {
15602
- const raw = JSON.parse(readFileSync23(breadcrumbPath(), "utf8"));
16280
+ const raw = JSON.parse(readFileSync24(breadcrumbPath(), "utf8"));
15603
16281
  if (typeof raw !== "object" || raw === null)
15604
16282
  return null;
15605
16283
  const r = raw;
@@ -15793,6 +16471,7 @@ var COMMANDS = [
15793
16471
  addOwnerCommand,
15794
16472
  removeOwnerCommand,
15795
16473
  listOwnersCommand,
16474
+ adminCommand,
15796
16475
  makeReportCommand("bug"),
15797
16476
  makeReportCommand("feature"),
15798
16477
  refreshUpdateCacheCommand,
@@ -15800,7 +16479,7 @@ var COMMANDS = [
15800
16479
  ];
15801
16480
  async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
15802
16481
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
15803
- printHelp6(io, commands);
16482
+ printHelp7(io, commands);
15804
16483
  return 0;
15805
16484
  }
15806
16485
  if (argv[0] === "--version" || argv[0] === "-v") {
@@ -15863,9 +16542,11 @@ function requiresCliFloorPreflight(verb, args) {
15863
16542
  return args[0] === "set" || args[0] === "rm";
15864
16543
  if (verb === "secrets")
15865
16544
  return args[0] === "push";
16545
+ if (verb === "admin")
16546
+ return args[0] === "status-revoke";
15866
16547
  return false;
15867
16548
  }
15868
- function printHelp6(io, commands) {
16549
+ function printHelp7(io, commands) {
15869
16550
  io.out("launchpad — manage Launchpad-deployed apps from the command line.");
15870
16551
  io.out("");
15871
16552
  io.out("Usage:");