@m-kopa/launchpad-cli 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,18 +19,20 @@ 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.0";
22
+ var CLI_VERSION = "0.55.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
26
26
  import * as path from "node:path";
27
27
  var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
28
28
  var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
29
+ var DEFAULT_STATUS_PAGE_HOST = "status.launchpad.m-kopa.us";
29
30
  function loadConfig(env = process.env) {
30
31
  const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
31
32
  const botUrl = rawBotUrl.replace(/\/+$/, "");
32
33
  const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
33
34
  const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
35
+ const statusPageHost = (env.LAUNCHPAD_STATUS_PAGE_HOST ?? DEFAULT_STATUS_PAGE_HOST).trim().toLowerCase();
34
36
  const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
35
37
  const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
36
38
  const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
@@ -40,6 +42,7 @@ function loadConfig(env = process.env) {
40
42
  return {
41
43
  botUrl,
42
44
  authGatewayUrl,
45
+ statusPageHost,
43
46
  authLegacy,
44
47
  sessionPath,
45
48
  cacheDir,
@@ -1867,7 +1870,7 @@ function describe10(e) {
1867
1870
  }
1868
1871
 
1869
1872
  // src/commands/deploy.ts
1870
- import { existsSync as existsSync6 } from "node:fs";
1873
+ import { existsSync as existsSync7 } from "node:fs";
1871
1874
  import * as path8 from "node:path";
1872
1875
 
1873
1876
  // src/bundle/orchestrate.ts
@@ -2657,6 +2660,7 @@ var ManifestSchema = z.object({
2657
2660
  app: AppBoundarySchema.optional(),
2658
2661
  verify: VerifySchema.optional(),
2659
2662
  confine_origin: z.boolean().optional(),
2663
+ origin: z.enum(["pages", "dispatch"]).optional(),
2660
2664
  catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
2661
2665
  sensitive: z.boolean().optional()
2662
2666
  }).strict().superRefine((m, ctx) => {
@@ -3047,7 +3051,9 @@ var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
3047
3051
  ".github/scripts/parse-tsc.mjs",
3048
3052
  ".github/scripts/parse-vitest.mjs",
3049
3053
  ".github/scripts/parse-gitleaks.mjs",
3050
- ".github/scripts/parse-bun-audit.mjs"
3054
+ ".github/scripts/parse-bun-audit.mjs",
3055
+ ".github/scripts/parse-html-validate.mjs",
3056
+ ".github/scripts/parse-hadolint.mjs"
3051
3057
  ]);
3052
3058
  function checkDenyList(path7) {
3053
3059
  const segments = path7.split("/");
@@ -3290,6 +3296,7 @@ var SpecSchema = z2.object({
3290
3296
  app: AppBoundarySchema.optional(),
3291
3297
  verify: VerifySchema.optional(),
3292
3298
  confine_origin: z2.boolean().optional(),
3299
+ origin: z2.enum(["pages", "dispatch"]).optional(),
3293
3300
  sensitive: z2.boolean().optional(),
3294
3301
  catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
3295
3302
  env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
@@ -3335,6 +3342,43 @@ var SpecSchema = z2.object({
3335
3342
  });
3336
3343
  }
3337
3344
  }
3345
+ if (s.origin === "dispatch") {
3346
+ if (isContainer) {
3347
+ ctx.addIssue({
3348
+ code: z2.ZodIssueCode.custom,
3349
+ path: ["origin"],
3350
+ message: "origin: dispatch is not available for container apps — they run on the container runtime (ADR 0015/0016), not in a Workers dispatch namespace."
3351
+ });
3352
+ }
3353
+ if ((s.auth ?? "access") !== "gateway") {
3354
+ ctx.addIssue({
3355
+ code: z2.ZodIssueCode.custom,
3356
+ path: ["origin"],
3357
+ message: "origin: dispatch requires an explicit auth: gateway (auth defaults to 'access' when omitted). A dispatch-namespace Worker has no public hostname for Cloudflare Access to gate, so the gateway must be the sole gate."
3358
+ });
3359
+ }
3360
+ if (s.confine_origin !== undefined) {
3361
+ ctx.addIssue({
3362
+ code: z2.ZodIssueCode.custom,
3363
+ path: ["confine_origin"],
3364
+ message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3365
+ });
3366
+ }
3367
+ if (s.env_vars !== undefined) {
3368
+ ctx.addIssue({
3369
+ code: z2.ZodIssueCode.custom,
3370
+ path: ["env_vars"],
3371
+ message: "env_vars are not yet supported with origin: dispatch — a dispatch workspace has no Pages module to apply them, and accepting them here would silently drop them. Worker-level vars land with the v2 deploy path."
3372
+ });
3373
+ }
3374
+ if (s.build !== undefined) {
3375
+ ctx.addIssue({
3376
+ code: z2.ZodIssueCode.custom,
3377
+ path: ["build"],
3378
+ message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
3379
+ });
3380
+ }
3381
+ }
3338
3382
  if (!isContainer && s.containers !== undefined) {
3339
3383
  ctx.addIssue({
3340
3384
  code: z2.ZodIssueCode.custom,
@@ -3428,6 +3472,19 @@ var ManifestV2Schema = z2.object({
3428
3472
  spec: SpecSchema,
3429
3473
  status: StatusSchema.optional()
3430
3474
  }).strict();
3475
+ function parseManifestV2(input) {
3476
+ const result = ManifestV2Schema.safeParse(input);
3477
+ if (result.success) {
3478
+ return { kind: "ok", manifest: result.data };
3479
+ }
3480
+ return {
3481
+ kind: "schema-error",
3482
+ issues: result.error.issues.map((issue) => ({
3483
+ path: issue.path.map(String).join(".") || "(root)",
3484
+ message: issue.message
3485
+ }))
3486
+ };
3487
+ }
3431
3488
  // ../launchpad-engine/dist/secrets-audit.js
3432
3489
  function auditSecretPresence(bindings, surfaces) {
3433
3490
  const byId = new Map(surfaces.map((s) => [s.id, s]));
@@ -3547,298 +3604,569 @@ function deriveResources(m) {
3547
3604
  }
3548
3605
  return items;
3549
3606
  }
3550
- // ../launchpad-engine/dist/wrangler.js
3551
- var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
3552
- var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
3553
- function generateWranglerToml(manifest) {
3554
- if (manifest.deployment.type !== "container") {
3607
+ // ../launchpad-engine/dist/container-runtime-capability.js
3608
+ var MAX_PATH_BYTES = 256;
3609
+ var MAX_ENV_NAMES = 64;
3610
+ var MAX_ENV_NAME_BYTES = 64;
3611
+ var CONTAINER_NAME = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3612
+ var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
3613
+ var IMAGE_SEGMENT = /^[A-Za-z0-9._-]+$/;
3614
+ var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
3615
+ var RESERVED_ENV_PREFIXES = ["LAUNCHPAD_", "CF_ACCESS_", "CLOUDFLARE_"];
3616
+ var RESERVED_ENV_NAMES = new Set([
3617
+ "BINDINGS_PROXY_SECRET",
3618
+ "BINDINGS_PROXY_URL"
3619
+ ]);
3620
+ function validateDeploymentCapability(manifest) {
3621
+ const view = deploymentView(manifest);
3622
+ if (view.type !== "container") {
3555
3623
  return {
3556
- kind: "not-applicable",
3557
- reason: `deployment.type '${manifest.deployment.type}' does not use wrangler.toml`
3624
+ kind: "supported",
3625
+ manifestValid: true,
3626
+ runtimeSupported: true,
3627
+ runtime: "pages"
3558
3628
  };
3559
3629
  }
3560
- const containers = manifest.containers ?? [];
3561
- if (containers.length === 0) {
3630
+ if (view.containers.length !== 1) {
3631
+ return unsupported("unsupported_container_topology", "containers", `the container runtime supports exactly one container; found ${view.containers.length}`);
3632
+ }
3633
+ const container = view.containers[0];
3634
+ if (byteLength(container.name) > 32 || container.name.length < 2 || !CONTAINER_NAME.test(container.name)) {
3635
+ return unsupported("invalid_container_name", "containers.0.name", "container name must be a 2-32 character lowercase slug");
3636
+ }
3637
+ if (container.port < 1024 || container.port > 65535) {
3638
+ return unsupported("unsupported_container_port", "containers.0.port", "container port must be between 1024 and 65535");
3639
+ }
3640
+ if (!validImagePath(container.image)) {
3641
+ return unsupported("invalid_container_image", "containers.0.image", "container image must be a contained repository-relative Dockerfile path beginning ./");
3642
+ }
3643
+ if (!validHealthcheck(container.healthcheck)) {
3644
+ return unsupported("invalid_container_healthcheck", "containers.0.healthcheck", "container healthcheck must be a simple absolute path without traversal, query, or fragment");
3645
+ }
3646
+ const environmentNames = container.env ?? [];
3647
+ if (environmentNames.length > MAX_ENV_NAMES) {
3648
+ return unsupported("too_many_container_environment_names", "containers.0.env", `container env may contain at most ${MAX_ENV_NAMES} names`);
3649
+ }
3650
+ const seen = new Set;
3651
+ const bindingName = containerBindingName(container.name);
3652
+ for (let index = 0;index < environmentNames.length; index += 1) {
3653
+ const name = environmentNames[index];
3654
+ const path7 = `containers.0.env.${index}`;
3655
+ if (byteLength(name) > MAX_ENV_NAME_BYTES || !ENV_NAME.test(name)) {
3656
+ return unsupported("invalid_container_environment_name", path7, "container environment names must be at most 64 bytes and use UPPER_SNAKE_CASE");
3657
+ }
3658
+ if (seen.has(name)) {
3659
+ return unsupported("duplicate_container_environment_name", path7, `container environment name '${name}' is duplicated`);
3660
+ }
3661
+ seen.add(name);
3662
+ if (RESERVED_ENV_NAMES.has(name) || RESERVED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)) || name === bindingName) {
3663
+ return unsupported("reserved_container_environment_name", path7, `container environment name '${name}' is owned by the Launchpad runtime`);
3664
+ }
3665
+ }
3666
+ return {
3667
+ kind: "supported",
3668
+ manifestValid: true,
3669
+ runtimeSupported: true,
3670
+ runtime: "container",
3671
+ capability: {
3672
+ appName: view.appName,
3673
+ apiVersion: manifest.apiVersion,
3674
+ container,
3675
+ className: containerClassName(view.appName),
3676
+ bindingName,
3677
+ dockerfilePathFromContainerDirectory: `../${container.image.slice(2)}`,
3678
+ environmentNames
3679
+ }
3680
+ };
3681
+ }
3682
+ function containerClassName(containerName) {
3683
+ return `AppContainer${containerName.split("-").map(capitalise).join("")}`;
3684
+ }
3685
+ function containerBindingName(containerName) {
3686
+ return `APP_CONTAINER_${containerName.split("-").map((part) => part.toUpperCase()).join("_")}`;
3687
+ }
3688
+ function deploymentView(manifest) {
3689
+ if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
3562
3690
  return {
3563
- kind: "ok",
3564
- content: emit(manifest, []),
3565
- warnings: ["container manifest has no containers[] — emitting empty TOML"]
3691
+ type: manifest.spec.appType,
3692
+ appName: manifest.metadata.slug,
3693
+ containers: manifest.spec.containers ?? []
3566
3694
  };
3567
3695
  }
3568
- return { kind: "ok", content: emit(manifest, containers), warnings: [] };
3696
+ return {
3697
+ type: manifest.deployment.type,
3698
+ appName: manifest.metadata.name,
3699
+ containers: manifest.containers ?? []
3700
+ };
3569
3701
  }
3570
- function emit(manifest, containers) {
3571
- const lines = [];
3572
- const header = `# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.
3573
- # Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})
3574
- # App: ${manifest.metadata.name}
3575
- #
3576
- # Regenerate with \`launchpad generate\` (\`launchpad deploy\` also
3577
- # re-emits before applying). Manual edits are overwritten on the
3578
- # next regeneration. Manifest is the source of truth — change
3579
- # ${manifest.metadata.name}/launchpad.yaml instead.`;
3580
- lines.push(header);
3581
- lines.push("");
3582
- lines.push(`name = "${escapeTomlString(manifest.metadata.name)}"`);
3583
- lines.push(`main = "worker/index.ts"`);
3584
- lines.push(`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`);
3585
- lines.push(`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((f) => `"${escapeTomlString(f)}"`).join(", ")}]`);
3586
- lines.push("");
3587
- lines.push("# Workers Logs (PATTERNS.md: non-optional for container apps).");
3588
- lines.push("[observability]");
3589
- lines.push("enabled = true");
3590
- lines.push("head_sampling_rate = 1.0");
3591
- lines.push("");
3592
- lines.push("# Platform-managed runtime config. Populated by the deploy");
3593
- lines.push("# workflow via `wrangler deploy --var KEY:VALUE`. Secrets");
3594
- lines.push("# (declared in launchpad.yaml secrets.bindings[]) go through");
3595
- lines.push("# `wrangler secret put` and are NOT here.");
3596
- lines.push("[vars]");
3597
- lines.push(`CF_ACCESS_AUD = ""`);
3598
- lines.push(`CF_ACCESS_TEAM_DOMAIN = ""`);
3599
- lines.push("");
3600
- for (const c of containers) {
3601
- const className = containerClassName(c.name);
3602
- const maxInstances = c.max_instances ?? 1;
3603
- lines.push("[[containers]]");
3604
- lines.push(`class_name = "${className}"`);
3605
- lines.push(`image = "${escapeTomlString(c.image)}"`);
3606
- lines.push(`max_instances = ${maxInstances}`);
3607
- lines.push("");
3702
+ function validImagePath(path7) {
3703
+ if (byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("./") || path7.includes("\\") || CONTROL_CHARACTER.test(path7)) {
3704
+ return false;
3608
3705
  }
3609
- for (const c of containers) {
3610
- const className = containerClassName(c.name);
3611
- const bindingName = containerBindingName(c.name);
3612
- lines.push("[[durable_objects.bindings]]");
3613
- lines.push(`name = "${bindingName}"`);
3614
- lines.push(`class_name = "${className}"`);
3615
- lines.push("");
3706
+ const segments = path7.slice(2).split("/");
3707
+ return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== ".." && IMAGE_SEGMENT.test(segment));
3708
+ }
3709
+ function validHealthcheck(path7) {
3710
+ if (byteLength(path7) === 0 || byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("/") || path7.includes("\\") || path7.includes("?") || path7.includes("#") || CONTROL_CHARACTER.test(path7)) {
3711
+ return false;
3616
3712
  }
3617
- lines.push("# Fresh-deploy migration. Container-enabled DOs MUST be");
3618
- lines.push("# SQLite-backed (ADR 0015 #4/#5 — non-sqlite fails [code: 10074]).");
3619
- lines.push("# Re-shaping containers on a live app needs a v2 migration");
3620
- lines.push("# block; the engine's TF-state-aware migration generator lands");
3621
- lines.push("# with Scope 8 drift detection.");
3622
- lines.push("[[migrations]]");
3623
- lines.push(`tag = "v1"`);
3624
- const classNames = containers.map((c) => `"${containerClassName(c.name)}"`);
3625
- lines.push(`new_sqlite_classes = [${classNames.join(", ")}]`);
3626
- lines.push("");
3713
+ const segments = path7.slice(1).split("/");
3714
+ return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
3715
+ }
3716
+ function byteLength(value) {
3717
+ return new TextEncoder().encode(value).byteLength;
3718
+ }
3719
+ function capitalise(value) {
3720
+ if (value.length === 0)
3721
+ return value;
3722
+ return value[0].toUpperCase() + value.slice(1);
3723
+ }
3724
+ function unsupported(reason, path7, message) {
3725
+ return {
3726
+ kind: "unsupported",
3727
+ manifestValid: true,
3728
+ runtimeSupported: false,
3729
+ reason,
3730
+ path: path7,
3731
+ message
3732
+ };
3733
+ }
3734
+ // ../launchpad-engine/dist/container-worker.js
3735
+ function generateContainerWorker(manifest) {
3736
+ const result = validateDeploymentCapability(manifest);
3737
+ if (result.kind === "unsupported") {
3738
+ return {
3739
+ kind: "unsupported",
3740
+ reason: result.reason,
3741
+ message: result.message
3742
+ };
3743
+ }
3744
+ if (result.runtime !== "container") {
3745
+ return {
3746
+ kind: "not-applicable",
3747
+ reason: "pages runtimes do not use the generated container Worker"
3748
+ };
3749
+ }
3750
+ return { kind: "ok", content: emit(result.capability) };
3751
+ }
3752
+ function emit(capability) {
3753
+ const app = JSON.stringify(capability.appName);
3754
+ const className = capability.className;
3755
+ const bindingName = capability.bindingName;
3756
+ const environmentFields = capability.environmentNames.map((name) => ` ${name}: string;`);
3757
+ const environmentValues = capability.environmentNames.map((name) => ` ${name}: this.env.${name} ?? "",`);
3758
+ const lines = [
3759
+ "/**",
3760
+ " * Auto-generated by @m-kopa/launchpad-engine.",
3761
+ " * Source: launchpad.yaml. Do not edit by hand.",
3762
+ " */",
3763
+ 'import { Container } from "@cloudflare/containers";',
3764
+ "import {",
3765
+ " AccessJwtVerificationError,",
3766
+ " createJwksCache,",
3767
+ " parseAccessJwtFromHeaders,",
3768
+ " verifyAccessJwt,",
3769
+ " type User,",
3770
+ '} from "@m-kopa/platform-auth";',
3771
+ "",
3772
+ "type Env = {",
3773
+ ` ${bindingName}: DurableObjectNamespace<${className}>;`,
3774
+ " BINDINGS_PROXY_SECRET: string;",
3775
+ " CF_ACCESS_AUD: string;",
3776
+ " CF_ACCESS_TEAM_DOMAIN: string;",
3777
+ " LAUNCHPAD_DEPLOYMENT_SHA: string;",
3778
+ ...environmentFields,
3779
+ "};",
3780
+ "",
3781
+ `export class ${className} extends Container<Env> {`,
3782
+ ` defaultPort = ${capability.container.port};`,
3783
+ ' sleepAfter = "5m";',
3784
+ ...environmentValues.length === 0 ? [] : ["", " envVars = {", ...environmentValues, " };"],
3785
+ "}",
3786
+ "",
3787
+ "let jwksCache: ReturnType<typeof createJwksCache> | null = null;",
3788
+ "",
3789
+ "function jwks(env: Env): ReturnType<typeof createJwksCache> {",
3790
+ " if (jwksCache === null) {",
3791
+ " jwksCache = createJwksCache(",
3792
+ " `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
3793
+ " );",
3794
+ " }",
3795
+ " return jwksCache;",
3796
+ "}",
3797
+ "",
3798
+ "function unauthenticated(code: string): Response {",
3799
+ " return Response.json(",
3800
+ ' { error: "unauthenticated", code },',
3801
+ ' { status: 401, headers: { "cache-control": "no-store" } },',
3802
+ " );",
3803
+ "}",
3804
+ "",
3805
+ "async function secretsEqual(left: string, right: string): Promise<boolean> {",
3806
+ " const encoder = new TextEncoder();",
3807
+ " const [leftDigest, rightDigest] = await Promise.all([",
3808
+ ' crypto.subtle.digest("SHA-256", encoder.encode(left)),',
3809
+ ' crypto.subtle.digest("SHA-256", encoder.encode(right)),',
3810
+ " ]);",
3811
+ " const leftBytes = new Uint8Array(leftDigest);",
3812
+ " const rightBytes = new Uint8Array(rightDigest);",
3813
+ " let difference = 0;",
3814
+ " for (let index = 0; index < leftBytes.length; index += 1) {",
3815
+ " difference |= leftBytes[index]! ^ rightBytes[index]!;",
3816
+ " }",
3817
+ " return difference === 0;",
3818
+ "}",
3819
+ "",
3820
+ "async function authenticate(request: Request, env: Env): Promise<User | Response> {",
3821
+ " if (!env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
3822
+ ' return unauthenticated("auth_unconfigured");',
3823
+ " }",
3824
+ " const token = parseAccessJwtFromHeaders(request.headers);",
3825
+ " if (token === null) {",
3826
+ ' return unauthenticated("access_jwt_missing");',
3827
+ " }",
3828
+ " try {",
3829
+ " return await verifyAccessJwt(token, {",
3830
+ " teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
3831
+ " audience: env.CF_ACCESS_AUD,",
3832
+ " jwks: jwks(env),",
3833
+ " clockTolerance: 30,",
3834
+ " });",
3835
+ " } catch (error) {",
3836
+ " if (error instanceof AccessJwtVerificationError) {",
3837
+ " return unauthenticated(error.code);",
3838
+ " }",
3839
+ ' console.error(JSON.stringify({ event: "container_runtime_error", kind: "access_jwt_verification_failed" }));',
3840
+ ' return unauthenticated("access_jwt_verification_failed");',
3841
+ " }",
3842
+ "}",
3843
+ "",
3844
+ "function forwardedRequest(",
3845
+ " request: Request,",
3846
+ " user: User,",
3847
+ " env: Env,",
3848
+ " requestId: string,",
3849
+ "): Request {",
3850
+ " const url = new URL(request.url);",
3851
+ " const headers = new Headers(request.headers);",
3852
+ " for (const name of Array.from(headers.keys())) {",
3853
+ " const lower = name.toLowerCase();",
3854
+ " if (",
3855
+ ' lower.startsWith("cf-access-") ||',
3856
+ ' lower.startsWith("x-user-") ||',
3857
+ ' lower.startsWith("x-bindings-proxy-") ||',
3858
+ ' lower === "x-request-id"',
3859
+ " ) {",
3860
+ " headers.delete(name);",
3861
+ " }",
3862
+ " }",
3863
+ ' headers.set("X-User-Sub", user.sub);',
3864
+ ' headers.set("X-User-Email", user.preferredUsername);',
3865
+ " if (user.groups.length > 0) {",
3866
+ ' headers.set("X-User-Groups", user.groups.join(","));',
3867
+ " }",
3868
+ ' headers.set("X-Request-ID", requestId);',
3869
+ ' headers.set("X-Bindings-Proxy-Secret", env.BINDINGS_PROXY_SECRET);',
3870
+ ' headers.set("X-Bindings-Proxy-Url", `${url.origin}/__platform/bindings`);',
3871
+ " return new Request(url.toString(), {",
3872
+ " method: request.method,",
3873
+ " headers,",
3874
+ ' body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,',
3875
+ " redirect: request.redirect,",
3876
+ " });",
3877
+ "}",
3878
+ "",
3879
+ "function runtimeEvent(",
3880
+ " env: Env,",
3881
+ " requestId: string,",
3882
+ " status: number,",
3883
+ " kind: string,",
3884
+ "): void {",
3885
+ " console.log(JSON.stringify({",
3886
+ ' event: "container_runtime_dispatch",',
3887
+ ` slug: ${app},`,
3888
+ " deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
3889
+ " request_id: requestId,",
3890
+ ` class_name: ${JSON.stringify(className)},`,
3891
+ ` binding_name: ${JSON.stringify(bindingName)},`,
3892
+ " status,",
3893
+ " kind,",
3894
+ " }));",
3895
+ "}",
3896
+ "",
3897
+ "export default {",
3898
+ " async fetch(request: Request, env: Env): Promise<Response> {",
3899
+ " const url = new URL(request.url);",
3900
+ " const requestId = crypto.randomUUID();",
3901
+ ' if (request.method === "GET" && url.pathname === "/health") {',
3902
+ ' const response = Response.json({ ok: true, role: "worker" });',
3903
+ ' runtimeEvent(env, requestId, response.status, "worker_health");',
3904
+ " return response;",
3905
+ " }",
3906
+ ' if (url.pathname === "/__platform/bindings" || url.pathname.startsWith("/__platform/bindings/")) {',
3907
+ ' const presented = request.headers.get("x-bindings-proxy-secret");',
3908
+ " if (!env.BINDINGS_PROXY_SECRET || !presented || !(await secretsEqual(presented, env.BINDINGS_PROXY_SECRET))) {",
3909
+ ' const response = Response.json({ error: "forbidden" }, { status: 403 });',
3910
+ ' runtimeEvent(env, requestId, response.status, "bindings_forbidden");',
3911
+ " return response;",
3912
+ " }",
3913
+ ' const response = Response.json({ error: "unknown_binding_endpoint" }, { status: 404 });',
3914
+ ' runtimeEvent(env, requestId, response.status, "bindings_unknown");',
3915
+ " return response;",
3916
+ " }",
3917
+ " const identity = await authenticate(request, env);",
3918
+ " if (identity instanceof Response) {",
3919
+ ' runtimeEvent(env, requestId, identity.status, "authentication_failed");',
3920
+ " return identity;",
3921
+ " }",
3922
+ ` const id = env.${bindingName}.idFromName("singleton");`,
3923
+ ` const response = await env.${bindingName}.get(id).fetch(`,
3924
+ " forwardedRequest(request, identity, env, requestId),",
3925
+ " );",
3926
+ ' runtimeEvent(env, requestId, response.status, "container_response");',
3927
+ " return response;",
3928
+ " },",
3929
+ "};",
3930
+ ""
3931
+ ];
3627
3932
  return lines.join(`
3628
3933
  `);
3629
3934
  }
3630
- function containerClassName(manifestName) {
3631
- return "AppContainer" + manifestName.split("-").map(capitalise).join("");
3632
- }
3633
- function containerBindingName(manifestName) {
3634
- return "APP_CONTAINER_" + manifestName.split("-").map((p) => p.toUpperCase()).join("_");
3935
+ // ../launchpad-engine/dist/wrangler.js
3936
+ var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
3937
+ var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
3938
+ function generateWranglerToml(manifest) {
3939
+ const result = validateDeploymentCapability(manifest);
3940
+ if (result.kind === "unsupported") {
3941
+ return {
3942
+ kind: "unsupported",
3943
+ reason: result.reason,
3944
+ message: result.message
3945
+ };
3946
+ }
3947
+ if (result.runtime !== "container") {
3948
+ return {
3949
+ kind: "not-applicable",
3950
+ reason: "pages runtimes do not use container/wrangler.toml"
3951
+ };
3952
+ }
3953
+ return { kind: "ok", content: emit2(result.capability), warnings: [] };
3635
3954
  }
3636
- function capitalise(s) {
3637
- if (s.length === 0)
3638
- return s;
3639
- return s[0].toUpperCase() + s.slice(1);
3955
+ function emit2(capability) {
3956
+ const maxInstances = capability.container.max_instances ?? 1;
3957
+ return [
3958
+ "# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
3959
+ `# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
3960
+ `# App: ${capability.appName}`,
3961
+ "",
3962
+ `name = "${escapeTomlString(capability.appName)}"`,
3963
+ 'main = "worker/index.ts"',
3964
+ `compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`,
3965
+ `compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => `"${flag}"`).join(", ")}]`,
3966
+ "",
3967
+ "[observability]",
3968
+ "enabled = true",
3969
+ "head_sampling_rate = 1.0",
3970
+ "",
3971
+ "[vars]",
3972
+ 'CF_ACCESS_AUD = ""',
3973
+ 'CF_ACCESS_TEAM_DOMAIN = ""',
3974
+ 'LAUNCHPAD_DEPLOYMENT_SHA = ""',
3975
+ "",
3976
+ "[[containers]]",
3977
+ `class_name = "${capability.className}"`,
3978
+ `image = "${escapeTomlString(capability.dockerfilePathFromContainerDirectory)}"`,
3979
+ `max_instances = ${maxInstances}`,
3980
+ "",
3981
+ "[[durable_objects.bindings]]",
3982
+ `name = "${capability.bindingName}"`,
3983
+ `class_name = "${capability.className}"`,
3984
+ "",
3985
+ "[[migrations]]",
3986
+ 'tag = "v1"',
3987
+ `new_sqlite_classes = ["${capability.className}"]`,
3988
+ ""
3989
+ ].join(`
3990
+ `);
3640
3991
  }
3641
- function escapeTomlString(s) {
3642
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
3992
+ function escapeTomlString(value) {
3993
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
3643
3994
  }
3644
3995
  // ../launchpad-engine/dist/github-workflow.js
3645
- var DEFAULT_WORKERS_SUBDOMAIN = "mkopa-launchpad.workers.dev";
3646
- var DEFAULT_BUN_VERSION = "1.3.4";
3996
+ var DEFAULT_NODE_VERSION = "22";
3647
3997
  var ACTIONS_CHECKOUT_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
3648
- var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
3998
+ var ACTIONS_SETUP_NODE_SHA = "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
3649
3999
  function generateGithubDeployWorkflow(manifest) {
3650
- if (manifest.deployment.type !== "container") {
4000
+ const result = validateDeploymentCapability(manifest);
4001
+ if (result.kind === "unsupported") {
3651
4002
  return {
3652
- kind: "not-applicable",
3653
- reason: `deployment.type '${manifest.deployment.type}' has no engine-generated deploy workflow yet`
4003
+ kind: "unsupported",
4004
+ reason: result.reason,
4005
+ message: result.message
3654
4006
  };
3655
4007
  }
3656
- const bindings = manifest.secrets?.bindings ?? [];
3657
- const warnings = [];
3658
- if ((manifest.containers ?? []).length === 0) {
3659
- warnings.push("container manifest has no containers[] — workflow will deploy an empty Worker");
4008
+ if (result.runtime !== "container") {
4009
+ return {
4010
+ kind: "not-applicable",
4011
+ reason: "pages runtimes do not use the container deploy workflow"
4012
+ };
3660
4013
  }
3661
- return {
3662
- kind: "ok",
3663
- content: emit2(manifest, bindings),
3664
- warnings
3665
- };
4014
+ return { kind: "ok", content: emit3(result.capability), warnings: [] };
3666
4015
  }
3667
- function emit2(manifest, bindings) {
3668
- const appName = manifest.metadata.name;
3669
- const lines = [];
3670
- lines.push(`# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.`);
3671
- lines.push(`# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})`);
3672
- lines.push(`# App: ${appName}`);
3673
- lines.push(`#`);
3674
- lines.push(`# Regenerate with \`launchpad generate\` (\`launchpad deploy\` also`);
3675
- lines.push(`# re-emits before applying). Manual edits are overwritten on the`);
3676
- lines.push(`# next regeneration. Manifest is the source of truth — change`);
3677
- lines.push(`# ${appName}/launchpad.yaml instead.`);
3678
- lines.push("");
3679
- lines.push(`name: deploy`);
3680
- lines.push("");
3681
- lines.push(`on:`);
3682
- lines.push(` push:`);
3683
- lines.push(` branches:`);
3684
- lines.push(` - main`);
3685
- lines.push(` workflow_dispatch:`);
3686
- lines.push("");
3687
- lines.push(`permissions:`);
3688
- lines.push(` contents: read`);
3689
- lines.push(` id-token: write`);
3690
- lines.push(` packages: read`);
3691
- lines.push("");
3692
- lines.push(`concurrency:`);
3693
- lines.push(` group: deploy-\${{ github.workflow }}-\${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3694
- lines.push(` cancel-in-progress: false`);
3695
- lines.push("");
3696
- lines.push(`defaults:`);
3697
- lines.push(` run:`);
3698
- lines.push(` # Container-shape apps keep wrangler.toml + Worker source +`);
3699
- lines.push(` # per-container Dockerfiles under container/. Engine convention.`);
3700
- lines.push(` working-directory: container`);
3701
- lines.push("");
3702
- lines.push(`jobs:`);
3703
- lines.push(` deploy:`);
3704
- lines.push(` name: deploy`);
3705
- lines.push(` runs-on: ubuntu-latest`);
3706
- lines.push(` environment: \${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3707
- lines.push(` steps:`);
3708
- lines.push(` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`);
3709
- lines.push("");
3710
- lines.push(` - uses: oven-sh/setup-bun@${SETUP_BUN_SHA} # v2`);
3711
- lines.push(` with:`);
3712
- lines.push(` bun-version: "${DEFAULT_BUN_VERSION}"`);
3713
- lines.push("");
3714
- lines.push(` - name: Install dependencies`);
3715
- lines.push(` env:`);
3716
- lines.push(` NODE_AUTH_TOKEN: \${{ secrets.NODE_AUTH_TOKEN }}`);
3717
- lines.push(` run: bun install --ignore-scripts`);
3718
- lines.push("");
3719
- lines.push(` - name: Deploy Worker + container images`);
3720
- lines.push(` # --var KEY:VALUE overrides the placeholder [vars] block in`);
3721
- lines.push(` # wrangler.toml. Missing either CF_ACCESS_* var trips the`);
3722
- lines.push(` # Worker's auth_unconfigured fail-closed branch on every`);
3723
- lines.push(` # request. Multi-container deploys build + push every`);
3724
- lines.push(` # [[containers]] block; total time scales linearly.`);
3725
- lines.push(` env:`);
3726
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3727
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3728
- lines.push(` run: |`);
3729
- lines.push(` bunx wrangler deploy \\`);
3730
- lines.push(` --var "CF_ACCESS_AUD:\${{ vars.CF_ACCESS_AUD }}" \\`);
3731
- lines.push(` --var "CF_ACCESS_TEAM_DOMAIN:\${{ vars.CF_ACCESS_TEAM_DOMAIN }}"`);
3732
- lines.push("");
3733
- for (const b of bindings) {
3734
- if (b.source !== "env-file")
3735
- continue;
3736
- emitSecretStep(lines, b);
3737
- }
3738
- const workerUrl = `https://${appName}.${DEFAULT_WORKERS_SUBDOMAIN}`;
3739
- lines.push(` - name: Smoke probe — Worker liveness (via .well-known bypass)`);
3740
- lines.push(` # In workers_dev_only mode CF Access intercepts every path`);
3741
- lines.push(` # (including /health) with HTTP 302 to the IdP. The only`);
3742
- lines.push(` # path that reaches the Worker without an Access JWT is`);
3743
- lines.push(` # /.well-known/openid-configuration, gated by a separate`);
3744
- lines.push(` # Bypass-policy Access app.`);
3745
- lines.push(` #`);
3746
- lines.push(` # Outcomes:`);
3747
- lines.push(` # 302 → bypass policy missing or misconfigured.`);
3748
- lines.push(` # 5xx → Worker is down (deploy bug).`);
3749
- lines.push(` # 401 + auth_unconfigured → CF_ACCESS_* vars didn't wire in.`);
3750
- lines.push(` # 401 + access_jwt_missing (any other 4xx) → HAPPY PATH.`);
3751
- lines.push(` run: |`);
3752
- lines.push(` set -euo pipefail`);
3753
- lines.push(` URL="${workerUrl}/.well-known/openid-configuration"`);
3754
- lines.push(` for i in 1 2 3 4 5; do`);
3755
- lines.push(` body_file="$(mktemp)"`);
3756
- lines.push(` code=$(curl -sS -o "$body_file" -w "%{http_code}" "$URL" || echo "000")`);
3757
- lines.push(` body=$(cat "$body_file")`);
3758
- lines.push(` rm -f "$body_file"`);
3759
- lines.push("");
3760
- lines.push(` if [ "$code" = "302" ]; then`);
3761
- lines.push(` echo "liveness-probe: bypass policy missing — Access redirected /.well-known/. Attempt $i."`);
3762
- lines.push(` sleep 5`);
3763
- lines.push(` continue`);
3764
- lines.push(` fi`);
3765
- lines.push(` if [ "\${code:0:1}" = "5" ] || [ "$code" = "000" ]; then`);
3766
- lines.push(` echo "liveness-probe: Worker down (got $code). Attempt $i."`);
3767
- lines.push(` sleep 5`);
3768
- lines.push(` continue`);
3769
- lines.push(` fi`);
3770
- lines.push(` if printf '%s' "$body" | grep -q "auth_unconfigured"; then`);
3771
- 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`);
3772
- lines.push(` echo "body: $body" >&2`);
3773
- lines.push(` exit 1`);
3774
- lines.push(` fi`);
3775
- lines.push(` echo "liveness-probe ok: Worker reachable (HTTP $code), env wired in (no auth_unconfigured)."`);
3776
- lines.push(` exit 0`);
3777
- lines.push(` done`);
3778
- lines.push(` echo "liveness-probe failed after 5 attempts" >&2`);
3779
- lines.push(` curl -sSI "$URL" >&2 || true`);
3780
- lines.push(` exit 1`);
3781
- lines.push("");
3782
- lines.push(` - name: Smoke probe — frontend route is auth-gated`);
3783
- lines.push(` # 302 or 401 prove unauthenticated traffic is blocked.`);
3784
- lines.push(` # HTTP 200 here would mean the SPA bundle is publicly`);
3785
- lines.push(` # readable — the regression we're catching.`);
3786
- lines.push(` run: |`);
3787
- lines.push(` set -euo pipefail`);
3788
- lines.push(` URL="${workerUrl}/"`);
3789
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3790
- lines.push(` case "$code" in`);
3791
- lines.push(` 302|401)`);
3792
- lines.push(` echo "frontend auth-gate ok: HTTP $code on unauthenticated request to /."`);
3793
- lines.push(` ;;`);
3794
- lines.push(` *)`);
3795
- lines.push(` echo "frontend auth-gate regression: expected 302 or 401 on /, got $code" >&2`);
3796
- lines.push(` curl -sSI "$URL" >&2 || true`);
3797
- lines.push(` exit 1`);
3798
- lines.push(` ;;`);
3799
- lines.push(` esac`);
3800
- lines.push("");
3801
- lines.push(` - name: Smoke probe — backend route is auth-gated`);
3802
- lines.push(` # Same logic as the frontend probe; confirms /api/* surface`);
3803
- lines.push(` # is gated. A 200 here would be the worse regression:`);
3804
- lines.push(` # backend data leaked unauthenticated.`);
3805
- lines.push(` run: |`);
3806
- lines.push(` set -euo pipefail`);
3807
- lines.push(` URL="${workerUrl}/api/health"`);
3808
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3809
- lines.push(` case "$code" in`);
3810
- lines.push(` 302|401)`);
3811
- lines.push(` echo "backend auth-gate ok: HTTP $code on unauthenticated request to /api/health."`);
3812
- lines.push(` ;;`);
3813
- lines.push(` *)`);
3814
- lines.push(` echo "backend auth-gate regression: expected 302 or 401 on /api/health, got $code" >&2`);
3815
- lines.push(` curl -sSI "$URL" >&2 || true`);
3816
- lines.push(` exit 1`);
3817
- lines.push(` ;;`);
3818
- lines.push(` esac`);
3819
- lines.push("");
4016
+ function emit3(capability) {
4017
+ const lines = [
4018
+ "# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
4019
+ `# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
4020
+ `# App: ${capability.appName}`,
4021
+ "",
4022
+ "name: deploy",
4023
+ "",
4024
+ "on:",
4025
+ " push:",
4026
+ " branches:",
4027
+ " - main",
4028
+ " workflow_dispatch:",
4029
+ "",
4030
+ "permissions:",
4031
+ " contents: read",
4032
+ "",
4033
+ "concurrency:",
4034
+ " group: deploy-${{ github.workflow }}-${{ github.sha }}",
4035
+ " cancel-in-progress: false",
4036
+ "",
4037
+ "jobs:",
4038
+ " authorize:",
4039
+ " name: authorize exact run attempt",
4040
+ " runs-on: ubuntu-latest",
4041
+ " steps:",
4042
+ ` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
4043
+ " with:",
4044
+ " persist-credentials: false",
4045
+ "",
4046
+ " - name: Verify protected runtime authorization",
4047
+ " env:",
4048
+ " GH_TOKEN: ${{ github.token }}",
4049
+ " run: |",
4050
+ " set -euo pipefail",
4051
+ " record=$(mktemp)",
4052
+ ` trap 'rm -f "$record"' EXIT`,
4053
+ ' api="${{ github.api_url }}/repos/${{ github.repository }}/contents/.launchpad/runtime-control/${GITHUB_SHA}.json?ref=launchpad-runtime-control"',
4054
+ " for attempt in $(seq 1 60); do",
4055
+ " if curl --fail --silent --show-error \\",
4056
+ ' -H "Authorization: Bearer ${GH_TOKEN}" \\',
4057
+ ' -H "Accept: application/vnd.github.raw+json" \\',
4058
+ ' -H "X-GitHub-Api-Version: 2022-11-28" \\',
4059
+ ' "$api" > "$record"; then',
4060
+ " break",
4061
+ " fi",
4062
+ ' if [ "$attempt" -eq 60 ]; then',
4063
+ ' echo "No exact-run authorization was published" >&2',
4064
+ " exit 1",
4065
+ " fi",
4066
+ " sleep 5",
4067
+ " done",
4068
+ " jq -e '",
4069
+ ' (.schema == "launchpad-runtime-control/v1") and',
4070
+ ' (.attempt_id | type == "string" and length > 0) and',
4071
+ ' (.lease_digest | test("^[0-9a-f]{64}$")) and',
4072
+ " (.commit_sha == env.GITHUB_SHA) and",
4073
+ " (.run_id == env.GITHUB_RUN_ID) and",
4074
+ " (.run_attempt == env.GITHUB_RUN_ATTEMPT) and",
4075
+ ' (.expires_at_epoch | type == "number" and . > now)',
4076
+ ` ' "$record" >/dev/null`,
4077
+ " wrangler_sha=$(sha256sum container/wrangler.toml | cut -d' ' -f1)",
4078
+ " worker_sha=$(sha256sum container/worker/index.ts | cut -d' ' -f1)",
4079
+ " workflow_sha=$(sha256sum .github/workflows/deploy.yml | cut -d' ' -f1)",
4080
+ " jq -e \\",
4081
+ ' --arg wrangler "$wrangler_sha" \\',
4082
+ ' --arg worker "$worker_sha" \\',
4083
+ ' --arg workflow "$workflow_sha" \\',
4084
+ ` '.artifact_digests["container/wrangler.toml"] == $wrangler and`,
4085
+ ' .artifact_digests["container/worker/index.ts"] == $worker and',
4086
+ ` .artifact_digests[".github/workflows/deploy.yml"] == $workflow' \\`,
4087
+ ' "$record" >/dev/null',
4088
+ "",
4089
+ " deploy:",
4090
+ " name: deploy admitted runtime",
4091
+ " needs: authorize",
4092
+ " runs-on: ubuntu-latest",
4093
+ " environment: ${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}",
4094
+ " steps:",
4095
+ ` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
4096
+ " with:",
4097
+ " persist-credentials: false",
4098
+ "",
4099
+ ` - uses: actions/setup-node@${ACTIONS_SETUP_NODE_SHA} # v6.4.0`,
4100
+ " with:",
4101
+ ` node-version: "${DEFAULT_NODE_VERSION}"`,
4102
+ " cache: npm",
4103
+ " cache-dependency-path: |",
4104
+ " container/package-lock.json",
4105
+ " container/backend/package-lock.json",
4106
+ "",
4107
+ " - name: Install frozen Worker dependencies",
4108
+ " working-directory: container",
4109
+ " run: npm ci --ignore-scripts",
4110
+ "",
4111
+ " - name: Install frozen workload dependencies",
4112
+ " working-directory: container/backend",
4113
+ " run: npm ci --ignore-scripts",
4114
+ "",
4115
+ " - name: Verify required deployment inputs",
4116
+ " env:",
4117
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4118
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4119
+ " CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
4120
+ " CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
4121
+ " BINDINGS_PROXY_SECRET: ${{ secrets.BINDINGS_PROXY_SECRET }}",
4122
+ " CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
4123
+ " CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
4124
+ ...capability.environmentNames.map((name) => ` ${name}: \${{ secrets.${name} }}`),
4125
+ " run: |",
4126
+ " set -euo pipefail",
4127
+ " for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID BINDINGS_PROXY_SECRET CF_ACCESS_AUD CF_ACCESS_TEAM_DOMAIN; do",
4128
+ ' if [ -z "${!name:-}" ]; then',
4129
+ ' echo "Required deployment input $name is empty" >&2',
4130
+ " exit 1",
4131
+ " fi",
4132
+ " done",
4133
+ ...capability.environmentNames.flatMap((name) => [
4134
+ ` if [ -z "\${${name}:-}" ]; then`,
4135
+ ` echo "Required container secret ${name} is empty" >&2`,
4136
+ " exit 1",
4137
+ " fi"
4138
+ ]),
4139
+ "",
4140
+ " - name: Deploy Worker and container image",
4141
+ " working-directory: container",
4142
+ " env:",
4143
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4144
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4145
+ " run: |",
4146
+ " npx --no-install wrangler deploy \\",
4147
+ ' --message "launchpad:${GITHUB_SHA}" \\',
4148
+ ' --tag "launchpad-deployment-sha:${GITHUB_SHA}" \\',
4149
+ ' --var "CF_ACCESS_AUD:${CF_ACCESS_AUD}" \\',
4150
+ ' --var "CF_ACCESS_TEAM_DOMAIN:${CF_ACCESS_TEAM_DOMAIN}" \\',
4151
+ ' --var "LAUNCHPAD_DEPLOYMENT_SHA:${GITHUB_SHA}"',
4152
+ "",
4153
+ ...secretSteps(capability)
4154
+ ];
3820
4155
  return lines.join(`
3821
4156
  `);
3822
4157
  }
3823
- function emitSecretStep(lines, binding) {
3824
- const name = binding.name;
3825
- const sourceNote = `env-file binding from launchpad.yaml — operator sets via \`launchpad secrets push\` or \`gh secret set\`.`;
3826
- lines.push(` - name: Push ${name} to the Worker`);
3827
- lines.push(` # ${sourceNote}`);
3828
- lines.push(` # Gated on the GH Actions secret existing so the first deploy`);
3829
- lines.push(` # on a fresh repo doesn't fail before secrets are provisioned.`);
3830
- lines.push(` # Skipping logs a ::warning:: so the gap shows in the deploy log.`);
3831
- lines.push(` env:`);
3832
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3833
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3834
- lines.push(` ${name}: \${{ secrets.${name} }}`);
3835
- lines.push(` run: |`);
3836
- lines.push(` if [ -z "\${${name}:-}" ]; then`);
3837
- 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."`);
3838
- lines.push(` exit 0`);
3839
- lines.push(` fi`);
3840
- lines.push(` printf '%s' "$${name}" | bunx wrangler secret put ${name}`);
3841
- lines.push("");
4158
+ function secretSteps(capability) {
4159
+ return ["BINDINGS_PROXY_SECRET", ...capability.environmentNames].flatMap((name) => [
4160
+ ` - name: Push ${name}`,
4161
+ " working-directory: container",
4162
+ " env:",
4163
+ " CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
4164
+ " CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
4165
+ ` ${name}: \${{ secrets.${name} }}`,
4166
+ " run: |",
4167
+ ` printf '%s' "$${name}" | npx --no-install wrangler secret put ${name}`,
4168
+ ""
4169
+ ]);
3842
4170
  }
3843
4171
  // ../launchpad-engine/dist/per-app-workspace.js
3844
4172
  var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
@@ -4190,6 +4518,21 @@ async function bundleAndDeploy(args) {
4190
4518
  import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
4191
4519
  import { join as join9 } from "node:path";
4192
4520
 
4521
+ // src/deploy/redact-diagnostic.ts
4522
+ var MAX_DIAGNOSTIC_LEN = 600;
4523
+ 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;
4524
+ var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
4525
+ function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
4526
+ if (text === undefined || text === null)
4527
+ return "";
4528
+ let t = text.replace(SECRET_KV, (_match, quote, key, separator) => `${quote}${key}${quote}${separator}***`);
4529
+ t = t.replace(LONG_TOKEN, "***");
4530
+ t = t.trim();
4531
+ if (t.length > maxLen)
4532
+ t = `${t.slice(0, maxLen - 1)}…`;
4533
+ return t;
4534
+ }
4535
+
4193
4536
  // src/deploy/deployment-status.ts
4194
4537
  async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
4195
4538
  let raw;
@@ -4218,11 +4561,39 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4218
4561
  return {
4219
4562
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4220
4563
  supported: raw.supported,
4221
- liveDeployment: raw.liveDeployment ?? null,
4222
- lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null,
4564
+ liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4565
+ lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4223
4566
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4224
4567
  };
4225
4568
  }
4569
+ function projectLastSuccessful(value) {
4570
+ if (value === null || typeof value !== "object")
4571
+ return null;
4572
+ const raw = value;
4573
+ if (typeof raw.id !== "string" || typeof raw.createdOn !== "string") {
4574
+ return null;
4575
+ }
4576
+ return {
4577
+ id: raw.id,
4578
+ createdOn: raw.createdOn,
4579
+ url: typeof raw.url === "string" ? raw.url : null
4580
+ };
4581
+ }
4582
+ function scrubLiveDeployment(deployment) {
4583
+ if (deployment === null)
4584
+ return null;
4585
+ return {
4586
+ ...deployment,
4587
+ ...deployment.commit === undefined ? {} : {
4588
+ commit: {
4589
+ ...deployment.commit,
4590
+ message: redactDiagnostic(deployment.commit.message)
4591
+ }
4592
+ },
4593
+ ...deployment.failedStage === undefined ? {} : { failedStage: redactDiagnostic(deployment.failedStage) },
4594
+ ...deployment.logExcerpt === undefined ? {} : { logExcerpt: redactDiagnostic(deployment.logExcerpt) }
4595
+ };
4596
+ }
4226
4597
 
4227
4598
  // src/deploy/commit-match-settle.ts
4228
4599
  var DEFAULT_REGISTRATION_BOUND_MS = 120000;
@@ -4388,21 +4759,6 @@ async function verifyServed(args, deps = { fetch }) {
4388
4759
  };
4389
4760
  }
4390
4761
 
4391
- // src/deploy/redact-diagnostic.ts
4392
- var MAX_DIAGNOSTIC_LEN = 600;
4393
- 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;
4394
- var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
4395
- function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
4396
- if (text === undefined || text === null)
4397
- return "";
4398
- let t = text.replace(SECRET_KV, (_m, key, sep2) => `${key}${sep2}***`);
4399
- t = t.replace(LONG_TOKEN, "***");
4400
- t = t.trim();
4401
- if (t.length > maxLen)
4402
- t = `${t.slice(0, maxLen - 1)}…`;
4403
- return t;
4404
- }
4405
-
4406
4762
  // src/commands/status.ts
4407
4763
  import { readFileSync as readFileSync7 } from "node:fs";
4408
4764
 
@@ -5263,7 +5619,29 @@ async function runStatus(args, io) {
5263
5619
  return mapBotError(e, parsed.slug, io);
5264
5620
  }
5265
5621
  if (lifecycle !== null && lifecycle.state !== "live") {
5266
- emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
5622
+ let output = lifecycleOutput(parsed.slug, lifecycle);
5623
+ if (lifecycle.state === "failed") {
5624
+ try {
5625
+ const deployment2 = await fetchDeploymentStatus(cfg, parsed.slug);
5626
+ output = {
5627
+ ...output,
5628
+ deployment: deployment2,
5629
+ ...deployment2 === null ? {
5630
+ deploymentUnavailableReason: "the deployment-status endpoint is unavailable or returned an unsupported response"
5631
+ } : {}
5632
+ };
5633
+ } catch (e) {
5634
+ if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5635
+ return mapBotError(e, parsed.slug, io);
5636
+ }
5637
+ output = {
5638
+ ...output,
5639
+ deployment: null,
5640
+ deploymentUnavailableReason: redactDiagnostic(describe12(e))
5641
+ };
5642
+ }
5643
+ }
5644
+ emit4(output, parsed.json, io);
5267
5645
  return 0;
5268
5646
  }
5269
5647
  let localYaml = null;
@@ -5311,7 +5689,7 @@ async function runStatus(args, io) {
5311
5689
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5312
5690
  return mapBotError(e, parsed.slug, io);
5313
5691
  }
5314
- io.err(`launchpad status: live deployment state unavailable (${describe12(e)}) — ` + `the report below is from the platform manifest view only.`);
5692
+ io.err(`launchpad status: live deployment state unavailable (${redactDiagnostic(describe12(e))}) — ` + `the report below is from the platform manifest view only.`);
5315
5693
  }
5316
5694
  let standingExceptions = null;
5317
5695
  try {
@@ -5340,7 +5718,7 @@ async function runStatus(args, io) {
5340
5718
  ...deploymentKnown ? { deployment } : {},
5341
5719
  ...standingExceptions !== null ? { standingExceptions } : {}
5342
5720
  };
5343
- emit3(out, parsed.json, io);
5721
+ emit4(out, parsed.json, io);
5344
5722
  return 0;
5345
5723
  }
5346
5724
  if (local === null) {
@@ -5357,7 +5735,7 @@ async function runStatus(args, io) {
5357
5735
  ...deploymentKnown ? { deployment } : {},
5358
5736
  ...standingExceptions !== null ? { standingExceptions } : {}
5359
5737
  };
5360
- emit3(out, parsed.json, io);
5738
+ emit4(out, parsed.json, io);
5361
5739
  if (parsed.strict) {
5362
5740
  if (deployment?.liveDeployment?.buildStatus === "failure") {
5363
5741
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
@@ -5440,7 +5818,7 @@ async function runStatus(args, io) {
5440
5818
  ...accessDrift !== undefined ? { accessDrift } : {},
5441
5819
  ...groupIssues.length === 0 ? {} : { groupIssues }
5442
5820
  };
5443
- emit3(result, parsed.json, io);
5821
+ emit4(result, parsed.json, io);
5444
5822
  if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
5445
5823
  return 1;
5446
5824
  }
@@ -5636,7 +6014,7 @@ function lifecycleOutput(slug, lc) {
5636
6014
  driftDetails: []
5637
6015
  };
5638
6016
  }
5639
- function emit3(out, asJson, io) {
6017
+ function emit4(out, asJson, io) {
5640
6018
  if (asJson) {
5641
6019
  io.out(JSON.stringify(out, null, 2));
5642
6020
  return;
@@ -5648,12 +6026,16 @@ function emit3(out, asJson, io) {
5648
6026
  return;
5649
6027
  case "provisioning_failed":
5650
6028
  io.out(`${out.slug}: provisioning FAILED at stage ${out.stage ?? "unknown"}` + (out.failedReason ? `: ${out.failedReason}` : ""));
5651
- if (isLikelyLiveFailureStage(out.stage)) {
6029
+ if (out.deployment?.reconciledToLive === true) {
6030
+ io.out(` that lifecycle state is stale: this read verified the live build and reconciled the record failed→live.`);
6031
+ io.out(` no action needed — re-run \`launchpad status ${out.slug}\` to see the reconciled state.`);
6032
+ } else if (isLikelyLiveFailureStage(out.stage)) {
5652
6033
  io.out(` the app may already be live — run \`launchpad recover ${out.slug}\` to reconcile the record;`);
5653
6034
  io.out(` if it is not serving, the same command starts the stage-dependent recovery.`);
5654
6035
  } else {
5655
6036
  io.out(` run \`launchpad recover ${out.slug}\` to start the stage-dependent recovery.`);
5656
6037
  }
6038
+ surfaceFailedLifecycleDeployment(out, io);
5657
6039
  return;
5658
6040
  case "destroying":
5659
6041
  io.out(`${out.slug}: destroying…`);
@@ -5750,6 +6132,20 @@ function triggerLabel(trigger) {
5750
6132
  return "launchpad deploy (bot)";
5751
6133
  return "an unknown mechanism";
5752
6134
  }
6135
+ function surfaceFailedLifecycleDeployment(out, io) {
6136
+ if (out.deploymentUnavailableReason !== undefined) {
6137
+ io.out(` latest deployment evidence unavailable: ${out.deploymentUnavailableReason}`);
6138
+ return;
6139
+ }
6140
+ if (out.deployment === undefined || out.deployment === null)
6141
+ return;
6142
+ if (!out.deployment.supported) {
6143
+ io.out(" latest deployment evidence: not applicable because this app has no Pages deployment surface.");
6144
+ return;
6145
+ }
6146
+ io.out(" latest deployment evidence (may not be the cause of this lifecycle failure):");
6147
+ surfaceDeployment(out, io);
6148
+ }
5753
6149
  function surfaceDeployment(out, io) {
5754
6150
  const dep = out.deployment;
5755
6151
  if (dep === undefined || dep === null || !dep.supported)
@@ -5766,15 +6162,15 @@ function surfaceDeployment(out, io) {
5766
6162
  const via = triggerLabel(live.trigger);
5767
6163
  switch (live.buildStatus) {
5768
6164
  case "success":
5769
- io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
6165
+ io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
5770
6166
  return;
5771
6167
  case "in_progress":
5772
- io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
6168
+ io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn}) [deployment ${live.id}]`);
5773
6169
  io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
5774
6170
  return;
5775
6171
  case "failure": {
5776
6172
  const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
5777
- io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
6173
+ io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
5778
6174
  if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
5779
6175
  io.out(" build log (excerpt):");
5780
6176
  for (const line of live.logExcerpt.split(`
@@ -6185,6 +6581,8 @@ import { readFileSync as readFileSync10 } from "node:fs";
6185
6581
 
6186
6582
  // src/deploy/git-files.ts
6187
6583
  import { spawn as spawn3 } from "node:child_process";
6584
+ import { existsSync as existsSync5 } from "node:fs";
6585
+ import { resolve as resolve7 } from "node:path";
6188
6586
 
6189
6587
  class GitFilesError extends Error {
6190
6588
  code = "git_files_error";
@@ -6202,6 +6600,7 @@ var PROTECTED_PATHS = new Set([
6202
6600
  ]);
6203
6601
  async function listDeployFiles(opts) {
6204
6602
  const sp = opts.spawner ?? spawn3;
6603
+ const exists = opts.exists ?? existsSync5;
6205
6604
  const stdout = await runGit2(sp, opts.cwd, [
6206
6605
  "ls-files",
6207
6606
  "-c",
@@ -6209,11 +6608,11 @@ async function listDeployFiles(opts) {
6209
6608
  "--exclude-standard",
6210
6609
  "-z"
6211
6610
  ]);
6212
- const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
6611
+ const paths = stdout.split("\x00").filter((p) => p.length > 0 && exists(resolve7(opts.cwd, p)) && !PROTECTED_PATHS.has(p));
6213
6612
  return [...paths].sort();
6214
6613
  }
6215
6614
  function runGit2(sp, cwd, args) {
6216
- return new Promise((resolve7, reject) => {
6615
+ return new Promise((resolve8, reject) => {
6217
6616
  const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
6218
6617
  const child = sp("git", [...args], spawnOpts);
6219
6618
  let stdout = "";
@@ -6229,7 +6628,7 @@ function runGit2(sp, cwd, args) {
6229
6628
  });
6230
6629
  child.once("close", (code) => {
6231
6630
  if (code === 0) {
6232
- resolve7(stdout);
6631
+ resolve8(stdout);
6233
6632
  return;
6234
6633
  }
6235
6634
  reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
@@ -6806,7 +7205,7 @@ function parseDeployFlags(args) {
6806
7205
  }
6807
7206
 
6808
7207
  // src/deploy/apply.ts
6809
- import { existsSync as existsSync5, rmSync } from "node:fs";
7208
+ import { existsSync as existsSync6, rmSync } from "node:fs";
6810
7209
  import { resolve as resolvePath, join as join10 } from "node:path";
6811
7210
 
6812
7211
  // src/manifest/load.ts
@@ -7173,7 +7572,7 @@ function sleep(ms) {
7173
7572
  }
7174
7573
  function deletePinIfPresent(cfg, slug, io) {
7175
7574
  const pinPath = join10(cfg.stateDir, slug, "group.json");
7176
- if (!existsSync5(pinPath))
7575
+ if (!existsSync6(pinPath))
7177
7576
  return;
7178
7577
  try {
7179
7578
  rmSync(pinPath, { force: true });
@@ -7311,12 +7710,12 @@ async function handleAccessDrift(params) {
7311
7710
 
7312
7711
  // src/deploy/dry-run.ts
7313
7712
  import { execFileSync } from "node:child_process";
7314
- import { resolve as resolve7 } from "node:path";
7713
+ import { resolve as resolve8 } from "node:path";
7315
7714
  var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
7316
7715
  async function runDeployDryRun(opts, io, deps = {}) {
7317
7716
  const cfg = deps.config ?? loadConfig();
7318
7717
  const fetcher = deps.fetcher ?? fetch;
7319
- const manifestPath = resolve7(process.cwd(), opts.file ?? "launchpad.yaml");
7718
+ const manifestPath = resolve8(process.cwd(), opts.file ?? "launchpad.yaml");
7320
7719
  const loaded = loadManifest(manifestPath);
7321
7720
  if (loaded.kind !== "ok") {
7322
7721
  return renderManifestError2(loaded, opts.json, io);
@@ -7674,6 +8073,81 @@ function handleNetworkError(e, io, slug, verb) {
7674
8073
  return 1;
7675
8074
  }
7676
8075
 
8076
+ // src/manifest/container-filesystem.ts
8077
+ import { lstatSync as lstatSync3, realpathSync } from "node:fs";
8078
+ import { dirname as dirname6, join as join11, relative as relative3, resolve as resolve9, sep as sep2 } from "node:path";
8079
+ function validateContainerFilesystem(manifestPath, capability) {
8080
+ const root = realpathSync(dirname6(manifestPath));
8081
+ const segments = capability.container.image.slice(2).split("/");
8082
+ let current = root;
8083
+ for (const segment of segments) {
8084
+ current = join11(current, segment);
8085
+ let stat;
8086
+ try {
8087
+ stat = lstatSync3(current);
8088
+ } catch (error) {
8089
+ return failure("container_image_unreadable", relative3(root, current), `cannot inspect container image path: ${describe15(error)}`);
8090
+ }
8091
+ if (stat.isSymbolicLink()) {
8092
+ return failure("container_image_symlink", relative3(root, current), "container image path must not contain symbolic links");
8093
+ }
8094
+ }
8095
+ let finalPath;
8096
+ try {
8097
+ finalPath = realpathSync(current);
8098
+ } catch (error) {
8099
+ return failure("container_image_unreadable", relative3(root, current), `cannot resolve container image path: ${describe15(error)}`);
8100
+ }
8101
+ const rootPrefix = resolve9(root) + sep2;
8102
+ if (!resolve9(finalPath).startsWith(rootPrefix)) {
8103
+ return failure("container_image_outside_repository", capability.container.image, "container image resolves outside the app repository");
8104
+ }
8105
+ if (!lstatSync3(finalPath).isFile()) {
8106
+ return failure("container_image_not_regular_file", capability.container.image, "container image must resolve to a regular Dockerfile");
8107
+ }
8108
+ return { kind: "ok", dockerfilePath: finalPath };
8109
+ }
8110
+ function failure(reason, path8, message) {
8111
+ return { kind: "error", reason, path: path8, message };
8112
+ }
8113
+ function describe15(error) {
8114
+ return error instanceof Error ? error.message : String(error);
8115
+ }
8116
+
8117
+ // src/manifest/runtime-capability.ts
8118
+ function validateCliRuntimeCapability(manifestPath, manifest) {
8119
+ const result = validateDeploymentCapability(manifest);
8120
+ if (result.kind === "unsupported" || result.runtime === "pages") {
8121
+ return result;
8122
+ }
8123
+ const filesystem = validateContainerFilesystem(manifestPath, result.capability);
8124
+ return filesystem.kind === "error" ? filesystem : result;
8125
+ }
8126
+ function rejectUnsupportedCliRuntime(args) {
8127
+ const result = validateCliRuntimeCapability(args.manifestPath, args.manifest);
8128
+ if (result.kind !== "unsupported" && result.kind !== "error")
8129
+ return null;
8130
+ if (args.json) {
8131
+ args.io.out(JSON.stringify({
8132
+ ok: false,
8133
+ manifest_valid: true,
8134
+ runtime_supported: false,
8135
+ reason: result.reason,
8136
+ path: result.path,
8137
+ message: result.message
8138
+ }));
8139
+ return 1;
8140
+ }
8141
+ if (args.structurallyValidName !== undefined) {
8142
+ args.io.out(`✓ ${args.structurallyValidName} - manifest is structurally valid`);
8143
+ args.io.err(`✗ runtime unsupported: ${result.reason}`);
8144
+ } else {
8145
+ args.io.err(`launchpad ${args.command}: runtime unsupported (${result.reason})`);
8146
+ }
8147
+ args.io.err(` ${result.path}: ${result.message}`);
8148
+ return 1;
8149
+ }
8150
+
7677
8151
  // src/commands/deploy.ts
7678
8152
  var deployCommand = {
7679
8153
  name: "deploy",
@@ -7726,7 +8200,7 @@ async function runDeploy(args, io) {
7726
8200
  }
7727
8201
  const cwd = process.cwd();
7728
8202
  const manifestPath = path8.join(cwd, "launchpad.yaml");
7729
- if (existsSync6(manifestPath)) {
8203
+ if (existsSync7(manifestPath)) {
7730
8204
  const contentMode = flags.mode.kind === "content" ? flags.mode : {
7731
8205
  allowStale: false,
7732
8206
  rebaseOntoHead: false,
@@ -7780,7 +8254,7 @@ async function runDeploy(args, io) {
7780
8254
  }
7781
8255
  let contentManifestYaml = null;
7782
8256
  const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
7783
- if (existsSync6(contentManifestPath)) {
8257
+ if (existsSync7(contentManifestPath)) {
7784
8258
  try {
7785
8259
  contentManifestYaml = readFileSync10(contentManifestPath, "utf8");
7786
8260
  } catch {
@@ -7860,7 +8334,7 @@ async function runDeploy(args, io) {
7860
8334
  io.err(`launchpad deploy: ${e.message}`);
7861
8335
  return 1;
7862
8336
  }
7863
- io.err(`launchpad deploy failed: ${describe15(e)}`);
8337
+ io.err(`launchpad deploy failed: ${describe16(e)}`);
7864
8338
  return 1;
7865
8339
  }
7866
8340
  }
@@ -7931,7 +8405,7 @@ function formatBytes2(n) {
7931
8405
  return `${(n / 1024).toFixed(1)}KB`;
7932
8406
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
7933
8407
  }
7934
- function describe15(e) {
8408
+ function describe16(e) {
7935
8409
  return e instanceof Error ? e.message : String(e);
7936
8410
  }
7937
8411
  function surfaceDeployExtras(body, io, slug, allowStale = false) {
@@ -8003,11 +8477,11 @@ function surfaceStaleBlock(body, io) {
8003
8477
  }
8004
8478
  var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
8005
8479
  function withTimeout(p, ms) {
8006
- return new Promise((resolve8, reject) => {
8480
+ return new Promise((resolve10, reject) => {
8007
8481
  const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
8008
8482
  p.then((v) => {
8009
8483
  clearTimeout(timer);
8010
- resolve8(v);
8484
+ resolve10(v);
8011
8485
  }, (e) => {
8012
8486
  clearTimeout(timer);
8013
8487
  reject(e instanceof Error ? e : new Error(String(e)));
@@ -8054,7 +8528,7 @@ async function advanceBaseShaMarker(cwd, sha, io) {
8054
8528
  return;
8055
8529
  await writeBaseShaMarker(cwd, sha);
8056
8530
  } catch (e) {
8057
- 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.`);
8531
+ io.err(`warning: couldn't update the local base-sha marker (${describe16(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
8058
8532
  }
8059
8533
  }
8060
8534
  async function runModelADeploy(args) {
@@ -8069,12 +8543,30 @@ async function runModelADeploy(args) {
8069
8543
  return 64;
8070
8544
  }
8071
8545
  slug = metaSlug;
8072
- const dtype = manifestObj?.deployment?.type;
8073
- if (dtype === "static" || dtype === "react" || dtype === "react+api" || dtype === "container") {
8074
- appType = dtype;
8546
+ const raw = manifestObj;
8547
+ const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
8548
+ const deploymentType = isV2 ? raw?.spec?.appType : raw?.deployment?.type;
8549
+ if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
8550
+ appType = deploymentType;
8551
+ }
8552
+ if (deploymentType === "container") {
8553
+ const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
8554
+ if (parsed.kind !== "ok") {
8555
+ io.err("launchpad deploy: manifest is structurally invalid:");
8556
+ for (const issue of parsed.issues) {
8557
+ io.err(` ${issue.path}: ${issue.message}`);
8558
+ }
8559
+ return 1;
8560
+ }
8561
+ const capability = validateCliRuntimeCapability(manifestPath, parsed.manifest);
8562
+ if (capability.kind === "unsupported" || capability.kind === "error") {
8563
+ io.err(`launchpad deploy: runtime unsupported (${capability.reason})`);
8564
+ io.err(` ${capability.path}: ${capability.message}`);
8565
+ return 1;
8566
+ }
8075
8567
  }
8076
8568
  } catch (e) {
8077
- io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe15(e)}`);
8569
+ io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe16(e)}`);
8078
8570
  return 1;
8079
8571
  }
8080
8572
  const noVerify = args.argv.includes("--no-verify");
@@ -8104,7 +8596,7 @@ async function runModelADeploy(args) {
8104
8596
  try {
8105
8597
  cfg = loadConfig();
8106
8598
  } catch (e) {
8107
- io.err(`launchpad deploy: ${describe15(e)}`);
8599
+ io.err(`launchpad deploy: ${describe16(e)}`);
8108
8600
  return 1;
8109
8601
  }
8110
8602
  try {
@@ -8154,7 +8646,7 @@ async function runModelADeploy(args) {
8154
8646
  io.err(" run `launchpad login` to refresh your session.");
8155
8647
  return 1;
8156
8648
  }
8157
- io.err(`launchpad deploy: unexpected error: ${describe15(e)}`);
8649
+ io.err(`launchpad deploy: unexpected error: ${describe16(e)}`);
8158
8650
  return 1;
8159
8651
  }
8160
8652
  switch (result.kind) {
@@ -8366,7 +8858,7 @@ function pendingReconcileOutcome(body) {
8366
8858
  }
8367
8859
 
8368
8860
  // src/commands/redeploy.ts
8369
- import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
8861
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
8370
8862
  import { resolve as resolvePath2 } from "node:path";
8371
8863
  import { parse as parseYaml7 } from "yaml";
8372
8864
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
@@ -8444,7 +8936,7 @@ async function runRedeploy(opts, io, deps = {}) {
8444
8936
  }
8445
8937
  function readLocalAppType(cwd, file) {
8446
8938
  const manifestPath = resolvePath2(cwd, file ?? "launchpad.yaml");
8447
- if (!existsSync7(manifestPath))
8939
+ if (!existsSync8(manifestPath))
8448
8940
  return null;
8449
8941
  try {
8450
8942
  const obj = parseYaml7(readFileSync11(manifestPath, "utf8"));
@@ -8629,7 +9121,7 @@ async function runEnvvars(args, io) {
8629
9121
  io.err(`launchpad envvars: ${e.message}`);
8630
9122
  return 1;
8631
9123
  }
8632
- io.err(`launchpad envvars failed: ${describe16(e)}`);
9124
+ io.err(`launchpad envvars failed: ${describe17(e)}`);
8633
9125
  return 1;
8634
9126
  }
8635
9127
  }
@@ -8726,16 +9218,16 @@ function renderList(envVars, io) {
8726
9218
  io.out(fmt(row));
8727
9219
  }
8728
9220
  }
8729
- function describe16(e) {
9221
+ function describe17(e) {
8730
9222
  return e instanceof Error ? e.message : String(e);
8731
9223
  }
8732
9224
 
8733
9225
  // src/commands/generate.ts
8734
9226
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
8735
- import { dirname as dirname6, resolve as resolve8, relative as relative3 } from "node:path";
9227
+ import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
8736
9228
  var generateCommand = {
8737
9229
  name: "generate",
8738
- summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
9230
+ summary: "emit derived runtime artefacts from launchpad.yaml",
8739
9231
  run: runGenerate
8740
9232
  };
8741
9233
  async function runGenerate(args, io) {
@@ -8745,33 +9237,50 @@ async function runGenerate(args, io) {
8745
9237
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
8746
9238
  return 64;
8747
9239
  }
8748
- const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
9240
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
8749
9241
  const result = loadManifest(manifestPath);
8750
9242
  if (result.kind !== "ok") {
8751
9243
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
8752
9244
  }
8753
- const appRoot = dirname6(manifestPath);
8754
- const wranglerPath = resolve8(appRoot, "container", "wrangler.toml");
8755
- const workflowPath = resolve8(appRoot, ".github", "workflows", "deploy.yml");
9245
+ const unsupported2 = rejectUnsupportedCliRuntime({
9246
+ command: "generate",
9247
+ manifestPath,
9248
+ manifest: result.manifest,
9249
+ io,
9250
+ json: flags.json
9251
+ });
9252
+ if (unsupported2 !== null)
9253
+ return unsupported2;
9254
+ const appRoot = dirname7(manifestPath);
9255
+ const wranglerPath = resolve10(appRoot, "container", "wrangler.toml");
9256
+ const workerPath = resolve10(appRoot, "container", "worker", "index.ts");
9257
+ const workflowPath = resolve10(appRoot, ".github", "workflows", "deploy.yml");
8756
9258
  const wranglerOut = generateWranglerToml(result.manifest);
9259
+ const workerRaw = generateContainerWorker(result.manifest);
9260
+ const workerOut = workerRaw.kind === "ok" ? { kind: "ok", content: workerRaw.content, warnings: [] } : workerRaw;
8757
9261
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
8758
9262
  if (flags.dryRun) {
8759
9263
  return renderDryRun([
8760
9264
  { artefact: "wrangler.toml", path: wranglerPath, result: wranglerOut },
9265
+ { artefact: "worker/index.ts", path: workerPath, result: workerOut },
8761
9266
  { artefact: "deploy.yml", path: workflowPath, result: workflowOut }
8762
9267
  ], appRoot, flags.json, io);
8763
9268
  }
8764
9269
  const reports = [
8765
9270
  applyOne("wrangler.toml", wranglerPath, wranglerOut, flags.force),
9271
+ applyOne("worker/index.ts", workerPath, workerOut, flags.force),
8766
9272
  applyOne("deploy.yml", workflowPath, workflowOut, flags.force)
8767
9273
  ];
8768
9274
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
8769
9275
  }
8770
9276
  function applyOne(artefact, path10, out, force) {
8771
- if (out.kind === "not-applicable") {
9277
+ if (out.kind === "not-applicable" || out.kind === "unsupported") {
8772
9278
  return {
8773
9279
  artefact,
8774
- action: { kind: "not-applicable", reason: out.reason },
9280
+ action: {
9281
+ kind: "not-applicable",
9282
+ reason: out.kind === "unsupported" ? `${out.reason}: ${out.message}` : out.reason
9283
+ },
8775
9284
  warnings: []
8776
9285
  };
8777
9286
  }
@@ -8794,7 +9303,7 @@ function applyOne(artefact, path10, out, force) {
8794
9303
  };
8795
9304
  }
8796
9305
  try {
8797
- mkdirSync2(dirname6(path10), { recursive: true });
9306
+ mkdirSync2(dirname7(path10), { recursive: true });
8798
9307
  writeFileSync2(path10, out.content, "utf8");
8799
9308
  return {
8800
9309
  artefact,
@@ -8830,12 +9339,12 @@ function readIfExists(path10) {
8830
9339
  function renderDryRun(inputs, appRoot, json, io) {
8831
9340
  if (json) {
8832
9341
  const files = inputs.map((i) => {
8833
- const previewPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
8834
- if (i.result.kind === "not-applicable") {
9342
+ const previewPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
9343
+ if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
8835
9344
  return {
8836
9345
  artefact: i.artefact,
8837
9346
  kind: "not-applicable",
8838
- reason: i.result.reason,
9347
+ reason: i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason,
8839
9348
  ...i.preview ? { preview: true, destination: i.preview.destination } : {}
8840
9349
  };
8841
9350
  }
@@ -8852,12 +9361,13 @@ function renderDryRun(inputs, appRoot, json, io) {
8852
9361
  return 0;
8853
9362
  }
8854
9363
  for (const i of inputs) {
8855
- if (i.result.kind === "not-applicable") {
8856
- io.out(`# ${i.artefact}: skipped — ${i.result.reason}`);
9364
+ if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
9365
+ const reason = i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason;
9366
+ io.out(`# ${i.artefact}: skipped - ${reason}`);
8857
9367
  io.out("");
8858
9368
  continue;
8859
9369
  }
8860
- const headerPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
9370
+ const headerPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
8861
9371
  const header = i.preview === undefined ? `--- ${headerPath} ---` : `--- [preview, destination = ${i.preview.destination}] ${headerPath} ---`;
8862
9372
  io.out(header);
8863
9373
  io.out(i.result.content);
@@ -8876,20 +9386,20 @@ function renderApplyHuman(reports, appRoot, io) {
8876
9386
  const a = r.action;
8877
9387
  switch (a.kind) {
8878
9388
  case "written":
8879
- io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative3(appRoot, a.path)}`);
9389
+ io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative4(appRoot, a.path)}`);
8880
9390
  break;
8881
9391
  case "unchanged":
8882
- io.out(`= ${r.artefact} — unchanged (${relative3(appRoot, a.path)})`);
9392
+ io.out(`= ${r.artefact} — unchanged (${relative4(appRoot, a.path)})`);
8883
9393
  break;
8884
9394
  case "would-overwrite":
8885
- io.err(`! ${r.artefact} — exists with different content (${relative3(appRoot, a.path)}); re-run with --force to overwrite`);
9395
+ io.err(`! ${r.artefact} — exists with different content (${relative4(appRoot, a.path)}); re-run with --force to overwrite`);
8886
9396
  wouldOverwrite++;
8887
9397
  break;
8888
9398
  case "not-applicable":
8889
9399
  io.out(`- ${r.artefact} — skipped (${a.reason})`);
8890
9400
  break;
8891
9401
  case "write-error":
8892
- io.err(`✗ ${r.artefact} — ${a.message} (${relative3(appRoot, a.path)})`);
9402
+ io.err(`✗ ${r.artefact} — ${a.message} (${relative4(appRoot, a.path)})`);
8893
9403
  writeError++;
8894
9404
  break;
8895
9405
  }
@@ -8908,17 +9418,17 @@ function renderApplyJson(reports, appRoot, io) {
8908
9418
  const base = { artefact: r.artefact, warnings: r.warnings };
8909
9419
  switch (a.kind) {
8910
9420
  case "written":
8911
- return { ...base, kind: a.kind, path: relative3(appRoot, a.path), bytes: a.bytes };
9421
+ return { ...base, kind: a.kind, path: relative4(appRoot, a.path), bytes: a.bytes };
8912
9422
  case "unchanged":
8913
9423
  case "would-overwrite":
8914
- return { ...base, kind: a.kind, path: relative3(appRoot, a.path) };
9424
+ return { ...base, kind: a.kind, path: relative4(appRoot, a.path) };
8915
9425
  case "not-applicable":
8916
9426
  return { ...base, kind: a.kind, reason: a.reason };
8917
9427
  case "write-error":
8918
9428
  return {
8919
9429
  ...base,
8920
9430
  kind: a.kind,
8921
- path: relative3(appRoot, a.path),
9431
+ path: relative4(appRoot, a.path),
8922
9432
  message: a.message
8923
9433
  };
8924
9434
  }
@@ -9042,13 +9552,13 @@ function decodeJwtPayload(token) {
9042
9552
  try {
9043
9553
  json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
9044
9554
  } catch (e) {
9045
- throw new JwtParseError(`could not base64-decode JWT payload: ${describe17(e)}`);
9555
+ throw new JwtParseError(`could not base64-decode JWT payload: ${describe18(e)}`);
9046
9556
  }
9047
9557
  let parsed;
9048
9558
  try {
9049
9559
  parsed = JSON.parse(json);
9050
9560
  } catch (e) {
9051
- throw new JwtParseError(`JWT payload is not JSON: ${describe17(e)}`);
9561
+ throw new JwtParseError(`JWT payload is not JSON: ${describe18(e)}`);
9052
9562
  }
9053
9563
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9054
9564
  throw new JwtParseError(`JWT payload is not an object`);
@@ -9059,7 +9569,7 @@ function b64UrlToB64(s) {
9059
9569
  const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
9060
9570
  return padded.replace(/-/g, "+").replace(/_/g, "/");
9061
9571
  }
9062
- function describe17(e) {
9572
+ function describe18(e) {
9063
9573
  return e instanceof Error ? e.message : String(e);
9064
9574
  }
9065
9575
 
@@ -9086,7 +9596,7 @@ async function runGroupsWhoami(args, io) {
9086
9596
  try {
9087
9597
  payload = decodeJwtPayload(session.accessToken);
9088
9598
  } catch (e) {
9089
- const message = e instanceof JwtParseError ? e.message : describe18(e);
9599
+ const message = e instanceof JwtParseError ? e.message : describe19(e);
9090
9600
  if (json) {
9091
9601
  io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
9092
9602
  } else {
@@ -9167,7 +9677,7 @@ function parseFlags2(args) {
9167
9677
  }
9168
9678
  return { kind: "ok", json };
9169
9679
  }
9170
- function describe18(e) {
9680
+ function describe19(e) {
9171
9681
  return e instanceof Error ? e.message : String(e);
9172
9682
  }
9173
9683
 
@@ -9270,7 +9780,7 @@ async function loadGroups(io, refresh, json) {
9270
9780
  }
9271
9781
  return { kind: "error", code: 3 };
9272
9782
  }
9273
- renderFetchError(io, describe19(e), json);
9783
+ renderFetchError(io, describe20(e), json);
9274
9784
  return { kind: "error", code: 2 };
9275
9785
  }
9276
9786
  }
@@ -9307,7 +9817,7 @@ async function lookupTenantWide(key) {
9307
9817
  return { kind: "unauthenticated", message: e.message };
9308
9818
  if (e instanceof ForbiddenError)
9309
9819
  return { kind: "forbidden", message: e.message };
9310
- return { kind: "error", message: describe19(e) };
9820
+ return { kind: "error", message: describe20(e) };
9311
9821
  }
9312
9822
  }
9313
9823
  function renderLookupFailure(io, json, hit) {
@@ -9453,9 +9963,9 @@ async function runShow(args, io) {
9453
9963
  }
9454
9964
  const key = parsed.flags.positional[0];
9455
9965
  const hit = await lookupTenantWide(key);
9456
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9457
- if (failure !== null)
9458
- return failure;
9966
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
9967
+ if (failure2 !== null)
9968
+ return failure2;
9459
9969
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9460
9970
  return 2;
9461
9971
  }
@@ -9502,9 +10012,9 @@ async function runResolve(args, io) {
9502
10012
  }
9503
10013
  const key = parsed.flags.positional[0];
9504
10014
  const hit = await lookupTenantWide(key);
9505
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9506
- if (failure !== null)
9507
- return failure;
10015
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
10016
+ if (failure2 !== null)
10017
+ return failure2;
9508
10018
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9509
10019
  return 2;
9510
10020
  }
@@ -9551,23 +10061,23 @@ function renderTable2(io, groups) {
9551
10061
  for (const r of rows)
9552
10062
  io.out(fmt(r));
9553
10063
  }
9554
- function describe19(e) {
10064
+ function describe20(e) {
9555
10065
  return e instanceof Error ? e.message : String(e);
9556
10066
  }
9557
10067
 
9558
10068
  // src/commands/init.ts
9559
10069
  import { createInterface } from "node:readline/promises";
9560
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9561
- import { dirname as dirname7, resolve as resolve9 } from "node:path";
10070
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
10071
+ import { dirname as dirname8, resolve as resolve11 } from "node:path";
9562
10072
  import { stringify as yamlStringify } from "yaml";
9563
10073
 
9564
10074
  // src/detect/index.ts
9565
- import { existsSync as existsSync8, readFileSync as readFileSync13, statSync } from "node:fs";
9566
- import { join as join12 } from "node:path";
10075
+ import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
10076
+ import { join as join13 } from "node:path";
9567
10077
  function detectAppShape(cwd) {
9568
- const hasPackageJson = existsSync8(join12(cwd, "package.json"));
9569
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync8(join12(cwd, file)));
9570
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync8(join12(cwd, n)));
10078
+ const hasPackageJson = existsSync9(join13(cwd, "package.json"));
10079
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join13(cwd, file)));
10080
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join13(cwd, n)));
9571
10081
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
9572
10082
  return {
9573
10083
  kind: "not-applicable",
@@ -9613,7 +10123,7 @@ var LOCKFILES = [
9613
10123
  { file: "yarn.lock", pm: "yarn" }
9614
10124
  ];
9615
10125
  function detectPackageManager(cwd) {
9616
- const present = LOCKFILES.filter(({ file }) => existsSync8(join12(cwd, file)));
10126
+ const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
9617
10127
  if (present.length === 0) {
9618
10128
  return {
9619
10129
  kind: "ambiguous",
@@ -9635,8 +10145,8 @@ function detectPackageManager(cwd) {
9635
10145
  }
9636
10146
  function detectVitePresence(cwd) {
9637
10147
  for (const name of VITE_CONFIG_NAMES) {
9638
- const p = join12(cwd, name);
9639
- if (existsSync8(p)) {
10148
+ const p = join13(cwd, name);
10149
+ if (existsSync9(p)) {
9640
10150
  return { kind: "ok", value: { path: p } };
9641
10151
  }
9642
10152
  }
@@ -9646,9 +10156,9 @@ function detectVitePresence(cwd) {
9646
10156
  };
9647
10157
  }
9648
10158
  function detectAppType(cwd) {
9649
- const fnDir = join12(cwd, "functions");
10159
+ const fnDir = join13(cwd, "functions");
9650
10160
  let hasFunctionsDir = false;
9651
- if (existsSync8(fnDir)) {
10161
+ if (existsSync9(fnDir)) {
9652
10162
  try {
9653
10163
  hasFunctionsDir = statSync(fnDir).isDirectory();
9654
10164
  } catch {
@@ -9658,8 +10168,8 @@ function detectAppType(cwd) {
9658
10168
  return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
9659
10169
  }
9660
10170
  function detectBuildCommand(cwd, pm) {
9661
- const pkgJsonPath = join12(cwd, "package.json");
9662
- if (!existsSync8(pkgJsonPath)) {
10171
+ const pkgJsonPath = join13(cwd, "package.json");
10172
+ if (!existsSync9(pkgJsonPath)) {
9663
10173
  return {
9664
10174
  kind: "ambiguous",
9665
10175
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -9731,8 +10241,8 @@ async function runInit(args, io, prompt) {
9731
10241
  return 64;
9732
10242
  }
9733
10243
  const { inputs, options } = parsed;
9734
- const outPath = resolve9(process.cwd(), options.out);
9735
- if (existsSync9(outPath) && !options.force) {
10244
+ const outPath = resolve11(process.cwd(), options.out);
10245
+ if (existsSync10(outPath) && !options.force) {
9736
10246
  io.err(`launchpad init: ${outPath} already exists`);
9737
10247
  io.err("Pass --force to overwrite.");
9738
10248
  return 64;
@@ -9779,7 +10289,7 @@ async function runInit(args, io, prompt) {
9779
10289
  }
9780
10290
  io.out(`✓ wrote ${outPath}`);
9781
10291
  if (options.gitignore) {
9782
- const gitignorePath = resolve9(process.cwd(), ".gitignore");
10292
+ const gitignorePath = resolve11(process.cwd(), ".gitignore");
9783
10293
  try {
9784
10294
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
9785
10295
  if (changed.length > 0) {
@@ -9794,9 +10304,9 @@ async function runInit(args, io, prompt) {
9794
10304
  }
9795
10305
  }
9796
10306
  if (fillResult.inputs.type === "static") {
9797
- const serveDir = resolve9(dirname7(outPath), STATIC_SERVE_DIR);
9798
- const indexPath = resolve9(serveDir, "index.html");
9799
- if (!existsSync9(indexPath)) {
10307
+ const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
10308
+ const indexPath = resolve11(serveDir, "index.html");
10309
+ if (!existsSync10(indexPath)) {
9800
10310
  try {
9801
10311
  mkdirSync3(serveDir, { recursive: true });
9802
10312
  writeFileSync3(indexPath, starterIndexHtml(fillResult.inputs.name), {
@@ -10188,7 +10698,7 @@ function renderYaml(manifest) {
10188
10698
  }
10189
10699
  function ensureGitignoreEntries(path10, entries) {
10190
10700
  let current = "";
10191
- if (existsSync9(path10)) {
10701
+ if (existsSync10(path10)) {
10192
10702
  current = readFileSync14(path10, "utf8");
10193
10703
  }
10194
10704
  const lines = current.split(/\r?\n/);
@@ -10262,13 +10772,13 @@ async function runLogin(args, io, deps) {
10262
10772
  io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
10263
10773
  return 0;
10264
10774
  } catch (e) {
10265
- io.err(`Gateway login failed: ${describe20(e)}`);
10775
+ io.err(`Gateway login failed: ${describe21(e)}`);
10266
10776
  io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
10267
10777
  io.err("");
10268
10778
  return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
10269
10779
  }
10270
10780
  } catch (e) {
10271
- io.err(`launchpad login failed: ${describe20(e)}`);
10781
+ io.err(`launchpad login failed: ${describe21(e)}`);
10272
10782
  return 1;
10273
10783
  }
10274
10784
  }
@@ -10310,7 +10820,7 @@ function printLoginHelp(io) {
10310
10820
  io.out(" 0 signed in / session stored");
10311
10821
  io.out(" 1 login failed (network, browser, or gateway error)");
10312
10822
  }
10313
- function describe20(e) {
10823
+ function describe21(e) {
10314
10824
  return e instanceof Error ? e.message : String(e);
10315
10825
  }
10316
10826
 
@@ -10347,14 +10857,14 @@ async function runLogout(args, io, deps) {
10347
10857
  });
10348
10858
  io.out("Server-side session revoked.");
10349
10859
  } catch (e) {
10350
- 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.`);
10860
+ io.err(`warning: could not revoke the session server-side (${describe22(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
10351
10861
  }
10352
10862
  }
10353
10863
  const had = await clearSession(cfg.sessionPath);
10354
10864
  io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
10355
10865
  return 0;
10356
10866
  } catch (e) {
10357
- io.err(`launchpad logout failed: ${describe21(e)}`);
10867
+ io.err(`launchpad logout failed: ${describe22(e)}`);
10358
10868
  return 1;
10359
10869
  }
10360
10870
  }
@@ -10374,7 +10884,7 @@ function printLogoutHelp(io) {
10374
10884
  io.out(" 0 logged out (or already logged out)");
10375
10885
  io.out(" 1 could not clear the local session (file IO / config error)");
10376
10886
  }
10377
- function describe21(e) {
10887
+ function describe22(e) {
10378
10888
  return e instanceof Error ? e.message : String(e);
10379
10889
  }
10380
10890
 
@@ -10444,7 +10954,7 @@ async function runLogs(args, io) {
10444
10954
  io.err(`launchpad logs: ${e.message}`);
10445
10955
  return 1;
10446
10956
  }
10447
- io.err(`launchpad logs failed: ${describe22(e)}`);
10957
+ io.err(`launchpad logs failed: ${describe23(e)}`);
10448
10958
  return 1;
10449
10959
  }
10450
10960
  }
@@ -10524,7 +11034,7 @@ function formatRelative2(iso) {
10524
11034
  return `${hr}h ago`;
10525
11035
  return `${Math.floor(hr / 24)}d ago`;
10526
11036
  }
10527
- function describe22(e) {
11037
+ function describe23(e) {
10528
11038
  return e instanceof Error ? e.message : String(e);
10529
11039
  }
10530
11040
 
@@ -10573,7 +11083,7 @@ async function runMerge(args, io) {
10573
11083
  try {
10574
11084
  body = await res.json();
10575
11085
  } catch (e) {
10576
- io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe23(e)}`);
11086
+ io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe24(e)}`);
10577
11087
  return 1;
10578
11088
  }
10579
11089
  io.out("");
@@ -10613,7 +11123,7 @@ async function runMerge(args, io) {
10613
11123
  io.err(`launchpad merge: ${e.message}`);
10614
11124
  return 1;
10615
11125
  }
10616
- io.err(`launchpad merge failed: ${describe23(e)}`);
11126
+ io.err(`launchpad merge failed: ${describe24(e)}`);
10617
11127
  return 1;
10618
11128
  }
10619
11129
  }
@@ -10712,12 +11222,12 @@ function renderBotError(status, env, io, prNumber = null) {
10712
11222
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
10713
11223
  }
10714
11224
  }
10715
- function describe23(e) {
11225
+ function describe24(e) {
10716
11226
  return e instanceof Error ? e.message : String(e);
10717
11227
  }
10718
11228
 
10719
11229
  // src/commands/plan.ts
10720
- import { resolve as resolve10 } from "node:path";
11230
+ import { resolve as resolve12 } from "node:path";
10721
11231
  var planCommand = {
10722
11232
  name: "plan",
10723
11233
  summary: "summarise what the manifest would deploy (offline)",
@@ -10730,8 +11240,19 @@ async function runPlan(args, io) {
10730
11240
  io.err("Usage: launchpad plan [--file <path>] [--json]");
10731
11241
  return 64;
10732
11242
  }
10733
- const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
11243
+ const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
10734
11244
  const result = loadManifest(manifestPath);
11245
+ if (result.kind === "ok") {
11246
+ const unsupported2 = rejectUnsupportedCliRuntime({
11247
+ command: "plan",
11248
+ manifestPath,
11249
+ manifest: result.manifest,
11250
+ io,
11251
+ json: flags.json
11252
+ });
11253
+ if (unsupported2 !== null)
11254
+ return unsupported2;
11255
+ }
10735
11256
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
10736
11257
  }
10737
11258
  function renderJson(result, io) {
@@ -10964,7 +11485,7 @@ async function runDestroy(args, io, prompt, isTty) {
10964
11485
  io.err(`launchpad destroy: ${e.message}`);
10965
11486
  return 1;
10966
11487
  }
10967
- io.err(`launchpad destroy failed: ${describe24(e)}`);
11488
+ io.err(`launchpad destroy failed: ${describe25(e)}`);
10968
11489
  return 1;
10969
11490
  }
10970
11491
  }
@@ -11164,7 +11685,7 @@ function printUsage4(io) {
11164
11685
  ].join(`
11165
11686
  `));
11166
11687
  }
11167
- function describe24(e) {
11688
+ function describe25(e) {
11168
11689
  return e instanceof Error ? e.message : String(e);
11169
11690
  }
11170
11691
 
@@ -11296,22 +11817,22 @@ async function treesEqual(cwd, tracked, headDir, headPaths) {
11296
11817
  if (tracked.length !== headPaths.length)
11297
11818
  return false;
11298
11819
  const expected = new Set(headPaths);
11299
- for (const relative5 of tracked) {
11300
- if (!expected.has(relative5))
11820
+ for (const relative6 of tracked) {
11821
+ if (!expected.has(relative6))
11301
11822
  return false;
11302
- if (!equal(await readEntry(path12.resolve(cwd, relative5)), await readEntry(path12.resolve(headDir, relative5)))) {
11823
+ if (!equal(await readEntry(path12.resolve(cwd, relative6)), await readEntry(path12.resolve(headDir, relative6)))) {
11303
11824
  return false;
11304
11825
  }
11305
11826
  }
11306
11827
  return true;
11307
11828
  }
11308
- async function listFiles(root, relative5 = "") {
11829
+ async function listFiles(root, relative6 = "") {
11309
11830
  const files = [];
11310
- const entries = await fs6.readdir(path12.resolve(root, relative5), {
11831
+ const entries = await fs6.readdir(path12.resolve(root, relative6), {
11311
11832
  withFileTypes: true
11312
11833
  });
11313
11834
  for (const entry of entries) {
11314
- const child = relative5.length === 0 ? entry.name : `${relative5}/${entry.name}`;
11835
+ const child = relative6.length === 0 ? entry.name : `${relative6}/${entry.name}`;
11315
11836
  if (entry.isDirectory())
11316
11837
  files.push(...await listFiles(root, child));
11317
11838
  else if (entry.isFile())
@@ -11320,12 +11841,12 @@ async function listFiles(root, relative5 = "") {
11320
11841
  return files.sort();
11321
11842
  }
11322
11843
  function gitOutput(cwd, args) {
11323
- return new Promise((resolve12, reject) => {
11844
+ return new Promise((resolve14, reject) => {
11324
11845
  execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
11325
11846
  if (error !== null)
11326
11847
  reject(error);
11327
11848
  else
11328
- resolve12(stdout);
11849
+ resolve14(stdout);
11329
11850
  });
11330
11851
  });
11331
11852
  }
@@ -11334,7 +11855,7 @@ function splitNull(value) {
11334
11855
  }
11335
11856
  async function reconcile(args) {
11336
11857
  const removals = await cleanAncestorRemovals(args);
11337
- const planned = await Promise.all(args.paths.map((relative5) => planPath(args, relative5, removals)));
11858
+ const planned = await Promise.all(args.paths.map((relative6) => planPath(args, relative6, removals)));
11338
11859
  const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
11339
11860
  if (conflicts.length > 0) {
11340
11861
  const report = {
@@ -11378,55 +11899,55 @@ async function reconcile(args) {
11378
11899
  }
11379
11900
  async function cleanAncestorRemovals(args) {
11380
11901
  const removals = new Set;
11381
- for (const relative5 of args.compare.deleted) {
11382
- assertSafePath(relative5);
11383
- const checked = await checkParents(args.cwd, relative5, removals);
11902
+ for (const relative6 of args.compare.deleted) {
11903
+ assertSafePath(relative6);
11904
+ const checked = await checkParents(args.cwd, relative6, removals);
11384
11905
  if (checked.kind === "blocked")
11385
11906
  continue;
11386
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
11907
+ const base = await readEntry(path12.resolve(args.baseDir, relative6));
11387
11908
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11388
11909
  if (local.kind !== "missing" && cleanLocal(base, local))
11389
- removals.add(relative5);
11910
+ removals.add(relative6);
11390
11911
  }
11391
11912
  return removals;
11392
11913
  }
11393
- async function planPath(args, relative5, removals) {
11394
- assertSafePath(relative5);
11395
- const checked = await checkParents(args.cwd, relative5, removals);
11396
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
11397
- const upstreamDeleted = args.compare.deleted.includes(relative5);
11398
- const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative5));
11399
- const unsupported = (args.compare.unsupported ?? []).includes(relative5);
11914
+ async function planPath(args, relative6, removals) {
11915
+ assertSafePath(relative6);
11916
+ const checked = await checkParents(args.cwd, relative6, removals);
11917
+ const base = await readEntry(path12.resolve(args.baseDir, relative6));
11918
+ const upstreamDeleted = args.compare.deleted.includes(relative6);
11919
+ const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative6));
11920
+ const unsupported2 = (args.compare.unsupported ?? []).includes(relative6);
11400
11921
  if (checked.kind === "blocked") {
11401
- return conflictPlan(relative5, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11922
+ return conflictPlan(relative6, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11402
11923
  }
11403
11924
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11404
- if (unsupported) {
11405
- return conflictPlan(relative5, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11925
+ if (unsupported2) {
11926
+ return conflictPlan(relative6, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11406
11927
  }
11407
11928
  if (alreadyResolved(local, head, upstreamDeleted)) {
11408
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: false };
11929
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: false };
11409
11930
  }
11410
11931
  if (unsupportedHead(head, upstreamDeleted)) {
11411
- return conflictPlan(relative5, checked.target, local, head, true, "unsupported upstream file type");
11932
+ return conflictPlan(relative6, checked.target, local, head, true, "unsupported upstream file type");
11412
11933
  }
11413
11934
  if (cleanLocal(base, local)) {
11414
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: true };
11935
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: true };
11415
11936
  }
11416
- return conflictPlan(relative5, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
11937
+ return conflictPlan(relative6, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
11417
11938
  }
11418
- function conflictPlan(relative5, target, local, head, upstreamDeleted, reason) {
11939
+ function conflictPlan(relative6, target, local, head, upstreamDeleted, reason) {
11419
11940
  const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
11420
- const digest = createHash3("sha256").update(relative5).digest("hex");
11941
+ const digest = createHash3("sha256").update(relative6).digest("hex");
11421
11942
  return {
11422
- relative: relative5,
11943
+ relative: relative6,
11423
11944
  target,
11424
11945
  local,
11425
11946
  head,
11426
11947
  upstreamDeleted,
11427
11948
  apply: false,
11428
11949
  conflict: {
11429
- path: relative5,
11950
+ path: relative6,
11430
11951
  reason,
11431
11952
  artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
11432
11953
  upstreamDeleted
@@ -11527,13 +12048,13 @@ function equal(left, right) {
11527
12048
  return false;
11528
12049
  return left.bytes.every((byte, index) => byte === right.bytes[index]);
11529
12050
  }
11530
- async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11531
- const checked = await checkParents(cwd, relative5, new Set, allowMetadata);
12051
+ async function writeAtomic(cwd, relative6, bytes, mode, allowMetadata = false) {
12052
+ const checked = await checkParents(cwd, relative6, new Set, allowMetadata);
11532
12053
  if (checked.kind === "blocked")
11533
12054
  throw new Error(checked.reason);
11534
12055
  const file = checked.target;
11535
12056
  await fs6.mkdir(path12.dirname(file), { recursive: true });
11536
- const rechecked = await checkParents(cwd, relative5, new Set, allowMetadata);
12057
+ const rechecked = await checkParents(cwd, relative6, new Set, allowMetadata);
11537
12058
  if (rechecked.kind === "blocked")
11538
12059
  throw new Error(rechecked.reason);
11539
12060
  const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
@@ -11548,8 +12069,8 @@ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11548
12069
  throw error;
11549
12070
  }
11550
12071
  }
11551
- async function writeJsonAtomic(cwd, relative5, value) {
11552
- await writeAtomic(cwd, relative5, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
12072
+ async function writeJsonAtomic(cwd, relative6, value) {
12073
+ await writeAtomic(cwd, relative6, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
11553
12074
  `), 384, true);
11554
12075
  }
11555
12076
  async function resetConflictArtifacts(cwd) {
@@ -11565,37 +12086,37 @@ async function cleanupConflictState(cwd) {
11565
12086
  throw new Error(report.reason);
11566
12087
  await fs6.rm(report.target, { force: true });
11567
12088
  }
11568
- function assertSafePath(relative5) {
11569
- if (!isSafeSourcePath(relative5)) {
11570
- throw new Error(`unsafe source path returned by bot: ${relative5}`);
12089
+ function assertSafePath(relative6) {
12090
+ if (!isSafeSourcePath(relative6)) {
12091
+ throw new Error(`unsafe source path returned by bot: ${relative6}`);
11571
12092
  }
11572
12093
  }
11573
- function isSafeSourcePath(relative5) {
11574
- return isSafeRelativePath(relative5) && !relative5.startsWith(`${MARKER_DIR}/`);
12094
+ function isSafeSourcePath(relative6) {
12095
+ return isSafeRelativePath(relative6) && !relative6.startsWith(`${MARKER_DIR}/`);
11575
12096
  }
11576
- function isSafeRelativePath(relative5) {
11577
- const normalized = path12.posix.normalize(relative5);
11578
- return relative5.length > 0 && !relative5.includes("\\") && !path12.isAbsolute(relative5) && normalized === relative5 && normalized !== ".." && !normalized.startsWith("../");
12097
+ function isSafeRelativePath(relative6) {
12098
+ const normalized = path12.posix.normalize(relative6);
12099
+ return relative6.length > 0 && !relative6.includes("\\") && !path12.isAbsolute(relative6) && normalized === relative6 && normalized !== ".." && !normalized.startsWith("../");
11579
12100
  }
11580
- function resolveInsideClone(cwd, relative5) {
12101
+ function resolveInsideClone(cwd, relative6) {
11581
12102
  const root = path12.resolve(cwd);
11582
- const target = path12.resolve(root, relative5);
12103
+ const target = path12.resolve(root, relative6);
11583
12104
  const fromRoot = path12.relative(root, target);
11584
12105
  if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
11585
- throw new Error(`unsafe source path returned by bot: ${relative5}`);
12106
+ throw new Error(`unsafe source path returned by bot: ${relative6}`);
11586
12107
  }
11587
12108
  return target;
11588
12109
  }
11589
- async function checkParents(cwd, relative5, projectedRemovals, allowMetadata = false) {
12110
+ async function checkParents(cwd, relative6, projectedRemovals, allowMetadata = false) {
11590
12111
  if (allowMetadata) {
11591
- if (!isSafeRelativePath(relative5)) {
11592
- throw new Error(`unsafe metadata path: ${relative5}`);
12112
+ if (!isSafeRelativePath(relative6)) {
12113
+ throw new Error(`unsafe metadata path: ${relative6}`);
11593
12114
  }
11594
12115
  } else {
11595
- assertSafePath(relative5);
12116
+ assertSafePath(relative6);
11596
12117
  }
11597
- const target = resolveInsideClone(cwd, relative5);
11598
- const components = relative5.split("/");
12118
+ const target = resolveInsideClone(cwd, relative6);
12119
+ const components = relative6.split("/");
11599
12120
  let current = path12.resolve(cwd);
11600
12121
  for (let index = 0;index < components.length - 1; index += 1) {
11601
12122
  current = path12.join(current, components[index]);
@@ -11734,7 +12255,7 @@ async function runPull(args, io) {
11734
12255
  io.err(`launchpad pull: ${e.message}`);
11735
12256
  return 1;
11736
12257
  }
11737
- io.err(`launchpad pull failed: ${describe25(e)}`);
12258
+ io.err(`launchpad pull failed: ${describe26(e)}`);
11738
12259
  return 1;
11739
12260
  }
11740
12261
  }
@@ -11842,7 +12363,7 @@ function printUsage5(io) {
11842
12363
  ].join(`
11843
12364
  `));
11844
12365
  }
11845
- function describe25(e) {
12366
+ function describe26(e) {
11846
12367
  return e instanceof Error ? e.message : String(e);
11847
12368
  }
11848
12369
 
@@ -12177,7 +12698,7 @@ async function runReview(args, io) {
12177
12698
  io.err(`launchpad review: ${e.message}`);
12178
12699
  return 1;
12179
12700
  }
12180
- io.err(`launchpad review failed: ${describe26(e)}`);
12701
+ io.err(`launchpad review failed: ${describe27(e)}`);
12181
12702
  return 1;
12182
12703
  }
12183
12704
  }
@@ -12255,12 +12776,12 @@ function severityRank(s) {
12255
12776
  return 3;
12256
12777
  }
12257
12778
  }
12258
- function describe26(e) {
12779
+ function describe27(e) {
12259
12780
  return e instanceof Error ? e.message : String(e);
12260
12781
  }
12261
12782
 
12262
12783
  // src/deploy/rollback.ts
12263
- import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
12784
+ import { existsSync as existsSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
12264
12785
  import { resolve as resolvePath3 } from "node:path";
12265
12786
  import { createInterface as createInterface3 } from "node:readline/promises";
12266
12787
 
@@ -12315,7 +12836,7 @@ async function runRollback(opts, io, deps = {}) {
12315
12836
  cwd: process.cwd()
12316
12837
  });
12317
12838
  } catch (e) {
12318
- io.err(`launchpad rollback: git rev-parse failed to start: ${describe27(e)}`);
12839
+ io.err(`launchpad rollback: git rev-parse failed to start: ${describe28(e)}`);
12319
12840
  return 2;
12320
12841
  }
12321
12842
  if (revParse.exitCode !== 0) {
@@ -12330,7 +12851,7 @@ async function runRollback(opts, io, deps = {}) {
12330
12851
  try {
12331
12852
  show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
12332
12853
  } catch (e) {
12333
- io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
12854
+ io.err(`launchpad rollback: git show failed to start: ${describe28(e)}`);
12334
12855
  return 2;
12335
12856
  }
12336
12857
  if (show.exitCode !== 0) {
@@ -12364,7 +12885,7 @@ async function runRollback(opts, io, deps = {}) {
12364
12885
  try {
12365
12886
  writeFileSync5(manifestPath, historicalYaml, "utf8");
12366
12887
  } catch (e) {
12367
- io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
12888
+ io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe28(e)}`);
12368
12889
  return 2;
12369
12890
  }
12370
12891
  io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
@@ -12385,7 +12906,7 @@ async function runRollback(opts, io, deps = {}) {
12385
12906
  }, io, applyDeps);
12386
12907
  }
12387
12908
  function readCurrentManifest(path14) {
12388
- if (!existsSync10(path14))
12909
+ if (!existsSync11(path14))
12389
12910
  return null;
12390
12911
  let raw;
12391
12912
  try {
@@ -12446,7 +12967,7 @@ async function defaultPrompt4(question) {
12446
12967
  rl.close();
12447
12968
  }
12448
12969
  }
12449
- function describe27(e) {
12970
+ function describe28(e) {
12450
12971
  return e instanceof Error ? e.message : String(e);
12451
12972
  }
12452
12973
  function renderManifestError3(result, io) {
@@ -12545,13 +13066,13 @@ function parseArgs12(args) {
12545
13066
  }
12546
13067
 
12547
13068
  // src/secrets/push.ts
12548
- import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
13069
+ import { existsSync as existsSync12, readFileSync as readFileSync16 } from "node:fs";
12549
13070
  import { resolve as resolvePath4 } from "node:path";
12550
13071
  import { createInterface as createInterface4 } from "node:readline/promises";
12551
13072
  import { stdin as input2, stdout as output2 } from "node:process";
12552
13073
 
12553
13074
  // src/secrets/env-parse.ts
12554
- var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
13075
+ var ENV_NAME2 = /^[A-Z][A-Z0-9_]*$/;
12555
13076
  var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
12556
13077
  function parseEnv(text) {
12557
13078
  const entries = new Map;
@@ -12576,7 +13097,7 @@ function parseEnv(text) {
12576
13097
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
12577
13098
  value = value.slice(1, -1);
12578
13099
  }
12579
- if (!ENV_NAME.test(key)) {
13100
+ if (!ENV_NAME2.test(key)) {
12580
13101
  invalidNames.push(key);
12581
13102
  continue;
12582
13103
  }
@@ -12602,7 +13123,7 @@ async function runSecretsPush(opts, io, deps = {}) {
12602
13123
  return 0;
12603
13124
  }
12604
13125
  const envPath = resolvePath4(process.cwd(), opts.env ?? ".env");
12605
- if (!existsSync11(envPath)) {
13126
+ if (!existsSync12(envPath)) {
12606
13127
  io.err(`launchpad secrets push: ${envPath}`);
12607
13128
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
12608
13129
  return 2;
@@ -12611,7 +13132,7 @@ async function runSecretsPush(opts, io, deps = {}) {
12611
13132
  try {
12612
13133
  envText = readFileSync16(envPath, "utf8");
12613
13134
  } catch (e) {
12614
- io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
13135
+ io.err(`launchpad secrets push: failed to read ${envPath}: ${describe29(e)}`);
12615
13136
  return 2;
12616
13137
  }
12617
13138
  const parsed = parseEnv(envText);
@@ -12795,10 +13316,10 @@ function mapPushError(e, slug, name, io) {
12795
13316
  io.err(`✗ secrets push ${name}: ${e.message}`);
12796
13317
  return 1;
12797
13318
  }
12798
- io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
13319
+ io.err(`✗ secrets push ${name} failed: ${describe29(e)}`);
12799
13320
  return 2;
12800
13321
  }
12801
- function describe28(e) {
13322
+ function describe29(e) {
12802
13323
  return e instanceof Error ? e.message : String(e);
12803
13324
  }
12804
13325
  function renderManifestError4(result, io) {
@@ -12934,7 +13455,7 @@ function renderManifestError5(result, io) {
12934
13455
  }
12935
13456
 
12936
13457
  // src/secrets/set.ts
12937
- import { existsSync as existsSync12, readFileSync as readFileSync17 } from "node:fs";
13458
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
12938
13459
  import { resolve as resolvePath6 } from "node:path";
12939
13460
  import { parse as parseYaml8 } from "yaml";
12940
13461
  var CELL_LABEL2 = {
@@ -12944,7 +13465,7 @@ var CELL_LABEL2 = {
12944
13465
  };
12945
13466
  function loadSet(fleetFile, setName, io) {
12946
13467
  const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
12947
- if (!existsSync12(path14)) {
13468
+ if (!existsSync13(path14)) {
12948
13469
  io.err(`✗ ${path14}`);
12949
13470
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
12950
13471
  return 2;
@@ -13036,7 +13557,7 @@ async function runSecretsPushSet(opts, io, deps = {}) {
13036
13557
  if (typeof set === "number")
13037
13558
  return set;
13038
13559
  const envPath = resolvePath6(process.cwd(), opts.env ?? ".env");
13039
- if (!existsSync12(envPath)) {
13560
+ if (!existsSync13(envPath)) {
13040
13561
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
13041
13562
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
13042
13563
  return 2;
@@ -13187,8 +13708,8 @@ function setPushExit(e) {
13187
13708
  }
13188
13709
 
13189
13710
  // src/commands/secrets-template.ts
13190
- import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "node:fs";
13191
- import { resolve as resolve12 } from "node:path";
13711
+ import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
13712
+ import { resolve as resolve14 } from "node:path";
13192
13713
  async function runSecretsTemplate(args, io) {
13193
13714
  const flags = parseFlags4(args);
13194
13715
  if (flags.kind === "usage-error") {
@@ -13196,7 +13717,7 @@ async function runSecretsTemplate(args, io) {
13196
13717
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
13197
13718
  return 64;
13198
13719
  }
13199
- const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
13720
+ const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
13200
13721
  const result = loadManifest(manifestPath);
13201
13722
  const renderResult = renderManifest(result, io);
13202
13723
  if (renderResult.kind !== "ok") {
@@ -13210,8 +13731,8 @@ async function runSecretsTemplate(args, io) {
13210
13731
  }
13211
13732
  return 0;
13212
13733
  }
13213
- const outPath = resolve12(process.cwd(), flags.out);
13214
- if (existsSync13(outPath) && !flags.force) {
13734
+ const outPath = resolve14(process.cwd(), flags.out);
13735
+ if (existsSync14(outPath) && !flags.force) {
13215
13736
  io.err(`launchpad secrets template: ${outPath} already exists`);
13216
13737
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
13217
13738
  return 64;
@@ -13507,16 +14028,16 @@ function printHelp3(io) {
13507
14028
 
13508
14029
  // src/commands/skills.ts
13509
14030
  import { fileURLToPath } from "node:url";
13510
- import { dirname as dirname9, join as join15, resolve as resolve13 } from "node:path";
13511
- import { promises as fs7, existsSync as existsSync14 } from "node:fs";
14031
+ import { dirname as dirname10, join as join16, resolve as resolve15 } from "node:path";
14032
+ import { promises as fs7, existsSync as existsSync15 } from "node:fs";
13512
14033
 
13513
14034
  // src/skills-bundle.ts
13514
14035
  import { homedir as homedir2 } from "node:os";
13515
- import { join as join14 } from "node:path";
14036
+ import { join as join15 } from "node:path";
13516
14037
  import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
13517
14038
  var SKILL_PREFIX = "launchpad-";
13518
14039
  function skillsTargetDir() {
13519
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
14040
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
13520
14041
  }
13521
14042
  function readInstalledSkills() {
13522
14043
  try {
@@ -13526,7 +14047,7 @@ function readInstalledSkills() {
13526
14047
  return { present: false, version: null };
13527
14048
  return {
13528
14049
  present: true,
13529
- version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
14050
+ version: readSkillVersion(join15(dir, bundle.name, "SKILL.md"))
13530
14051
  };
13531
14052
  } catch {
13532
14053
  return { present: false, version: null };
@@ -13588,7 +14109,7 @@ async function runSkills(args, io) {
13588
14109
  return 64;
13589
14110
  }
13590
14111
  } catch (e) {
13591
- io.err(`launchpad skills ${action}: ${describe29(e)}`);
14112
+ io.err(`launchpad skills ${action}: ${describe30(e)}`);
13592
14113
  return 1;
13593
14114
  }
13594
14115
  }
@@ -13611,13 +14132,13 @@ function resolveInstallEnv() {
13611
14132
  return { bundleDir, userSkillsDir };
13612
14133
  }
13613
14134
  function defaultBundleDir() {
13614
- const here = dirname9(fileURLToPath(import.meta.url));
14135
+ const here = dirname10(fileURLToPath(import.meta.url));
13615
14136
  const candidates = [
13616
- resolve13(here, "..", "skills"),
13617
- resolve13(here, "..", "..", "skills")
14137
+ resolve15(here, "..", "skills"),
14138
+ resolve15(here, "..", "..", "skills")
13618
14139
  ];
13619
14140
  for (const c of candidates) {
13620
- if (existsSync14(join15(c, "launchpad-onboard", "SKILL.md"))) {
14141
+ if (existsSync15(join16(c, "launchpad-onboard", "SKILL.md"))) {
13621
14142
  return c;
13622
14143
  }
13623
14144
  }
@@ -13638,12 +14159,12 @@ async function doInstall(io) {
13638
14159
  }
13639
14160
  let installed = 0;
13640
14161
  for (const skill of BUNDLED_SKILLS) {
13641
- const src = join15(env.bundleDir, skill);
14162
+ const src = join16(env.bundleDir, skill);
13642
14163
  if (!await isDir(src)) {
13643
14164
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
13644
14165
  return 1;
13645
14166
  }
13646
- const dest = join15(env.userSkillsDir, skill);
14167
+ const dest = join16(env.userSkillsDir, skill);
13647
14168
  await fs7.rm(dest, { recursive: true, force: true });
13648
14169
  await fs7.cp(src, dest, { recursive: true });
13649
14170
  installed++;
@@ -13668,7 +14189,7 @@ async function doUninstall(io) {
13668
14189
  continue;
13669
14190
  if (!isBundleManaged(entry.name))
13670
14191
  continue;
13671
- const target = join15(env.userSkillsDir, entry.name);
14192
+ const target = join16(env.userSkillsDir, entry.name);
13672
14193
  await fs7.rm(target, { recursive: true, force: true });
13673
14194
  removed++;
13674
14195
  io.out(`✗ removed ${target}`);
@@ -13691,7 +14212,7 @@ async function doList(io) {
13691
14212
  }
13692
14213
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
13693
14214
  for (const name of managedDirs) {
13694
- const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
14215
+ const skillFile = join16(env.userSkillsDir, name, "SKILL.md");
13695
14216
  const version = await readVersion(skillFile);
13696
14217
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
13697
14218
  }
@@ -13718,7 +14239,7 @@ async function isDir(path14) {
13718
14239
  return false;
13719
14240
  }
13720
14241
  }
13721
- function describe29(e) {
14242
+ function describe30(e) {
13722
14243
  return e instanceof Error ? e.message : String(e);
13723
14244
  }
13724
14245
 
@@ -13726,7 +14247,7 @@ function describe29(e) {
13726
14247
  import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
13727
14248
  import { promisify } from "node:util";
13728
14249
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13729
- import { dirname as dirname10, resolve as resolve14, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
14250
+ import { dirname as dirname11, resolve as resolve16, relative as relative6, isAbsolute as isAbsolute3, join as join17 } from "node:path";
13730
14251
  import { homedir as homedir3, tmpdir } from "node:os";
13731
14252
  import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
13732
14253
 
@@ -13771,9 +14292,9 @@ async function startLoopback(state, timeoutMs) {
13771
14292
  resolveCode(code);
13772
14293
  }
13773
14294
  });
13774
- const bound = await new Promise((resolve14) => {
13775
- server.once("error", () => resolve14(false));
13776
- server.listen(0, "127.0.0.1", () => resolve14(true));
14295
+ const bound = await new Promise((resolve16) => {
14296
+ server.once("error", () => resolve16(false));
14297
+ server.listen(0, "127.0.0.1", () => resolve16(true));
13777
14298
  });
13778
14299
  if (!bound)
13779
14300
  return null;
@@ -13849,7 +14370,7 @@ var PKG = "@m-kopa/launchpad-cli";
13849
14370
  var REGISTRY = "https://registry.npmjs.org";
13850
14371
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
13851
14372
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
13852
- var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
14373
+ var CHANNEL_MARKER = join17(homedir3(), ".launchpad", "channel");
13853
14374
  var EXIT_UPDATE_AVAILABLE = 10;
13854
14375
  var UPGRADE_ARGS = {
13855
14376
  npm: ["install", "-g", `${PKG}@latest`],
@@ -13978,8 +14499,8 @@ async function openSystemBrowser(url) {
13978
14499
  await execFileAsync(opener, [url]);
13979
14500
  }
13980
14501
  async function runInstallerScript(script) {
13981
- const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
13982
- const file = join16(dir, "install.sh");
14502
+ const dir = mkdtempSync(join17(tmpdir(), "launchpad-update-"));
14503
+ const file = join17(dir, "install.sh");
13983
14504
  try {
13984
14505
  writeFileSync7(file, script, { mode: 448 });
13985
14506
  return await new Promise((resolvePromise) => {
@@ -14066,7 +14587,7 @@ function errStderr(e) {
14066
14587
  return "";
14067
14588
  }
14068
14589
  async function detectPackageManager2() {
14069
- const pkgRoot = resolve14(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
14590
+ const pkgRoot = resolve16(dirname11(fileURLToPath2(import.meta.url)), "..", "..");
14070
14591
  const candidates = [];
14071
14592
  const npmRoot = await pmRoot("npm");
14072
14593
  if (npmRoot !== null)
@@ -14074,7 +14595,7 @@ async function detectPackageManager2() {
14074
14595
  const pnpmRoot = await pmRoot("pnpm");
14075
14596
  if (pnpmRoot !== null)
14076
14597
  candidates.push(["pnpm", pnpmRoot]);
14077
- candidates.push(["bun", resolve14(homedir3(), ".bun/install/global/node_modules")]);
14598
+ candidates.push(["bun", resolve16(homedir3(), ".bun/install/global/node_modules")]);
14078
14599
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
14079
14600
  return matches.length === 1 ? matches[0][0] : null;
14080
14601
  }
@@ -14088,7 +14609,7 @@ async function pmRoot(pm) {
14088
14609
  }
14089
14610
  }
14090
14611
  function pathContains(root, child) {
14091
- const rel = relative5(root, child);
14612
+ const rel = relative6(root, child);
14092
14613
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
14093
14614
  }
14094
14615
  function runUpgrade(pm) {
@@ -14211,8 +14732,8 @@ function printHelp5(io) {
14211
14732
  }
14212
14733
 
14213
14734
  // src/commands/validate.ts
14214
- import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14215
- import { dirname as dirname11, resolve as resolve15 } from "node:path";
14735
+ import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14736
+ import { dirname as dirname12, resolve as resolve17 } from "node:path";
14216
14737
  var validateCommand = {
14217
14738
  name: "validate",
14218
14739
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -14226,11 +14747,21 @@ async function runValidate(args, io) {
14226
14747
  return 64;
14227
14748
  }
14228
14749
  const { file, json, strictGroups } = parseResult;
14229
- const path14 = resolve15(process.cwd(), file ?? "launchpad.yaml");
14750
+ const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14230
14751
  const result = loadManifest(path14);
14231
14752
  if (result.kind !== "ok") {
14232
14753
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14233
14754
  }
14755
+ const unsupported2 = rejectUnsupportedCliRuntime({
14756
+ command: "validate",
14757
+ manifestPath: path14,
14758
+ manifest: result.manifest,
14759
+ io,
14760
+ json,
14761
+ structurallyValidName: result.manifest.metadata.name
14762
+ });
14763
+ if (unsupported2 !== null)
14764
+ return unsupported2;
14234
14765
  const boundary = checkBoundary(path14, result.manifest.app !== undefined);
14235
14766
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14236
14767
  if (serveErrors.length > 0) {
@@ -14240,7 +14771,7 @@ async function runValidate(args, io) {
14240
14771
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14241
14772
  }
14242
14773
  function checkBoundary(manifestPath, declared) {
14243
- const dir = dirname11(manifestPath);
14774
+ const dir = dirname12(manifestPath);
14244
14775
  let files;
14245
14776
  try {
14246
14777
  files = walkCwd(dir).files;
@@ -14272,9 +14803,9 @@ function checkStaticServeDir(manifestPath, manifest) {
14272
14803
  `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.`
14273
14804
  ];
14274
14805
  }
14275
- const dir = dirname11(manifestPath);
14276
- const abs = resolve15(dir, serveDir);
14277
- if (!existsSync15(abs)) {
14806
+ const dir = dirname12(manifestPath);
14807
+ const abs = resolve17(dir, serveDir);
14808
+ if (!existsSync16(abs)) {
14278
14809
  return [
14279
14810
  `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).`
14280
14811
  ];
@@ -14283,7 +14814,7 @@ function checkStaticServeDir(manifestPath, manifest) {
14283
14814
  try {
14284
14815
  isDir2 = statSync2(abs).isDirectory();
14285
14816
  } catch (e) {
14286
- return [`failed to inspect static served directory '${serveDir}/': ${describe30(e)}`];
14817
+ return [`failed to inspect static served directory '${serveDir}/': ${describe31(e)}`];
14287
14818
  }
14288
14819
  if (!isDir2) {
14289
14820
  return [`static app serves '${serveDir}/' but that path is not a directory.`];
@@ -14292,14 +14823,14 @@ function checkStaticServeDir(manifestPath, manifest) {
14292
14823
  try {
14293
14824
  entries = readdirSync3(abs);
14294
14825
  } catch (e) {
14295
- return [`failed to read static served directory '${serveDir}/': ${describe30(e)}`];
14826
+ return [`failed to read static served directory '${serveDir}/': ${describe31(e)}`];
14296
14827
  }
14297
14828
  if (entries.length === 0) {
14298
14829
  return [
14299
14830
  `static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
14300
14831
  ];
14301
14832
  }
14302
- if (!existsSync15(resolve15(abs, "index.html"))) {
14833
+ if (!existsSync16(resolve17(abs, "index.html"))) {
14303
14834
  return [
14304
14835
  `static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
14305
14836
  ];
@@ -14347,7 +14878,7 @@ async function checkGroup(allowedGroup, cfg) {
14347
14878
  if (e instanceof ForbiddenError) {
14348
14879
  return { kind: "forbidden", message: e.message };
14349
14880
  }
14350
- return { kind: "network", message: describe30(e) };
14881
+ return { kind: "network", message: describe31(e) };
14351
14882
  }
14352
14883
  }
14353
14884
  async function checkGroups(groups) {
@@ -14444,6 +14975,8 @@ function renderHumanError(result, io) {
14444
14975
  }
14445
14976
  function renderJsonOk(result, group, boundary, io) {
14446
14977
  const base = {
14978
+ manifest_valid: true,
14979
+ runtime_supported: true,
14447
14980
  name: result.manifest.metadata.name,
14448
14981
  deploymentType: result.manifest.deployment.type,
14449
14982
  appBoundary: {
@@ -14560,7 +15093,7 @@ function parseArgs13(args) {
14560
15093
  }
14561
15094
  return { kind: "ok", file, json, strictGroups };
14562
15095
  }
14563
- function describe30(e) {
15096
+ function describe31(e) {
14564
15097
  return e instanceof Error ? e.message : String(e);
14565
15098
  }
14566
15099
 
@@ -14613,7 +15146,7 @@ async function runWhoami(args, io) {
14613
15146
  io.err(e.message);
14614
15147
  return 1;
14615
15148
  }
14616
- io.err(`launchpad whoami failed: ${describe31(e)}`);
15149
+ io.err(`launchpad whoami failed: ${describe32(e)}`);
14617
15150
  return 1;
14618
15151
  }
14619
15152
  }
@@ -14643,11 +15176,11 @@ async function runShare(args, io) {
14643
15176
  io.err(e.message);
14644
15177
  return 1;
14645
15178
  }
14646
- io.err(`launchpad whoami --share failed: ${describe31(e)}`);
15179
+ io.err(`launchpad whoami --share failed: ${describe32(e)}`);
14647
15180
  return 1;
14648
15181
  }
14649
15182
  }
14650
- function describe31(e) {
15183
+ function describe32(e) {
14651
15184
  return e instanceof Error ? e.message : String(e);
14652
15185
  }
14653
15186
 
@@ -14813,7 +15346,7 @@ async function runEditor(action, args, io, deps) {
14813
15346
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
14814
15347
  return 1;
14815
15348
  }
14816
- io.err(`launchpad ${verb} failed: ${describe32(e)}`);
15349
+ io.err(`launchpad ${verb} failed: ${describe33(e)}`);
14817
15350
  return 1;
14818
15351
  }
14819
15352
  }
@@ -14847,7 +15380,7 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
14847
15380
  nonThrowingStatuses: [409]
14848
15381
  });
14849
15382
  }
14850
- function describe32(e) {
15383
+ function describe33(e) {
14851
15384
  return e instanceof Error ? e.message : String(e);
14852
15385
  }
14853
15386
  var grantEditorCommand = makeEditorCommand("grant");
@@ -15053,7 +15586,7 @@ async function runOwner(action, args, io, deps) {
15053
15586
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
15054
15587
  return 1;
15055
15588
  }
15056
- io.err(`launchpad ${verb} failed: ${describe33(e)}`);
15589
+ io.err(`launchpad ${verb} failed: ${describe34(e)}`);
15057
15590
  return 1;
15058
15591
  }
15059
15592
  }
@@ -15087,12 +15620,92 @@ async function sendMutation2(action, slug, target, version, cfg, deps) {
15087
15620
  nonThrowingStatuses: [409]
15088
15621
  });
15089
15622
  }
15090
- function describe33(e) {
15623
+ function describe34(e) {
15091
15624
  return e instanceof Error ? e.message : String(e);
15092
15625
  }
15093
15626
  var addOwnerCommand = makeOwnerCommand("add");
15094
15627
  var removeOwnerCommand = makeOwnerCommand("remove");
15095
15628
 
15629
+ // src/commands/admin.ts
15630
+ var REAL_DEPS3 = { loadConfig, apiJson };
15631
+ var adminCommand = makeAdminCommand();
15632
+ function makeAdminCommand(deps = REAL_DEPS3) {
15633
+ return {
15634
+ name: "admin",
15635
+ summary: "run authenticated platform-operator actions",
15636
+ run: (args, io) => runAdmin(args, io, deps)
15637
+ };
15638
+ }
15639
+ function statusPageUrl(value, allowedHost) {
15640
+ try {
15641
+ const url = new URL(value);
15642
+ if (url.protocol !== "https:")
15643
+ return null;
15644
+ if (url.hostname !== allowedHost)
15645
+ return null;
15646
+ if (!/^\/d\/[^/]+$/u.test(url.pathname))
15647
+ return null;
15648
+ if (url.username !== "" || url.password !== "")
15649
+ return null;
15650
+ if (url.search !== "" || url.hash !== "")
15651
+ return null;
15652
+ return url.toString();
15653
+ } catch {
15654
+ return null;
15655
+ }
15656
+ }
15657
+ async function runAdmin(args, io, deps) {
15658
+ if (args.includes("--help") || args.includes("-h")) {
15659
+ printHelp6(io);
15660
+ return 0;
15661
+ }
15662
+ const config = deps.loadConfig();
15663
+ const parsed = parseStatusRevokeArgs(args, config.statusPageHost ?? DEFAULT_STATUS_PAGE_HOST);
15664
+ if (parsed.kind === "unknown") {
15665
+ io.err(`launchpad admin: unknown subcommand '${parsed.subcommand}'`);
15666
+ printHelp6(io);
15667
+ return 64;
15668
+ }
15669
+ if (parsed.kind === "invalid") {
15670
+ io.err("launchpad admin: expected `status-revoke <status-url>`");
15671
+ printHelp6(io);
15672
+ return 64;
15673
+ }
15674
+ return revokeStatusPage(parsed.url, config, io, deps);
15675
+ }
15676
+ function parseStatusRevokeArgs(args, allowedHost) {
15677
+ if (args[0] !== undefined && args[0] !== "status-revoke") {
15678
+ return { kind: "unknown", subcommand: args[0] };
15679
+ }
15680
+ if (args.length !== 2)
15681
+ return { kind: "invalid" };
15682
+ const url = statusPageUrl(args[1], allowedHost);
15683
+ return url === null ? { kind: "invalid" } : { kind: "ok", url };
15684
+ }
15685
+ async function revokeStatusPage(url, config, io, deps) {
15686
+ try {
15687
+ const response = await deps.apiJson(config, {
15688
+ method: "POST",
15689
+ path: "/status-page/revoke",
15690
+ jsonBody: { url }
15691
+ });
15692
+ io.out(statusRevokeMessage(response));
15693
+ return 0;
15694
+ } catch (error) {
15695
+ io.err(`launchpad admin status-revoke: ${error instanceof Error ? error.message : String(error)}`);
15696
+ return 1;
15697
+ }
15698
+ }
15699
+ function statusRevokeMessage(response) {
15700
+ return response.revoked ? "Status page revoked." : "Status page was already absent.";
15701
+ }
15702
+ function printHelp6(io) {
15703
+ io.out("launchpad admin - authenticated platform-operator actions.");
15704
+ io.out("");
15705
+ io.out("Usage:");
15706
+ io.out(" launchpad admin status-revoke <status-url>");
15707
+ }
15708
+
15096
15709
  // src/commands/list-owners.ts
15097
15710
  function realDeps4() {
15098
15711
  return { loadConfig, apiJson };
@@ -15168,9 +15781,9 @@ var listOwnersCommand = makeListOwnersCommand();
15168
15781
  // src/update-notifier.ts
15169
15782
  import { spawn as spawn6 } from "node:child_process";
15170
15783
  import { homedir as homedir4 } from "node:os";
15171
- import { join as join17 } from "node:path";
15784
+ import { join as join18 } from "node:path";
15172
15785
  import {
15173
- existsSync as existsSync16,
15786
+ existsSync as existsSync17,
15174
15787
  mkdirSync as mkdirSync4,
15175
15788
  readFileSync as readFileSync21,
15176
15789
  writeFileSync as writeFileSync8
@@ -15178,7 +15791,7 @@ import {
15178
15791
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
15179
15792
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
15180
15793
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
15181
- var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
15794
+ var CACHE_FILE = join18(homedir4(), ".launchpad", "update-check.json");
15182
15795
  function readCache2() {
15183
15796
  try {
15184
15797
  const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
@@ -15194,22 +15807,22 @@ function readCache2() {
15194
15807
  }
15195
15808
  function writeCache2(state) {
15196
15809
  try {
15197
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15810
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15198
15811
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
15199
15812
  `, { mode: 384 });
15200
15813
  } catch {}
15201
15814
  }
15202
- var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
15815
+ var SKILLS_HINT_MARKER = join18(homedir4(), ".launchpad", "skills-hint-shown");
15203
15816
  function absentHintShown() {
15204
15817
  try {
15205
- return existsSync16(SKILLS_HINT_MARKER);
15818
+ return existsSync17(SKILLS_HINT_MARKER);
15206
15819
  } catch {
15207
15820
  return false;
15208
15821
  }
15209
15822
  }
15210
15823
  function markAbsentHintShown() {
15211
15824
  try {
15212
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15825
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15213
15826
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
15214
15827
  `, { mode: 384 });
15215
15828
  } catch {}
@@ -15312,8 +15925,8 @@ var refreshUpdateCacheCommand = {
15312
15925
  // src/telemetry.ts
15313
15926
  import { spawn as spawn7 } from "node:child_process";
15314
15927
  import { homedir as homedir5 } from "node:os";
15315
- import { join as join18 } from "node:path";
15316
- import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
15928
+ import { join as join19 } from "node:path";
15929
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
15317
15930
  import { randomUUID } from "node:crypto";
15318
15931
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
15319
15932
  var POSTHOG_HOST = "https://us.i.posthog.com";
@@ -15321,8 +15934,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
15321
15934
  var CAPTURE_PATH = "/i/v0/e/";
15322
15935
  var SEND_TIMEOUT_MS = 3000;
15323
15936
  var APP = "launchpad-cli";
15324
- var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
15325
- var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
15937
+ var DEVICE_ID_FILE = join19(homedir5(), ".launchpad", "telemetry-id.json");
15938
+ var FIRST_RUN_MARKER = join19(homedir5(), ".launchpad", "telemetry-notice-shown");
15326
15939
  function buildEvent(ctx, identity, nowMs) {
15327
15940
  const properties = {
15328
15941
  app: APP,
@@ -15396,7 +16009,7 @@ function getOrCreateDeviceId() {
15396
16009
  } catch {}
15397
16010
  const id = randomUUID();
15398
16011
  try {
15399
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16012
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15400
16013
  writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
15401
16014
  `, {
15402
16015
  mode: 384
@@ -15467,14 +16080,14 @@ function telemetryNoticeLine() {
15467
16080
  }
15468
16081
  function firstRunNoticeShown() {
15469
16082
  try {
15470
- return existsSync17(FIRST_RUN_MARKER);
16083
+ return existsSync18(FIRST_RUN_MARKER);
15471
16084
  } catch {
15472
16085
  return false;
15473
16086
  }
15474
16087
  }
15475
16088
  function markFirstRunNotice() {
15476
16089
  try {
15477
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16090
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15478
16091
  writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
15479
16092
  `, { mode: 384 });
15480
16093
  } catch {}
@@ -15542,14 +16155,14 @@ import { platform } from "node:os";
15542
16155
  // src/report/breadcrumb.ts
15543
16156
  import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
15544
16157
  import { homedir as homedir6 } from "node:os";
15545
- import { join as join19, dirname as dirname12 } from "node:path";
16158
+ import { join as join20, dirname as dirname13 } from "node:path";
15546
16159
  function breadcrumbPath() {
15547
- return join19(homedir6(), ".launchpad", "last-run.json");
16160
+ return join20(homedir6(), ".launchpad", "last-run.json");
15548
16161
  }
15549
16162
  function writeBreadcrumb(verb, exit) {
15550
16163
  try {
15551
16164
  const p = breadcrumbPath();
15552
- mkdirSync6(dirname12(p), { recursive: true });
16165
+ mkdirSync6(dirname13(p), { recursive: true });
15553
16166
  writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
15554
16167
  } catch {}
15555
16168
  }
@@ -15749,6 +16362,7 @@ var COMMANDS = [
15749
16362
  addOwnerCommand,
15750
16363
  removeOwnerCommand,
15751
16364
  listOwnersCommand,
16365
+ adminCommand,
15752
16366
  makeReportCommand("bug"),
15753
16367
  makeReportCommand("feature"),
15754
16368
  refreshUpdateCacheCommand,
@@ -15756,7 +16370,7 @@ var COMMANDS = [
15756
16370
  ];
15757
16371
  async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
15758
16372
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
15759
- printHelp6(io, commands);
16373
+ printHelp7(io, commands);
15760
16374
  return 0;
15761
16375
  }
15762
16376
  if (argv[0] === "--version" || argv[0] === "-v") {
@@ -15819,9 +16433,11 @@ function requiresCliFloorPreflight(verb, args) {
15819
16433
  return args[0] === "set" || args[0] === "rm";
15820
16434
  if (verb === "secrets")
15821
16435
  return args[0] === "push";
16436
+ if (verb === "admin")
16437
+ return args[0] === "status-revoke";
15822
16438
  return false;
15823
16439
  }
15824
- function printHelp6(io, commands) {
16440
+ function printHelp7(io, commands) {
15825
16441
  io.out("launchpad — manage Launchpad-deployed apps from the command line.");
15826
16442
  io.out("");
15827
16443
  io.out("Usage:");