@m-kopa/launchpad-cli 0.54.1 → 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.1";
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,
@@ -3469,6 +3472,19 @@ var ManifestV2Schema = z2.object({
3469
3472
  spec: SpecSchema,
3470
3473
  status: StatusSchema.optional()
3471
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
+ }
3472
3488
  // ../launchpad-engine/dist/secrets-audit.js
3473
3489
  function auditSecretPresence(bindings, surfaces) {
3474
3490
  const byId = new Map(surfaces.map((s) => [s.id, s]));
@@ -3588,298 +3604,569 @@ function deriveResources(m) {
3588
3604
  }
3589
3605
  return items;
3590
3606
  }
3591
- // ../launchpad-engine/dist/wrangler.js
3592
- var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
3593
- var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
3594
- function generateWranglerToml(manifest) {
3595
- if (manifest.deployment.type !== "container") {
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") {
3596
3623
  return {
3597
- kind: "not-applicable",
3598
- reason: `deployment.type '${manifest.deployment.type}' does not use wrangler.toml`
3624
+ kind: "supported",
3625
+ manifestValid: true,
3626
+ runtimeSupported: true,
3627
+ runtime: "pages"
3599
3628
  };
3600
3629
  }
3601
- const containers = manifest.containers ?? [];
3602
- 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") {
3603
3690
  return {
3604
- kind: "ok",
3605
- content: emit(manifest, []),
3606
- warnings: ["container manifest has no containers[] — emitting empty TOML"]
3691
+ type: manifest.spec.appType,
3692
+ appName: manifest.metadata.slug,
3693
+ containers: manifest.spec.containers ?? []
3607
3694
  };
3608
3695
  }
3609
- 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
+ };
3610
3701
  }
3611
- function emit(manifest, containers) {
3612
- const lines = [];
3613
- const header = `# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.
3614
- # Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})
3615
- # App: ${manifest.metadata.name}
3616
- #
3617
- # Regenerate with \`launchpad generate\` (\`launchpad deploy\` also
3618
- # re-emits before applying). Manual edits are overwritten on the
3619
- # next regeneration. Manifest is the source of truth — change
3620
- # ${manifest.metadata.name}/launchpad.yaml instead.`;
3621
- lines.push(header);
3622
- lines.push("");
3623
- lines.push(`name = "${escapeTomlString(manifest.metadata.name)}"`);
3624
- lines.push(`main = "worker/index.ts"`);
3625
- lines.push(`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`);
3626
- lines.push(`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((f) => `"${escapeTomlString(f)}"`).join(", ")}]`);
3627
- lines.push("");
3628
- lines.push("# Workers Logs (PATTERNS.md: non-optional for container apps).");
3629
- lines.push("[observability]");
3630
- lines.push("enabled = true");
3631
- lines.push("head_sampling_rate = 1.0");
3632
- lines.push("");
3633
- lines.push("# Platform-managed runtime config. Populated by the deploy");
3634
- lines.push("# workflow via `wrangler deploy --var KEY:VALUE`. Secrets");
3635
- lines.push("# (declared in launchpad.yaml secrets.bindings[]) go through");
3636
- lines.push("# `wrangler secret put` and are NOT here.");
3637
- lines.push("[vars]");
3638
- lines.push(`CF_ACCESS_AUD = ""`);
3639
- lines.push(`CF_ACCESS_TEAM_DOMAIN = ""`);
3640
- lines.push("");
3641
- for (const c of containers) {
3642
- const className = containerClassName(c.name);
3643
- const maxInstances = c.max_instances ?? 1;
3644
- lines.push("[[containers]]");
3645
- lines.push(`class_name = "${className}"`);
3646
- lines.push(`image = "${escapeTomlString(c.image)}"`);
3647
- lines.push(`max_instances = ${maxInstances}`);
3648
- lines.push("");
3702
+ function validImagePath(path7) {
3703
+ if (byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("./") || path7.includes("\\") || CONTROL_CHARACTER.test(path7)) {
3704
+ return false;
3649
3705
  }
3650
- for (const c of containers) {
3651
- const className = containerClassName(c.name);
3652
- const bindingName = containerBindingName(c.name);
3653
- lines.push("[[durable_objects.bindings]]");
3654
- lines.push(`name = "${bindingName}"`);
3655
- lines.push(`class_name = "${className}"`);
3656
- lines.push("");
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;
3657
3712
  }
3658
- lines.push("# Fresh-deploy migration. Container-enabled DOs MUST be");
3659
- lines.push("# SQLite-backed (ADR 0015 #4/#5 — non-sqlite fails [code: 10074]).");
3660
- lines.push("# Re-shaping containers on a live app needs a v2 migration");
3661
- lines.push("# block; the engine's TF-state-aware migration generator lands");
3662
- lines.push("# with Scope 8 drift detection.");
3663
- lines.push("[[migrations]]");
3664
- lines.push(`tag = "v1"`);
3665
- const classNames = containers.map((c) => `"${containerClassName(c.name)}"`);
3666
- lines.push(`new_sqlite_classes = [${classNames.join(", ")}]`);
3667
- lines.push("");
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
+ ];
3668
3932
  return lines.join(`
3669
3933
  `);
3670
3934
  }
3671
- function containerClassName(manifestName) {
3672
- return "AppContainer" + manifestName.split("-").map(capitalise).join("");
3673
- }
3674
- function containerBindingName(manifestName) {
3675
- return "APP_CONTAINER_" + manifestName.split("-").map((p) => p.toUpperCase()).join("_");
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: [] };
3676
3954
  }
3677
- function capitalise(s) {
3678
- if (s.length === 0)
3679
- return s;
3680
- 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
+ `);
3681
3991
  }
3682
- function escapeTomlString(s) {
3683
- 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");
3684
3994
  }
3685
3995
  // ../launchpad-engine/dist/github-workflow.js
3686
- var DEFAULT_WORKERS_SUBDOMAIN = "mkopa-launchpad.workers.dev";
3687
- var DEFAULT_BUN_VERSION = "1.3.4";
3996
+ var DEFAULT_NODE_VERSION = "22";
3688
3997
  var ACTIONS_CHECKOUT_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
3689
- var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
3998
+ var ACTIONS_SETUP_NODE_SHA = "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
3690
3999
  function generateGithubDeployWorkflow(manifest) {
3691
- if (manifest.deployment.type !== "container") {
4000
+ const result = validateDeploymentCapability(manifest);
4001
+ if (result.kind === "unsupported") {
3692
4002
  return {
3693
- kind: "not-applicable",
3694
- reason: `deployment.type '${manifest.deployment.type}' has no engine-generated deploy workflow yet`
4003
+ kind: "unsupported",
4004
+ reason: result.reason,
4005
+ message: result.message
3695
4006
  };
3696
4007
  }
3697
- const bindings = manifest.secrets?.bindings ?? [];
3698
- const warnings = [];
3699
- if ((manifest.containers ?? []).length === 0) {
3700
- warnings.push("container manifest has no containers[] — workflow will deploy an empty Worker");
4008
+ if (result.runtime !== "container") {
4009
+ return {
4010
+ kind: "not-applicable",
4011
+ reason: "pages runtimes do not use the container deploy workflow"
4012
+ };
3701
4013
  }
3702
- return {
3703
- kind: "ok",
3704
- content: emit2(manifest, bindings),
3705
- warnings
3706
- };
4014
+ return { kind: "ok", content: emit3(result.capability), warnings: [] };
3707
4015
  }
3708
- function emit2(manifest, bindings) {
3709
- const appName = manifest.metadata.name;
3710
- const lines = [];
3711
- lines.push(`# Auto-generated by @krsecurity/launchpad-engine. Do not edit by hand.`);
3712
- lines.push(`# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})`);
3713
- lines.push(`# App: ${appName}`);
3714
- lines.push(`#`);
3715
- lines.push(`# Regenerate with \`launchpad generate\` (\`launchpad deploy\` also`);
3716
- lines.push(`# re-emits before applying). Manual edits are overwritten on the`);
3717
- lines.push(`# next regeneration. Manifest is the source of truth — change`);
3718
- lines.push(`# ${appName}/launchpad.yaml instead.`);
3719
- lines.push("");
3720
- lines.push(`name: deploy`);
3721
- lines.push("");
3722
- lines.push(`on:`);
3723
- lines.push(` push:`);
3724
- lines.push(` branches:`);
3725
- lines.push(` - main`);
3726
- lines.push(` workflow_dispatch:`);
3727
- lines.push("");
3728
- lines.push(`permissions:`);
3729
- lines.push(` contents: read`);
3730
- lines.push(` id-token: write`);
3731
- lines.push(` packages: read`);
3732
- lines.push("");
3733
- lines.push(`concurrency:`);
3734
- lines.push(` group: deploy-\${{ github.workflow }}-\${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3735
- lines.push(` cancel-in-progress: false`);
3736
- lines.push("");
3737
- lines.push(`defaults:`);
3738
- lines.push(` run:`);
3739
- lines.push(` # Container-shape apps keep wrangler.toml + Worker source +`);
3740
- lines.push(` # per-container Dockerfiles under container/. Engine convention.`);
3741
- lines.push(` working-directory: container`);
3742
- lines.push("");
3743
- lines.push(`jobs:`);
3744
- lines.push(` deploy:`);
3745
- lines.push(` name: deploy`);
3746
- lines.push(` runs-on: ubuntu-latest`);
3747
- lines.push(` environment: \${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}`);
3748
- lines.push(` steps:`);
3749
- lines.push(` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`);
3750
- lines.push("");
3751
- lines.push(` - uses: oven-sh/setup-bun@${SETUP_BUN_SHA} # v2`);
3752
- lines.push(` with:`);
3753
- lines.push(` bun-version: "${DEFAULT_BUN_VERSION}"`);
3754
- lines.push("");
3755
- lines.push(` - name: Install dependencies`);
3756
- lines.push(` env:`);
3757
- lines.push(` NODE_AUTH_TOKEN: \${{ secrets.NODE_AUTH_TOKEN }}`);
3758
- lines.push(` run: bun install --ignore-scripts`);
3759
- lines.push("");
3760
- lines.push(` - name: Deploy Worker + container images`);
3761
- lines.push(` # --var KEY:VALUE overrides the placeholder [vars] block in`);
3762
- lines.push(` # wrangler.toml. Missing either CF_ACCESS_* var trips the`);
3763
- lines.push(` # Worker's auth_unconfigured fail-closed branch on every`);
3764
- lines.push(` # request. Multi-container deploys build + push every`);
3765
- lines.push(` # [[containers]] block; total time scales linearly.`);
3766
- lines.push(` env:`);
3767
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3768
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3769
- lines.push(` run: |`);
3770
- lines.push(` bunx wrangler deploy \\`);
3771
- lines.push(` --var "CF_ACCESS_AUD:\${{ vars.CF_ACCESS_AUD }}" \\`);
3772
- lines.push(` --var "CF_ACCESS_TEAM_DOMAIN:\${{ vars.CF_ACCESS_TEAM_DOMAIN }}"`);
3773
- lines.push("");
3774
- for (const b of bindings) {
3775
- if (b.source !== "env-file")
3776
- continue;
3777
- emitSecretStep(lines, b);
3778
- }
3779
- const workerUrl = `https://${appName}.${DEFAULT_WORKERS_SUBDOMAIN}`;
3780
- lines.push(` - name: Smoke probe — Worker liveness (via .well-known bypass)`);
3781
- lines.push(` # In workers_dev_only mode CF Access intercepts every path`);
3782
- lines.push(` # (including /health) with HTTP 302 to the IdP. The only`);
3783
- lines.push(` # path that reaches the Worker without an Access JWT is`);
3784
- lines.push(` # /.well-known/openid-configuration, gated by a separate`);
3785
- lines.push(` # Bypass-policy Access app.`);
3786
- lines.push(` #`);
3787
- lines.push(` # Outcomes:`);
3788
- lines.push(` # 302 → bypass policy missing or misconfigured.`);
3789
- lines.push(` # 5xx → Worker is down (deploy bug).`);
3790
- lines.push(` # 401 + auth_unconfigured → CF_ACCESS_* vars didn't wire in.`);
3791
- lines.push(` # 401 + access_jwt_missing (any other 4xx) → HAPPY PATH.`);
3792
- lines.push(` run: |`);
3793
- lines.push(` set -euo pipefail`);
3794
- lines.push(` URL="${workerUrl}/.well-known/openid-configuration"`);
3795
- lines.push(` for i in 1 2 3 4 5; do`);
3796
- lines.push(` body_file="$(mktemp)"`);
3797
- lines.push(` code=$(curl -sS -o "$body_file" -w "%{http_code}" "$URL" || echo "000")`);
3798
- lines.push(` body=$(cat "$body_file")`);
3799
- lines.push(` rm -f "$body_file"`);
3800
- lines.push("");
3801
- lines.push(` if [ "$code" = "302" ]; then`);
3802
- lines.push(` echo "liveness-probe: bypass policy missing — Access redirected /.well-known/. Attempt $i."`);
3803
- lines.push(` sleep 5`);
3804
- lines.push(` continue`);
3805
- lines.push(` fi`);
3806
- lines.push(` if [ "\${code:0:1}" = "5" ] || [ "$code" = "000" ]; then`);
3807
- lines.push(` echo "liveness-probe: Worker down (got $code). Attempt $i."`);
3808
- lines.push(` sleep 5`);
3809
- lines.push(` continue`);
3810
- lines.push(` fi`);
3811
- lines.push(` if printf '%s' "$body" | grep -q "auth_unconfigured"; then`);
3812
- lines.push(` echo "liveness-probe FAIL: Worker is up but in fail-closed misconfig branch — CF_ACCESS_AUD / CF_ACCESS_TEAM_DOMAIN not wired in." >&2`);
3813
- lines.push(` echo "body: $body" >&2`);
3814
- lines.push(` exit 1`);
3815
- lines.push(` fi`);
3816
- lines.push(` echo "liveness-probe ok: Worker reachable (HTTP $code), env wired in (no auth_unconfigured)."`);
3817
- lines.push(` exit 0`);
3818
- lines.push(` done`);
3819
- lines.push(` echo "liveness-probe failed after 5 attempts" >&2`);
3820
- lines.push(` curl -sSI "$URL" >&2 || true`);
3821
- lines.push(` exit 1`);
3822
- lines.push("");
3823
- lines.push(` - name: Smoke probe — frontend route is auth-gated`);
3824
- lines.push(` # 302 or 401 prove unauthenticated traffic is blocked.`);
3825
- lines.push(` # HTTP 200 here would mean the SPA bundle is publicly`);
3826
- lines.push(` # readable — the regression we're catching.`);
3827
- lines.push(` run: |`);
3828
- lines.push(` set -euo pipefail`);
3829
- lines.push(` URL="${workerUrl}/"`);
3830
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3831
- lines.push(` case "$code" in`);
3832
- lines.push(` 302|401)`);
3833
- lines.push(` echo "frontend auth-gate ok: HTTP $code on unauthenticated request to /."`);
3834
- lines.push(` ;;`);
3835
- lines.push(` *)`);
3836
- lines.push(` echo "frontend auth-gate regression: expected 302 or 401 on /, got $code" >&2`);
3837
- lines.push(` curl -sSI "$URL" >&2 || true`);
3838
- lines.push(` exit 1`);
3839
- lines.push(` ;;`);
3840
- lines.push(` esac`);
3841
- lines.push("");
3842
- lines.push(` - name: Smoke probe — backend route is auth-gated`);
3843
- lines.push(` # Same logic as the frontend probe; confirms /api/* surface`);
3844
- lines.push(` # is gated. A 200 here would be the worse regression:`);
3845
- lines.push(` # backend data leaked unauthenticated.`);
3846
- lines.push(` run: |`);
3847
- lines.push(` set -euo pipefail`);
3848
- lines.push(` URL="${workerUrl}/api/health"`);
3849
- lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
3850
- lines.push(` case "$code" in`);
3851
- lines.push(` 302|401)`);
3852
- lines.push(` echo "backend auth-gate ok: HTTP $code on unauthenticated request to /api/health."`);
3853
- lines.push(` ;;`);
3854
- lines.push(` *)`);
3855
- lines.push(` echo "backend auth-gate regression: expected 302 or 401 on /api/health, got $code" >&2`);
3856
- lines.push(` curl -sSI "$URL" >&2 || true`);
3857
- lines.push(` exit 1`);
3858
- lines.push(` ;;`);
3859
- lines.push(` esac`);
3860
- lines.push("");
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
+ ];
3861
4155
  return lines.join(`
3862
4156
  `);
3863
4157
  }
3864
- function emitSecretStep(lines, binding) {
3865
- const name = binding.name;
3866
- const sourceNote = `env-file binding from launchpad.yaml — operator sets via \`launchpad secrets push\` or \`gh secret set\`.`;
3867
- lines.push(` - name: Push ${name} to the Worker`);
3868
- lines.push(` # ${sourceNote}`);
3869
- lines.push(` # Gated on the GH Actions secret existing so the first deploy`);
3870
- lines.push(` # on a fresh repo doesn't fail before secrets are provisioned.`);
3871
- lines.push(` # Skipping logs a ::warning:: so the gap shows in the deploy log.`);
3872
- lines.push(` env:`);
3873
- lines.push(` CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}`);
3874
- lines.push(` CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}`);
3875
- lines.push(` ${name}: \${{ secrets.${name} }}`);
3876
- lines.push(` run: |`);
3877
- lines.push(` if [ -z "\${${name}:-}" ]; then`);
3878
- lines.push(` echo "::warning::${name} GH Actions secret is unset — Worker will be missing this binding until the secret is provisioned + this workflow is rerun."`);
3879
- lines.push(` exit 0`);
3880
- lines.push(` fi`);
3881
- lines.push(` printf '%s' "$${name}" | bunx wrangler secret put ${name}`);
3882
- lines.push("");
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
+ ]);
3883
4170
  }
3884
4171
  // ../launchpad-engine/dist/per-app-workspace.js
3885
4172
  var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
@@ -4231,6 +4518,21 @@ async function bundleAndDeploy(args) {
4231
4518
  import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
4232
4519
  import { join as join9 } from "node:path";
4233
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
+
4234
4536
  // src/deploy/deployment-status.ts
4235
4537
  async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
4236
4538
  let raw;
@@ -4259,11 +4561,39 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
4259
4561
  return {
4260
4562
  slug: typeof raw.slug === "string" ? raw.slug : slug,
4261
4563
  supported: raw.supported,
4262
- liveDeployment: raw.liveDeployment ?? null,
4263
- lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null,
4564
+ liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
4565
+ lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
4264
4566
  ...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
4265
4567
  };
4266
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
+ }
4267
4597
 
4268
4598
  // src/deploy/commit-match-settle.ts
4269
4599
  var DEFAULT_REGISTRATION_BOUND_MS = 120000;
@@ -4429,21 +4759,6 @@ async function verifyServed(args, deps = { fetch }) {
4429
4759
  };
4430
4760
  }
4431
4761
 
4432
- // src/deploy/redact-diagnostic.ts
4433
- var MAX_DIAGNOSTIC_LEN = 600;
4434
- var SECRET_KV = /\b(secret|secrets?|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session)\b(\s*[=:]\s*|\s+)(\S+)/gi;
4435
- var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
4436
- function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
4437
- if (text === undefined || text === null)
4438
- return "";
4439
- let t = text.replace(SECRET_KV, (_m, key, sep2) => `${key}${sep2}***`);
4440
- t = t.replace(LONG_TOKEN, "***");
4441
- t = t.trim();
4442
- if (t.length > maxLen)
4443
- t = `${t.slice(0, maxLen - 1)}…`;
4444
- return t;
4445
- }
4446
-
4447
4762
  // src/commands/status.ts
4448
4763
  import { readFileSync as readFileSync7 } from "node:fs";
4449
4764
 
@@ -5304,7 +5619,29 @@ async function runStatus(args, io) {
5304
5619
  return mapBotError(e, parsed.slug, io);
5305
5620
  }
5306
5621
  if (lifecycle !== null && lifecycle.state !== "live") {
5307
- 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);
5308
5645
  return 0;
5309
5646
  }
5310
5647
  let localYaml = null;
@@ -5352,7 +5689,7 @@ async function runStatus(args, io) {
5352
5689
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5353
5690
  return mapBotError(e, parsed.slug, io);
5354
5691
  }
5355
- 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.`);
5356
5693
  }
5357
5694
  let standingExceptions = null;
5358
5695
  try {
@@ -5381,7 +5718,7 @@ async function runStatus(args, io) {
5381
5718
  ...deploymentKnown ? { deployment } : {},
5382
5719
  ...standingExceptions !== null ? { standingExceptions } : {}
5383
5720
  };
5384
- emit3(out, parsed.json, io);
5721
+ emit4(out, parsed.json, io);
5385
5722
  return 0;
5386
5723
  }
5387
5724
  if (local === null) {
@@ -5398,7 +5735,7 @@ async function runStatus(args, io) {
5398
5735
  ...deploymentKnown ? { deployment } : {},
5399
5736
  ...standingExceptions !== null ? { standingExceptions } : {}
5400
5737
  };
5401
- emit3(out, parsed.json, io);
5738
+ emit4(out, parsed.json, io);
5402
5739
  if (parsed.strict) {
5403
5740
  if (deployment?.liveDeployment?.buildStatus === "failure") {
5404
5741
  io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
@@ -5481,7 +5818,7 @@ async function runStatus(args, io) {
5481
5818
  ...accessDrift !== undefined ? { accessDrift } : {},
5482
5819
  ...groupIssues.length === 0 ? {} : { groupIssues }
5483
5820
  };
5484
- emit3(result, parsed.json, io);
5821
+ emit4(result, parsed.json, io);
5485
5822
  if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
5486
5823
  return 1;
5487
5824
  }
@@ -5677,7 +6014,7 @@ function lifecycleOutput(slug, lc) {
5677
6014
  driftDetails: []
5678
6015
  };
5679
6016
  }
5680
- function emit3(out, asJson, io) {
6017
+ function emit4(out, asJson, io) {
5681
6018
  if (asJson) {
5682
6019
  io.out(JSON.stringify(out, null, 2));
5683
6020
  return;
@@ -5689,12 +6026,16 @@ function emit3(out, asJson, io) {
5689
6026
  return;
5690
6027
  case "provisioning_failed":
5691
6028
  io.out(`${out.slug}: provisioning FAILED at stage ${out.stage ?? "unknown"}` + (out.failedReason ? `: ${out.failedReason}` : ""));
5692
- 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)) {
5693
6033
  io.out(` the app may already be live — run \`launchpad recover ${out.slug}\` to reconcile the record;`);
5694
6034
  io.out(` if it is not serving, the same command starts the stage-dependent recovery.`);
5695
6035
  } else {
5696
6036
  io.out(` run \`launchpad recover ${out.slug}\` to start the stage-dependent recovery.`);
5697
6037
  }
6038
+ surfaceFailedLifecycleDeployment(out, io);
5698
6039
  return;
5699
6040
  case "destroying":
5700
6041
  io.out(`${out.slug}: destroying…`);
@@ -5791,6 +6132,20 @@ function triggerLabel(trigger) {
5791
6132
  return "launchpad deploy (bot)";
5792
6133
  return "an unknown mechanism";
5793
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
+ }
5794
6149
  function surfaceDeployment(out, io) {
5795
6150
  const dep = out.deployment;
5796
6151
  if (dep === undefined || dep === null || !dep.supported)
@@ -5807,15 +6162,15 @@ function surfaceDeployment(out, io) {
5807
6162
  const via = triggerLabel(live.trigger);
5808
6163
  switch (live.buildStatus) {
5809
6164
  case "success":
5810
- 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}]`);
5811
6166
  return;
5812
6167
  case "in_progress":
5813
- 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}]`);
5814
6169
  io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
5815
6170
  return;
5816
6171
  case "failure": {
5817
6172
  const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
5818
- 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}]`);
5819
6174
  if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
5820
6175
  io.out(" build log (excerpt):");
5821
6176
  for (const line of live.logExcerpt.split(`
@@ -7718,6 +8073,81 @@ function handleNetworkError(e, io, slug, verb) {
7718
8073
  return 1;
7719
8074
  }
7720
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
+
7721
8151
  // src/commands/deploy.ts
7722
8152
  var deployCommand = {
7723
8153
  name: "deploy",
@@ -7904,7 +8334,7 @@ async function runDeploy(args, io) {
7904
8334
  io.err(`launchpad deploy: ${e.message}`);
7905
8335
  return 1;
7906
8336
  }
7907
- io.err(`launchpad deploy failed: ${describe15(e)}`);
8337
+ io.err(`launchpad deploy failed: ${describe16(e)}`);
7908
8338
  return 1;
7909
8339
  }
7910
8340
  }
@@ -7975,7 +8405,7 @@ function formatBytes2(n) {
7975
8405
  return `${(n / 1024).toFixed(1)}KB`;
7976
8406
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
7977
8407
  }
7978
- function describe15(e) {
8408
+ function describe16(e) {
7979
8409
  return e instanceof Error ? e.message : String(e);
7980
8410
  }
7981
8411
  function surfaceDeployExtras(body, io, slug, allowStale = false) {
@@ -8047,11 +8477,11 @@ function surfaceStaleBlock(body, io) {
8047
8477
  }
8048
8478
  var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
8049
8479
  function withTimeout(p, ms) {
8050
- return new Promise((resolve9, reject) => {
8480
+ return new Promise((resolve10, reject) => {
8051
8481
  const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
8052
8482
  p.then((v) => {
8053
8483
  clearTimeout(timer);
8054
- resolve9(v);
8484
+ resolve10(v);
8055
8485
  }, (e) => {
8056
8486
  clearTimeout(timer);
8057
8487
  reject(e instanceof Error ? e : new Error(String(e)));
@@ -8098,7 +8528,7 @@ async function advanceBaseShaMarker(cwd, sha, io) {
8098
8528
  return;
8099
8529
  await writeBaseShaMarker(cwd, sha);
8100
8530
  } catch (e) {
8101
- io.err(`warning: couldn't update the local base-sha marker (${describe15(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
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.`);
8102
8532
  }
8103
8533
  }
8104
8534
  async function runModelADeploy(args) {
@@ -8113,12 +8543,30 @@ async function runModelADeploy(args) {
8113
8543
  return 64;
8114
8544
  }
8115
8545
  slug = metaSlug;
8116
- const dtype = manifestObj?.deployment?.type;
8117
- if (dtype === "static" || dtype === "react" || dtype === "react+api" || dtype === "container") {
8118
- 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
+ }
8119
8567
  }
8120
8568
  } catch (e) {
8121
- io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe15(e)}`);
8569
+ io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe16(e)}`);
8122
8570
  return 1;
8123
8571
  }
8124
8572
  const noVerify = args.argv.includes("--no-verify");
@@ -8148,7 +8596,7 @@ async function runModelADeploy(args) {
8148
8596
  try {
8149
8597
  cfg = loadConfig();
8150
8598
  } catch (e) {
8151
- io.err(`launchpad deploy: ${describe15(e)}`);
8599
+ io.err(`launchpad deploy: ${describe16(e)}`);
8152
8600
  return 1;
8153
8601
  }
8154
8602
  try {
@@ -8198,7 +8646,7 @@ async function runModelADeploy(args) {
8198
8646
  io.err(" run `launchpad login` to refresh your session.");
8199
8647
  return 1;
8200
8648
  }
8201
- io.err(`launchpad deploy: unexpected error: ${describe15(e)}`);
8649
+ io.err(`launchpad deploy: unexpected error: ${describe16(e)}`);
8202
8650
  return 1;
8203
8651
  }
8204
8652
  switch (result.kind) {
@@ -8673,7 +9121,7 @@ async function runEnvvars(args, io) {
8673
9121
  io.err(`launchpad envvars: ${e.message}`);
8674
9122
  return 1;
8675
9123
  }
8676
- io.err(`launchpad envvars failed: ${describe16(e)}`);
9124
+ io.err(`launchpad envvars failed: ${describe17(e)}`);
8677
9125
  return 1;
8678
9126
  }
8679
9127
  }
@@ -8770,16 +9218,16 @@ function renderList(envVars, io) {
8770
9218
  io.out(fmt(row));
8771
9219
  }
8772
9220
  }
8773
- function describe16(e) {
9221
+ function describe17(e) {
8774
9222
  return e instanceof Error ? e.message : String(e);
8775
9223
  }
8776
9224
 
8777
9225
  // src/commands/generate.ts
8778
9226
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
8779
- import { dirname as dirname6, resolve as resolve9, relative as relative3 } from "node:path";
9227
+ import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
8780
9228
  var generateCommand = {
8781
9229
  name: "generate",
8782
- summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
9230
+ summary: "emit derived runtime artefacts from launchpad.yaml",
8783
9231
  run: runGenerate
8784
9232
  };
8785
9233
  async function runGenerate(args, io) {
@@ -8789,33 +9237,50 @@ async function runGenerate(args, io) {
8789
9237
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
8790
9238
  return 64;
8791
9239
  }
8792
- const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
9240
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
8793
9241
  const result = loadManifest(manifestPath);
8794
9242
  if (result.kind !== "ok") {
8795
9243
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
8796
9244
  }
8797
- const appRoot = dirname6(manifestPath);
8798
- const wranglerPath = resolve9(appRoot, "container", "wrangler.toml");
8799
- const workflowPath = resolve9(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");
8800
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;
8801
9261
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
8802
9262
  if (flags.dryRun) {
8803
9263
  return renderDryRun([
8804
9264
  { artefact: "wrangler.toml", path: wranglerPath, result: wranglerOut },
9265
+ { artefact: "worker/index.ts", path: workerPath, result: workerOut },
8805
9266
  { artefact: "deploy.yml", path: workflowPath, result: workflowOut }
8806
9267
  ], appRoot, flags.json, io);
8807
9268
  }
8808
9269
  const reports = [
8809
9270
  applyOne("wrangler.toml", wranglerPath, wranglerOut, flags.force),
9271
+ applyOne("worker/index.ts", workerPath, workerOut, flags.force),
8810
9272
  applyOne("deploy.yml", workflowPath, workflowOut, flags.force)
8811
9273
  ];
8812
9274
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
8813
9275
  }
8814
9276
  function applyOne(artefact, path10, out, force) {
8815
- if (out.kind === "not-applicable") {
9277
+ if (out.kind === "not-applicable" || out.kind === "unsupported") {
8816
9278
  return {
8817
9279
  artefact,
8818
- 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
+ },
8819
9284
  warnings: []
8820
9285
  };
8821
9286
  }
@@ -8838,7 +9303,7 @@ function applyOne(artefact, path10, out, force) {
8838
9303
  };
8839
9304
  }
8840
9305
  try {
8841
- mkdirSync2(dirname6(path10), { recursive: true });
9306
+ mkdirSync2(dirname7(path10), { recursive: true });
8842
9307
  writeFileSync2(path10, out.content, "utf8");
8843
9308
  return {
8844
9309
  artefact,
@@ -8874,12 +9339,12 @@ function readIfExists(path10) {
8874
9339
  function renderDryRun(inputs, appRoot, json, io) {
8875
9340
  if (json) {
8876
9341
  const files = inputs.map((i) => {
8877
- const previewPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
8878
- 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") {
8879
9344
  return {
8880
9345
  artefact: i.artefact,
8881
9346
  kind: "not-applicable",
8882
- reason: i.result.reason,
9347
+ reason: i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason,
8883
9348
  ...i.preview ? { preview: true, destination: i.preview.destination } : {}
8884
9349
  };
8885
9350
  }
@@ -8896,12 +9361,13 @@ function renderDryRun(inputs, appRoot, json, io) {
8896
9361
  return 0;
8897
9362
  }
8898
9363
  for (const i of inputs) {
8899
- if (i.result.kind === "not-applicable") {
8900
- 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}`);
8901
9367
  io.out("");
8902
9368
  continue;
8903
9369
  }
8904
- const headerPath = i.preview === undefined ? relative3(appRoot, i.path) : i.path;
9370
+ const headerPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
8905
9371
  const header = i.preview === undefined ? `--- ${headerPath} ---` : `--- [preview, destination = ${i.preview.destination}] ${headerPath} ---`;
8906
9372
  io.out(header);
8907
9373
  io.out(i.result.content);
@@ -8920,20 +9386,20 @@ function renderApplyHuman(reports, appRoot, io) {
8920
9386
  const a = r.action;
8921
9387
  switch (a.kind) {
8922
9388
  case "written":
8923
- 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)}`);
8924
9390
  break;
8925
9391
  case "unchanged":
8926
- io.out(`= ${r.artefact} — unchanged (${relative3(appRoot, a.path)})`);
9392
+ io.out(`= ${r.artefact} — unchanged (${relative4(appRoot, a.path)})`);
8927
9393
  break;
8928
9394
  case "would-overwrite":
8929
- 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`);
8930
9396
  wouldOverwrite++;
8931
9397
  break;
8932
9398
  case "not-applicable":
8933
9399
  io.out(`- ${r.artefact} — skipped (${a.reason})`);
8934
9400
  break;
8935
9401
  case "write-error":
8936
- io.err(`✗ ${r.artefact} — ${a.message} (${relative3(appRoot, a.path)})`);
9402
+ io.err(`✗ ${r.artefact} — ${a.message} (${relative4(appRoot, a.path)})`);
8937
9403
  writeError++;
8938
9404
  break;
8939
9405
  }
@@ -8952,17 +9418,17 @@ function renderApplyJson(reports, appRoot, io) {
8952
9418
  const base = { artefact: r.artefact, warnings: r.warnings };
8953
9419
  switch (a.kind) {
8954
9420
  case "written":
8955
- 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 };
8956
9422
  case "unchanged":
8957
9423
  case "would-overwrite":
8958
- return { ...base, kind: a.kind, path: relative3(appRoot, a.path) };
9424
+ return { ...base, kind: a.kind, path: relative4(appRoot, a.path) };
8959
9425
  case "not-applicable":
8960
9426
  return { ...base, kind: a.kind, reason: a.reason };
8961
9427
  case "write-error":
8962
9428
  return {
8963
9429
  ...base,
8964
9430
  kind: a.kind,
8965
- path: relative3(appRoot, a.path),
9431
+ path: relative4(appRoot, a.path),
8966
9432
  message: a.message
8967
9433
  };
8968
9434
  }
@@ -9086,13 +9552,13 @@ function decodeJwtPayload(token) {
9086
9552
  try {
9087
9553
  json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
9088
9554
  } catch (e) {
9089
- throw new JwtParseError(`could not base64-decode JWT payload: ${describe17(e)}`);
9555
+ throw new JwtParseError(`could not base64-decode JWT payload: ${describe18(e)}`);
9090
9556
  }
9091
9557
  let parsed;
9092
9558
  try {
9093
9559
  parsed = JSON.parse(json);
9094
9560
  } catch (e) {
9095
- throw new JwtParseError(`JWT payload is not JSON: ${describe17(e)}`);
9561
+ throw new JwtParseError(`JWT payload is not JSON: ${describe18(e)}`);
9096
9562
  }
9097
9563
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9098
9564
  throw new JwtParseError(`JWT payload is not an object`);
@@ -9103,7 +9569,7 @@ function b64UrlToB64(s) {
9103
9569
  const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
9104
9570
  return padded.replace(/-/g, "+").replace(/_/g, "/");
9105
9571
  }
9106
- function describe17(e) {
9572
+ function describe18(e) {
9107
9573
  return e instanceof Error ? e.message : String(e);
9108
9574
  }
9109
9575
 
@@ -9130,7 +9596,7 @@ async function runGroupsWhoami(args, io) {
9130
9596
  try {
9131
9597
  payload = decodeJwtPayload(session.accessToken);
9132
9598
  } catch (e) {
9133
- const message = e instanceof JwtParseError ? e.message : describe18(e);
9599
+ const message = e instanceof JwtParseError ? e.message : describe19(e);
9134
9600
  if (json) {
9135
9601
  io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
9136
9602
  } else {
@@ -9211,7 +9677,7 @@ function parseFlags2(args) {
9211
9677
  }
9212
9678
  return { kind: "ok", json };
9213
9679
  }
9214
- function describe18(e) {
9680
+ function describe19(e) {
9215
9681
  return e instanceof Error ? e.message : String(e);
9216
9682
  }
9217
9683
 
@@ -9314,7 +9780,7 @@ async function loadGroups(io, refresh, json) {
9314
9780
  }
9315
9781
  return { kind: "error", code: 3 };
9316
9782
  }
9317
- renderFetchError(io, describe19(e), json);
9783
+ renderFetchError(io, describe20(e), json);
9318
9784
  return { kind: "error", code: 2 };
9319
9785
  }
9320
9786
  }
@@ -9351,7 +9817,7 @@ async function lookupTenantWide(key) {
9351
9817
  return { kind: "unauthenticated", message: e.message };
9352
9818
  if (e instanceof ForbiddenError)
9353
9819
  return { kind: "forbidden", message: e.message };
9354
- return { kind: "error", message: describe19(e) };
9820
+ return { kind: "error", message: describe20(e) };
9355
9821
  }
9356
9822
  }
9357
9823
  function renderLookupFailure(io, json, hit) {
@@ -9497,9 +9963,9 @@ async function runShow(args, io) {
9497
9963
  }
9498
9964
  const key = parsed.flags.positional[0];
9499
9965
  const hit = await lookupTenantWide(key);
9500
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9501
- if (failure !== null)
9502
- return failure;
9966
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
9967
+ if (failure2 !== null)
9968
+ return failure2;
9503
9969
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9504
9970
  return 2;
9505
9971
  }
@@ -9546,9 +10012,9 @@ async function runResolve(args, io) {
9546
10012
  }
9547
10013
  const key = parsed.flags.positional[0];
9548
10014
  const hit = await lookupTenantWide(key);
9549
- const failure = renderLookupFailure(io, parsed.flags.json, hit);
9550
- if (failure !== null)
9551
- return failure;
10015
+ const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
10016
+ if (failure2 !== null)
10017
+ return failure2;
9552
10018
  if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
9553
10019
  return 2;
9554
10020
  }
@@ -9595,23 +10061,23 @@ function renderTable2(io, groups) {
9595
10061
  for (const r of rows)
9596
10062
  io.out(fmt(r));
9597
10063
  }
9598
- function describe19(e) {
10064
+ function describe20(e) {
9599
10065
  return e instanceof Error ? e.message : String(e);
9600
10066
  }
9601
10067
 
9602
10068
  // src/commands/init.ts
9603
10069
  import { createInterface } from "node:readline/promises";
9604
10070
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9605
- import { dirname as dirname7, resolve as resolve10 } from "node:path";
10071
+ import { dirname as dirname8, resolve as resolve11 } from "node:path";
9606
10072
  import { stringify as yamlStringify } from "yaml";
9607
10073
 
9608
10074
  // src/detect/index.ts
9609
10075
  import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
9610
- import { join as join12 } from "node:path";
10076
+ import { join as join13 } from "node:path";
9611
10077
  function detectAppShape(cwd) {
9612
- const hasPackageJson = existsSync9(join12(cwd, "package.json"));
9613
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join12(cwd, file)));
9614
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join12(cwd, n)));
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)));
9615
10081
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
9616
10082
  return {
9617
10083
  kind: "not-applicable",
@@ -9657,7 +10123,7 @@ var LOCKFILES = [
9657
10123
  { file: "yarn.lock", pm: "yarn" }
9658
10124
  ];
9659
10125
  function detectPackageManager(cwd) {
9660
- const present = LOCKFILES.filter(({ file }) => existsSync9(join12(cwd, file)));
10126
+ const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
9661
10127
  if (present.length === 0) {
9662
10128
  return {
9663
10129
  kind: "ambiguous",
@@ -9679,7 +10145,7 @@ function detectPackageManager(cwd) {
9679
10145
  }
9680
10146
  function detectVitePresence(cwd) {
9681
10147
  for (const name of VITE_CONFIG_NAMES) {
9682
- const p = join12(cwd, name);
10148
+ const p = join13(cwd, name);
9683
10149
  if (existsSync9(p)) {
9684
10150
  return { kind: "ok", value: { path: p } };
9685
10151
  }
@@ -9690,7 +10156,7 @@ function detectVitePresence(cwd) {
9690
10156
  };
9691
10157
  }
9692
10158
  function detectAppType(cwd) {
9693
- const fnDir = join12(cwd, "functions");
10159
+ const fnDir = join13(cwd, "functions");
9694
10160
  let hasFunctionsDir = false;
9695
10161
  if (existsSync9(fnDir)) {
9696
10162
  try {
@@ -9702,7 +10168,7 @@ function detectAppType(cwd) {
9702
10168
  return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
9703
10169
  }
9704
10170
  function detectBuildCommand(cwd, pm) {
9705
- const pkgJsonPath = join12(cwd, "package.json");
10171
+ const pkgJsonPath = join13(cwd, "package.json");
9706
10172
  if (!existsSync9(pkgJsonPath)) {
9707
10173
  return {
9708
10174
  kind: "ambiguous",
@@ -9775,7 +10241,7 @@ async function runInit(args, io, prompt) {
9775
10241
  return 64;
9776
10242
  }
9777
10243
  const { inputs, options } = parsed;
9778
- const outPath = resolve10(process.cwd(), options.out);
10244
+ const outPath = resolve11(process.cwd(), options.out);
9779
10245
  if (existsSync10(outPath) && !options.force) {
9780
10246
  io.err(`launchpad init: ${outPath} already exists`);
9781
10247
  io.err("Pass --force to overwrite.");
@@ -9823,7 +10289,7 @@ async function runInit(args, io, prompt) {
9823
10289
  }
9824
10290
  io.out(`✓ wrote ${outPath}`);
9825
10291
  if (options.gitignore) {
9826
- const gitignorePath = resolve10(process.cwd(), ".gitignore");
10292
+ const gitignorePath = resolve11(process.cwd(), ".gitignore");
9827
10293
  try {
9828
10294
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
9829
10295
  if (changed.length > 0) {
@@ -9838,8 +10304,8 @@ async function runInit(args, io, prompt) {
9838
10304
  }
9839
10305
  }
9840
10306
  if (fillResult.inputs.type === "static") {
9841
- const serveDir = resolve10(dirname7(outPath), STATIC_SERVE_DIR);
9842
- const indexPath = resolve10(serveDir, "index.html");
10307
+ const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
10308
+ const indexPath = resolve11(serveDir, "index.html");
9843
10309
  if (!existsSync10(indexPath)) {
9844
10310
  try {
9845
10311
  mkdirSync3(serveDir, { recursive: true });
@@ -10306,13 +10772,13 @@ async function runLogin(args, io, deps) {
10306
10772
  io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
10307
10773
  return 0;
10308
10774
  } catch (e) {
10309
- io.err(`Gateway login failed: ${describe20(e)}`);
10775
+ io.err(`Gateway login failed: ${describe21(e)}`);
10310
10776
  io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
10311
10777
  io.err("");
10312
10778
  return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
10313
10779
  }
10314
10780
  } catch (e) {
10315
- io.err(`launchpad login failed: ${describe20(e)}`);
10781
+ io.err(`launchpad login failed: ${describe21(e)}`);
10316
10782
  return 1;
10317
10783
  }
10318
10784
  }
@@ -10354,7 +10820,7 @@ function printLoginHelp(io) {
10354
10820
  io.out(" 0 signed in / session stored");
10355
10821
  io.out(" 1 login failed (network, browser, or gateway error)");
10356
10822
  }
10357
- function describe20(e) {
10823
+ function describe21(e) {
10358
10824
  return e instanceof Error ? e.message : String(e);
10359
10825
  }
10360
10826
 
@@ -10391,14 +10857,14 @@ async function runLogout(args, io, deps) {
10391
10857
  });
10392
10858
  io.out("Server-side session revoked.");
10393
10859
  } catch (e) {
10394
- io.err(`warning: could not revoke the session server-side (${describe21(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
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.`);
10395
10861
  }
10396
10862
  }
10397
10863
  const had = await clearSession(cfg.sessionPath);
10398
10864
  io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
10399
10865
  return 0;
10400
10866
  } catch (e) {
10401
- io.err(`launchpad logout failed: ${describe21(e)}`);
10867
+ io.err(`launchpad logout failed: ${describe22(e)}`);
10402
10868
  return 1;
10403
10869
  }
10404
10870
  }
@@ -10418,7 +10884,7 @@ function printLogoutHelp(io) {
10418
10884
  io.out(" 0 logged out (or already logged out)");
10419
10885
  io.out(" 1 could not clear the local session (file IO / config error)");
10420
10886
  }
10421
- function describe21(e) {
10887
+ function describe22(e) {
10422
10888
  return e instanceof Error ? e.message : String(e);
10423
10889
  }
10424
10890
 
@@ -10488,7 +10954,7 @@ async function runLogs(args, io) {
10488
10954
  io.err(`launchpad logs: ${e.message}`);
10489
10955
  return 1;
10490
10956
  }
10491
- io.err(`launchpad logs failed: ${describe22(e)}`);
10957
+ io.err(`launchpad logs failed: ${describe23(e)}`);
10492
10958
  return 1;
10493
10959
  }
10494
10960
  }
@@ -10568,7 +11034,7 @@ function formatRelative2(iso) {
10568
11034
  return `${hr}h ago`;
10569
11035
  return `${Math.floor(hr / 24)}d ago`;
10570
11036
  }
10571
- function describe22(e) {
11037
+ function describe23(e) {
10572
11038
  return e instanceof Error ? e.message : String(e);
10573
11039
  }
10574
11040
 
@@ -10617,7 +11083,7 @@ async function runMerge(args, io) {
10617
11083
  try {
10618
11084
  body = await res.json();
10619
11085
  } catch (e) {
10620
- 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)}`);
10621
11087
  return 1;
10622
11088
  }
10623
11089
  io.out("");
@@ -10657,7 +11123,7 @@ async function runMerge(args, io) {
10657
11123
  io.err(`launchpad merge: ${e.message}`);
10658
11124
  return 1;
10659
11125
  }
10660
- io.err(`launchpad merge failed: ${describe23(e)}`);
11126
+ io.err(`launchpad merge failed: ${describe24(e)}`);
10661
11127
  return 1;
10662
11128
  }
10663
11129
  }
@@ -10756,12 +11222,12 @@ function renderBotError(status, env, io, prNumber = null) {
10756
11222
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
10757
11223
  }
10758
11224
  }
10759
- function describe23(e) {
11225
+ function describe24(e) {
10760
11226
  return e instanceof Error ? e.message : String(e);
10761
11227
  }
10762
11228
 
10763
11229
  // src/commands/plan.ts
10764
- import { resolve as resolve11 } from "node:path";
11230
+ import { resolve as resolve12 } from "node:path";
10765
11231
  var planCommand = {
10766
11232
  name: "plan",
10767
11233
  summary: "summarise what the manifest would deploy (offline)",
@@ -10774,8 +11240,19 @@ async function runPlan(args, io) {
10774
11240
  io.err("Usage: launchpad plan [--file <path>] [--json]");
10775
11241
  return 64;
10776
11242
  }
10777
- const manifestPath = resolve11(process.cwd(), flags.file ?? "launchpad.yaml");
11243
+ const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
10778
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
+ }
10779
11256
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
10780
11257
  }
10781
11258
  function renderJson(result, io) {
@@ -11008,7 +11485,7 @@ async function runDestroy(args, io, prompt, isTty) {
11008
11485
  io.err(`launchpad destroy: ${e.message}`);
11009
11486
  return 1;
11010
11487
  }
11011
- io.err(`launchpad destroy failed: ${describe24(e)}`);
11488
+ io.err(`launchpad destroy failed: ${describe25(e)}`);
11012
11489
  return 1;
11013
11490
  }
11014
11491
  }
@@ -11208,7 +11685,7 @@ function printUsage4(io) {
11208
11685
  ].join(`
11209
11686
  `));
11210
11687
  }
11211
- function describe24(e) {
11688
+ function describe25(e) {
11212
11689
  return e instanceof Error ? e.message : String(e);
11213
11690
  }
11214
11691
 
@@ -11340,22 +11817,22 @@ async function treesEqual(cwd, tracked, headDir, headPaths) {
11340
11817
  if (tracked.length !== headPaths.length)
11341
11818
  return false;
11342
11819
  const expected = new Set(headPaths);
11343
- for (const relative5 of tracked) {
11344
- if (!expected.has(relative5))
11820
+ for (const relative6 of tracked) {
11821
+ if (!expected.has(relative6))
11345
11822
  return false;
11346
- 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)))) {
11347
11824
  return false;
11348
11825
  }
11349
11826
  }
11350
11827
  return true;
11351
11828
  }
11352
- async function listFiles(root, relative5 = "") {
11829
+ async function listFiles(root, relative6 = "") {
11353
11830
  const files = [];
11354
- const entries = await fs6.readdir(path12.resolve(root, relative5), {
11831
+ const entries = await fs6.readdir(path12.resolve(root, relative6), {
11355
11832
  withFileTypes: true
11356
11833
  });
11357
11834
  for (const entry of entries) {
11358
- const child = relative5.length === 0 ? entry.name : `${relative5}/${entry.name}`;
11835
+ const child = relative6.length === 0 ? entry.name : `${relative6}/${entry.name}`;
11359
11836
  if (entry.isDirectory())
11360
11837
  files.push(...await listFiles(root, child));
11361
11838
  else if (entry.isFile())
@@ -11364,12 +11841,12 @@ async function listFiles(root, relative5 = "") {
11364
11841
  return files.sort();
11365
11842
  }
11366
11843
  function gitOutput(cwd, args) {
11367
- return new Promise((resolve13, reject) => {
11844
+ return new Promise((resolve14, reject) => {
11368
11845
  execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
11369
11846
  if (error !== null)
11370
11847
  reject(error);
11371
11848
  else
11372
- resolve13(stdout);
11849
+ resolve14(stdout);
11373
11850
  });
11374
11851
  });
11375
11852
  }
@@ -11378,7 +11855,7 @@ function splitNull(value) {
11378
11855
  }
11379
11856
  async function reconcile(args) {
11380
11857
  const removals = await cleanAncestorRemovals(args);
11381
- 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)));
11382
11859
  const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
11383
11860
  if (conflicts.length > 0) {
11384
11861
  const report = {
@@ -11422,55 +11899,55 @@ async function reconcile(args) {
11422
11899
  }
11423
11900
  async function cleanAncestorRemovals(args) {
11424
11901
  const removals = new Set;
11425
- for (const relative5 of args.compare.deleted) {
11426
- assertSafePath(relative5);
11427
- 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);
11428
11905
  if (checked.kind === "blocked")
11429
11906
  continue;
11430
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
11907
+ const base = await readEntry(path12.resolve(args.baseDir, relative6));
11431
11908
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11432
11909
  if (local.kind !== "missing" && cleanLocal(base, local))
11433
- removals.add(relative5);
11910
+ removals.add(relative6);
11434
11911
  }
11435
11912
  return removals;
11436
11913
  }
11437
- async function planPath(args, relative5, removals) {
11438
- assertSafePath(relative5);
11439
- const checked = await checkParents(args.cwd, relative5, removals);
11440
- const base = await readEntry(path12.resolve(args.baseDir, relative5));
11441
- const upstreamDeleted = args.compare.deleted.includes(relative5);
11442
- const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative5));
11443
- const unsupported = (args.compare.unsupported ?? []).includes(relative5);
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);
11444
11921
  if (checked.kind === "blocked") {
11445
- return conflictPlan(relative5, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11922
+ return conflictPlan(relative6, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11446
11923
  }
11447
11924
  const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11448
- if (unsupported) {
11449
- return conflictPlan(relative5, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11925
+ if (unsupported2) {
11926
+ return conflictPlan(relative6, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11450
11927
  }
11451
11928
  if (alreadyResolved(local, head, upstreamDeleted)) {
11452
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: false };
11929
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: false };
11453
11930
  }
11454
11931
  if (unsupportedHead(head, upstreamDeleted)) {
11455
- 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");
11456
11933
  }
11457
11934
  if (cleanLocal(base, local)) {
11458
- return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: true };
11935
+ return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: true };
11459
11936
  }
11460
- 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));
11461
11938
  }
11462
- function conflictPlan(relative5, target, local, head, upstreamDeleted, reason) {
11939
+ function conflictPlan(relative6, target, local, head, upstreamDeleted, reason) {
11463
11940
  const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
11464
- const digest = createHash3("sha256").update(relative5).digest("hex");
11941
+ const digest = createHash3("sha256").update(relative6).digest("hex");
11465
11942
  return {
11466
- relative: relative5,
11943
+ relative: relative6,
11467
11944
  target,
11468
11945
  local,
11469
11946
  head,
11470
11947
  upstreamDeleted,
11471
11948
  apply: false,
11472
11949
  conflict: {
11473
- path: relative5,
11950
+ path: relative6,
11474
11951
  reason,
11475
11952
  artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
11476
11953
  upstreamDeleted
@@ -11571,13 +12048,13 @@ function equal(left, right) {
11571
12048
  return false;
11572
12049
  return left.bytes.every((byte, index) => byte === right.bytes[index]);
11573
12050
  }
11574
- async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11575
- 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);
11576
12053
  if (checked.kind === "blocked")
11577
12054
  throw new Error(checked.reason);
11578
12055
  const file = checked.target;
11579
12056
  await fs6.mkdir(path12.dirname(file), { recursive: true });
11580
- const rechecked = await checkParents(cwd, relative5, new Set, allowMetadata);
12057
+ const rechecked = await checkParents(cwd, relative6, new Set, allowMetadata);
11581
12058
  if (rechecked.kind === "blocked")
11582
12059
  throw new Error(rechecked.reason);
11583
12060
  const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
@@ -11592,8 +12069,8 @@ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11592
12069
  throw error;
11593
12070
  }
11594
12071
  }
11595
- async function writeJsonAtomic(cwd, relative5, value) {
11596
- 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)}
11597
12074
  `), 384, true);
11598
12075
  }
11599
12076
  async function resetConflictArtifacts(cwd) {
@@ -11609,37 +12086,37 @@ async function cleanupConflictState(cwd) {
11609
12086
  throw new Error(report.reason);
11610
12087
  await fs6.rm(report.target, { force: true });
11611
12088
  }
11612
- function assertSafePath(relative5) {
11613
- if (!isSafeSourcePath(relative5)) {
11614
- 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}`);
11615
12092
  }
11616
12093
  }
11617
- function isSafeSourcePath(relative5) {
11618
- return isSafeRelativePath(relative5) && !relative5.startsWith(`${MARKER_DIR}/`);
12094
+ function isSafeSourcePath(relative6) {
12095
+ return isSafeRelativePath(relative6) && !relative6.startsWith(`${MARKER_DIR}/`);
11619
12096
  }
11620
- function isSafeRelativePath(relative5) {
11621
- const normalized = path12.posix.normalize(relative5);
11622
- return relative5.length > 0 && !relative5.includes("\\") && !path12.isAbsolute(relative5) && normalized === relative5 && normalized !== ".." && !normalized.startsWith("../");
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("../");
11623
12100
  }
11624
- function resolveInsideClone(cwd, relative5) {
12101
+ function resolveInsideClone(cwd, relative6) {
11625
12102
  const root = path12.resolve(cwd);
11626
- const target = path12.resolve(root, relative5);
12103
+ const target = path12.resolve(root, relative6);
11627
12104
  const fromRoot = path12.relative(root, target);
11628
12105
  if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
11629
- throw new Error(`unsafe source path returned by bot: ${relative5}`);
12106
+ throw new Error(`unsafe source path returned by bot: ${relative6}`);
11630
12107
  }
11631
12108
  return target;
11632
12109
  }
11633
- async function checkParents(cwd, relative5, projectedRemovals, allowMetadata = false) {
12110
+ async function checkParents(cwd, relative6, projectedRemovals, allowMetadata = false) {
11634
12111
  if (allowMetadata) {
11635
- if (!isSafeRelativePath(relative5)) {
11636
- throw new Error(`unsafe metadata path: ${relative5}`);
12112
+ if (!isSafeRelativePath(relative6)) {
12113
+ throw new Error(`unsafe metadata path: ${relative6}`);
11637
12114
  }
11638
12115
  } else {
11639
- assertSafePath(relative5);
12116
+ assertSafePath(relative6);
11640
12117
  }
11641
- const target = resolveInsideClone(cwd, relative5);
11642
- const components = relative5.split("/");
12118
+ const target = resolveInsideClone(cwd, relative6);
12119
+ const components = relative6.split("/");
11643
12120
  let current = path12.resolve(cwd);
11644
12121
  for (let index = 0;index < components.length - 1; index += 1) {
11645
12122
  current = path12.join(current, components[index]);
@@ -11778,7 +12255,7 @@ async function runPull(args, io) {
11778
12255
  io.err(`launchpad pull: ${e.message}`);
11779
12256
  return 1;
11780
12257
  }
11781
- io.err(`launchpad pull failed: ${describe25(e)}`);
12258
+ io.err(`launchpad pull failed: ${describe26(e)}`);
11782
12259
  return 1;
11783
12260
  }
11784
12261
  }
@@ -11886,7 +12363,7 @@ function printUsage5(io) {
11886
12363
  ].join(`
11887
12364
  `));
11888
12365
  }
11889
- function describe25(e) {
12366
+ function describe26(e) {
11890
12367
  return e instanceof Error ? e.message : String(e);
11891
12368
  }
11892
12369
 
@@ -12221,7 +12698,7 @@ async function runReview(args, io) {
12221
12698
  io.err(`launchpad review: ${e.message}`);
12222
12699
  return 1;
12223
12700
  }
12224
- io.err(`launchpad review failed: ${describe26(e)}`);
12701
+ io.err(`launchpad review failed: ${describe27(e)}`);
12225
12702
  return 1;
12226
12703
  }
12227
12704
  }
@@ -12299,7 +12776,7 @@ function severityRank(s) {
12299
12776
  return 3;
12300
12777
  }
12301
12778
  }
12302
- function describe26(e) {
12779
+ function describe27(e) {
12303
12780
  return e instanceof Error ? e.message : String(e);
12304
12781
  }
12305
12782
 
@@ -12359,7 +12836,7 @@ async function runRollback(opts, io, deps = {}) {
12359
12836
  cwd: process.cwd()
12360
12837
  });
12361
12838
  } catch (e) {
12362
- 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)}`);
12363
12840
  return 2;
12364
12841
  }
12365
12842
  if (revParse.exitCode !== 0) {
@@ -12374,7 +12851,7 @@ async function runRollback(opts, io, deps = {}) {
12374
12851
  try {
12375
12852
  show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
12376
12853
  } catch (e) {
12377
- io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
12854
+ io.err(`launchpad rollback: git show failed to start: ${describe28(e)}`);
12378
12855
  return 2;
12379
12856
  }
12380
12857
  if (show.exitCode !== 0) {
@@ -12408,7 +12885,7 @@ async function runRollback(opts, io, deps = {}) {
12408
12885
  try {
12409
12886
  writeFileSync5(manifestPath, historicalYaml, "utf8");
12410
12887
  } catch (e) {
12411
- io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
12888
+ io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe28(e)}`);
12412
12889
  return 2;
12413
12890
  }
12414
12891
  io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
@@ -12490,7 +12967,7 @@ async function defaultPrompt4(question) {
12490
12967
  rl.close();
12491
12968
  }
12492
12969
  }
12493
- function describe27(e) {
12970
+ function describe28(e) {
12494
12971
  return e instanceof Error ? e.message : String(e);
12495
12972
  }
12496
12973
  function renderManifestError3(result, io) {
@@ -12595,7 +13072,7 @@ import { createInterface as createInterface4 } from "node:readline/promises";
12595
13072
  import { stdin as input2, stdout as output2 } from "node:process";
12596
13073
 
12597
13074
  // src/secrets/env-parse.ts
12598
- var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
13075
+ var ENV_NAME2 = /^[A-Z][A-Z0-9_]*$/;
12599
13076
  var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
12600
13077
  function parseEnv(text) {
12601
13078
  const entries = new Map;
@@ -12620,7 +13097,7 @@ function parseEnv(text) {
12620
13097
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
12621
13098
  value = value.slice(1, -1);
12622
13099
  }
12623
- if (!ENV_NAME.test(key)) {
13100
+ if (!ENV_NAME2.test(key)) {
12624
13101
  invalidNames.push(key);
12625
13102
  continue;
12626
13103
  }
@@ -12655,7 +13132,7 @@ async function runSecretsPush(opts, io, deps = {}) {
12655
13132
  try {
12656
13133
  envText = readFileSync16(envPath, "utf8");
12657
13134
  } catch (e) {
12658
- io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
13135
+ io.err(`launchpad secrets push: failed to read ${envPath}: ${describe29(e)}`);
12659
13136
  return 2;
12660
13137
  }
12661
13138
  const parsed = parseEnv(envText);
@@ -12839,10 +13316,10 @@ function mapPushError(e, slug, name, io) {
12839
13316
  io.err(`✗ secrets push ${name}: ${e.message}`);
12840
13317
  return 1;
12841
13318
  }
12842
- io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
13319
+ io.err(`✗ secrets push ${name} failed: ${describe29(e)}`);
12843
13320
  return 2;
12844
13321
  }
12845
- function describe28(e) {
13322
+ function describe29(e) {
12846
13323
  return e instanceof Error ? e.message : String(e);
12847
13324
  }
12848
13325
  function renderManifestError4(result, io) {
@@ -13232,7 +13709,7 @@ function setPushExit(e) {
13232
13709
 
13233
13710
  // src/commands/secrets-template.ts
13234
13711
  import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
13235
- import { resolve as resolve13 } from "node:path";
13712
+ import { resolve as resolve14 } from "node:path";
13236
13713
  async function runSecretsTemplate(args, io) {
13237
13714
  const flags = parseFlags4(args);
13238
13715
  if (flags.kind === "usage-error") {
@@ -13240,7 +13717,7 @@ async function runSecretsTemplate(args, io) {
13240
13717
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
13241
13718
  return 64;
13242
13719
  }
13243
- const manifestPath = resolve13(process.cwd(), flags.file ?? "launchpad.yaml");
13720
+ const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
13244
13721
  const result = loadManifest(manifestPath);
13245
13722
  const renderResult = renderManifest(result, io);
13246
13723
  if (renderResult.kind !== "ok") {
@@ -13254,7 +13731,7 @@ async function runSecretsTemplate(args, io) {
13254
13731
  }
13255
13732
  return 0;
13256
13733
  }
13257
- const outPath = resolve13(process.cwd(), flags.out);
13734
+ const outPath = resolve14(process.cwd(), flags.out);
13258
13735
  if (existsSync14(outPath) && !flags.force) {
13259
13736
  io.err(`launchpad secrets template: ${outPath} already exists`);
13260
13737
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
@@ -13551,16 +14028,16 @@ function printHelp3(io) {
13551
14028
 
13552
14029
  // src/commands/skills.ts
13553
14030
  import { fileURLToPath } from "node:url";
13554
- import { dirname as dirname9, join as join15, resolve as resolve14 } from "node:path";
14031
+ import { dirname as dirname10, join as join16, resolve as resolve15 } from "node:path";
13555
14032
  import { promises as fs7, existsSync as existsSync15 } from "node:fs";
13556
14033
 
13557
14034
  // src/skills-bundle.ts
13558
14035
  import { homedir as homedir2 } from "node:os";
13559
- import { join as join14 } from "node:path";
14036
+ import { join as join15 } from "node:path";
13560
14037
  import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
13561
14038
  var SKILL_PREFIX = "launchpad-";
13562
14039
  function skillsTargetDir() {
13563
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
14040
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
13564
14041
  }
13565
14042
  function readInstalledSkills() {
13566
14043
  try {
@@ -13570,7 +14047,7 @@ function readInstalledSkills() {
13570
14047
  return { present: false, version: null };
13571
14048
  return {
13572
14049
  present: true,
13573
- version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
14050
+ version: readSkillVersion(join15(dir, bundle.name, "SKILL.md"))
13574
14051
  };
13575
14052
  } catch {
13576
14053
  return { present: false, version: null };
@@ -13632,7 +14109,7 @@ async function runSkills(args, io) {
13632
14109
  return 64;
13633
14110
  }
13634
14111
  } catch (e) {
13635
- io.err(`launchpad skills ${action}: ${describe29(e)}`);
14112
+ io.err(`launchpad skills ${action}: ${describe30(e)}`);
13636
14113
  return 1;
13637
14114
  }
13638
14115
  }
@@ -13655,13 +14132,13 @@ function resolveInstallEnv() {
13655
14132
  return { bundleDir, userSkillsDir };
13656
14133
  }
13657
14134
  function defaultBundleDir() {
13658
- const here = dirname9(fileURLToPath(import.meta.url));
14135
+ const here = dirname10(fileURLToPath(import.meta.url));
13659
14136
  const candidates = [
13660
- resolve14(here, "..", "skills"),
13661
- resolve14(here, "..", "..", "skills")
14137
+ resolve15(here, "..", "skills"),
14138
+ resolve15(here, "..", "..", "skills")
13662
14139
  ];
13663
14140
  for (const c of candidates) {
13664
- if (existsSync15(join15(c, "launchpad-onboard", "SKILL.md"))) {
14141
+ if (existsSync15(join16(c, "launchpad-onboard", "SKILL.md"))) {
13665
14142
  return c;
13666
14143
  }
13667
14144
  }
@@ -13682,12 +14159,12 @@ async function doInstall(io) {
13682
14159
  }
13683
14160
  let installed = 0;
13684
14161
  for (const skill of BUNDLED_SKILLS) {
13685
- const src = join15(env.bundleDir, skill);
14162
+ const src = join16(env.bundleDir, skill);
13686
14163
  if (!await isDir(src)) {
13687
14164
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
13688
14165
  return 1;
13689
14166
  }
13690
- const dest = join15(env.userSkillsDir, skill);
14167
+ const dest = join16(env.userSkillsDir, skill);
13691
14168
  await fs7.rm(dest, { recursive: true, force: true });
13692
14169
  await fs7.cp(src, dest, { recursive: true });
13693
14170
  installed++;
@@ -13712,7 +14189,7 @@ async function doUninstall(io) {
13712
14189
  continue;
13713
14190
  if (!isBundleManaged(entry.name))
13714
14191
  continue;
13715
- const target = join15(env.userSkillsDir, entry.name);
14192
+ const target = join16(env.userSkillsDir, entry.name);
13716
14193
  await fs7.rm(target, { recursive: true, force: true });
13717
14194
  removed++;
13718
14195
  io.out(`✗ removed ${target}`);
@@ -13735,7 +14212,7 @@ async function doList(io) {
13735
14212
  }
13736
14213
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
13737
14214
  for (const name of managedDirs) {
13738
- const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
14215
+ const skillFile = join16(env.userSkillsDir, name, "SKILL.md");
13739
14216
  const version = await readVersion(skillFile);
13740
14217
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
13741
14218
  }
@@ -13762,7 +14239,7 @@ async function isDir(path14) {
13762
14239
  return false;
13763
14240
  }
13764
14241
  }
13765
- function describe29(e) {
14242
+ function describe30(e) {
13766
14243
  return e instanceof Error ? e.message : String(e);
13767
14244
  }
13768
14245
 
@@ -13770,7 +14247,7 @@ function describe29(e) {
13770
14247
  import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
13771
14248
  import { promisify } from "node:util";
13772
14249
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13773
- import { dirname as dirname10, resolve as resolve15, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
14250
+ import { dirname as dirname11, resolve as resolve16, relative as relative6, isAbsolute as isAbsolute3, join as join17 } from "node:path";
13774
14251
  import { homedir as homedir3, tmpdir } from "node:os";
13775
14252
  import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
13776
14253
 
@@ -13815,9 +14292,9 @@ async function startLoopback(state, timeoutMs) {
13815
14292
  resolveCode(code);
13816
14293
  }
13817
14294
  });
13818
- const bound = await new Promise((resolve15) => {
13819
- server.once("error", () => resolve15(false));
13820
- server.listen(0, "127.0.0.1", () => resolve15(true));
14295
+ const bound = await new Promise((resolve16) => {
14296
+ server.once("error", () => resolve16(false));
14297
+ server.listen(0, "127.0.0.1", () => resolve16(true));
13821
14298
  });
13822
14299
  if (!bound)
13823
14300
  return null;
@@ -13893,7 +14370,7 @@ var PKG = "@m-kopa/launchpad-cli";
13893
14370
  var REGISTRY = "https://registry.npmjs.org";
13894
14371
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
13895
14372
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
13896
- var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
14373
+ var CHANNEL_MARKER = join17(homedir3(), ".launchpad", "channel");
13897
14374
  var EXIT_UPDATE_AVAILABLE = 10;
13898
14375
  var UPGRADE_ARGS = {
13899
14376
  npm: ["install", "-g", `${PKG}@latest`],
@@ -14022,8 +14499,8 @@ async function openSystemBrowser(url) {
14022
14499
  await execFileAsync(opener, [url]);
14023
14500
  }
14024
14501
  async function runInstallerScript(script) {
14025
- const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
14026
- const file = join16(dir, "install.sh");
14502
+ const dir = mkdtempSync(join17(tmpdir(), "launchpad-update-"));
14503
+ const file = join17(dir, "install.sh");
14027
14504
  try {
14028
14505
  writeFileSync7(file, script, { mode: 448 });
14029
14506
  return await new Promise((resolvePromise) => {
@@ -14110,7 +14587,7 @@ function errStderr(e) {
14110
14587
  return "";
14111
14588
  }
14112
14589
  async function detectPackageManager2() {
14113
- const pkgRoot = resolve15(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
14590
+ const pkgRoot = resolve16(dirname11(fileURLToPath2(import.meta.url)), "..", "..");
14114
14591
  const candidates = [];
14115
14592
  const npmRoot = await pmRoot("npm");
14116
14593
  if (npmRoot !== null)
@@ -14118,7 +14595,7 @@ async function detectPackageManager2() {
14118
14595
  const pnpmRoot = await pmRoot("pnpm");
14119
14596
  if (pnpmRoot !== null)
14120
14597
  candidates.push(["pnpm", pnpmRoot]);
14121
- candidates.push(["bun", resolve15(homedir3(), ".bun/install/global/node_modules")]);
14598
+ candidates.push(["bun", resolve16(homedir3(), ".bun/install/global/node_modules")]);
14122
14599
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
14123
14600
  return matches.length === 1 ? matches[0][0] : null;
14124
14601
  }
@@ -14132,7 +14609,7 @@ async function pmRoot(pm) {
14132
14609
  }
14133
14610
  }
14134
14611
  function pathContains(root, child) {
14135
- const rel = relative5(root, child);
14612
+ const rel = relative6(root, child);
14136
14613
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
14137
14614
  }
14138
14615
  function runUpgrade(pm) {
@@ -14256,7 +14733,7 @@ function printHelp5(io) {
14256
14733
 
14257
14734
  // src/commands/validate.ts
14258
14735
  import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14259
- import { dirname as dirname11, resolve as resolve16 } from "node:path";
14736
+ import { dirname as dirname12, resolve as resolve17 } from "node:path";
14260
14737
  var validateCommand = {
14261
14738
  name: "validate",
14262
14739
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -14270,11 +14747,21 @@ async function runValidate(args, io) {
14270
14747
  return 64;
14271
14748
  }
14272
14749
  const { file, json, strictGroups } = parseResult;
14273
- const path14 = resolve16(process.cwd(), file ?? "launchpad.yaml");
14750
+ const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
14274
14751
  const result = loadManifest(path14);
14275
14752
  if (result.kind !== "ok") {
14276
14753
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
14277
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;
14278
14765
  const boundary = checkBoundary(path14, result.manifest.app !== undefined);
14279
14766
  const serveErrors = checkStaticServeDir(path14, result.manifest);
14280
14767
  if (serveErrors.length > 0) {
@@ -14284,7 +14771,7 @@ async function runValidate(args, io) {
14284
14771
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
14285
14772
  }
14286
14773
  function checkBoundary(manifestPath, declared) {
14287
- const dir = dirname11(manifestPath);
14774
+ const dir = dirname12(manifestPath);
14288
14775
  let files;
14289
14776
  try {
14290
14777
  files = walkCwd(dir).files;
@@ -14316,8 +14803,8 @@ function checkStaticServeDir(manifestPath, manifest) {
14316
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.`
14317
14804
  ];
14318
14805
  }
14319
- const dir = dirname11(manifestPath);
14320
- const abs = resolve16(dir, serveDir);
14806
+ const dir = dirname12(manifestPath);
14807
+ const abs = resolve17(dir, serveDir);
14321
14808
  if (!existsSync16(abs)) {
14322
14809
  return [
14323
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).`
@@ -14327,7 +14814,7 @@ function checkStaticServeDir(manifestPath, manifest) {
14327
14814
  try {
14328
14815
  isDir2 = statSync2(abs).isDirectory();
14329
14816
  } catch (e) {
14330
- return [`failed to inspect static served directory '${serveDir}/': ${describe30(e)}`];
14817
+ return [`failed to inspect static served directory '${serveDir}/': ${describe31(e)}`];
14331
14818
  }
14332
14819
  if (!isDir2) {
14333
14820
  return [`static app serves '${serveDir}/' but that path is not a directory.`];
@@ -14336,14 +14823,14 @@ function checkStaticServeDir(manifestPath, manifest) {
14336
14823
  try {
14337
14824
  entries = readdirSync3(abs);
14338
14825
  } catch (e) {
14339
- return [`failed to read static served directory '${serveDir}/': ${describe30(e)}`];
14826
+ return [`failed to read static served directory '${serveDir}/': ${describe31(e)}`];
14340
14827
  }
14341
14828
  if (entries.length === 0) {
14342
14829
  return [
14343
14830
  `static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
14344
14831
  ];
14345
14832
  }
14346
- if (!existsSync16(resolve16(abs, "index.html"))) {
14833
+ if (!existsSync16(resolve17(abs, "index.html"))) {
14347
14834
  return [
14348
14835
  `static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
14349
14836
  ];
@@ -14391,7 +14878,7 @@ async function checkGroup(allowedGroup, cfg) {
14391
14878
  if (e instanceof ForbiddenError) {
14392
14879
  return { kind: "forbidden", message: e.message };
14393
14880
  }
14394
- return { kind: "network", message: describe30(e) };
14881
+ return { kind: "network", message: describe31(e) };
14395
14882
  }
14396
14883
  }
14397
14884
  async function checkGroups(groups) {
@@ -14488,6 +14975,8 @@ function renderHumanError(result, io) {
14488
14975
  }
14489
14976
  function renderJsonOk(result, group, boundary, io) {
14490
14977
  const base = {
14978
+ manifest_valid: true,
14979
+ runtime_supported: true,
14491
14980
  name: result.manifest.metadata.name,
14492
14981
  deploymentType: result.manifest.deployment.type,
14493
14982
  appBoundary: {
@@ -14604,7 +15093,7 @@ function parseArgs13(args) {
14604
15093
  }
14605
15094
  return { kind: "ok", file, json, strictGroups };
14606
15095
  }
14607
- function describe30(e) {
15096
+ function describe31(e) {
14608
15097
  return e instanceof Error ? e.message : String(e);
14609
15098
  }
14610
15099
 
@@ -14657,7 +15146,7 @@ async function runWhoami(args, io) {
14657
15146
  io.err(e.message);
14658
15147
  return 1;
14659
15148
  }
14660
- io.err(`launchpad whoami failed: ${describe31(e)}`);
15149
+ io.err(`launchpad whoami failed: ${describe32(e)}`);
14661
15150
  return 1;
14662
15151
  }
14663
15152
  }
@@ -14687,11 +15176,11 @@ async function runShare(args, io) {
14687
15176
  io.err(e.message);
14688
15177
  return 1;
14689
15178
  }
14690
- io.err(`launchpad whoami --share failed: ${describe31(e)}`);
15179
+ io.err(`launchpad whoami --share failed: ${describe32(e)}`);
14691
15180
  return 1;
14692
15181
  }
14693
15182
  }
14694
- function describe31(e) {
15183
+ function describe32(e) {
14695
15184
  return e instanceof Error ? e.message : String(e);
14696
15185
  }
14697
15186
 
@@ -14857,7 +15346,7 @@ async function runEditor(action, args, io, deps) {
14857
15346
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
14858
15347
  return 1;
14859
15348
  }
14860
- io.err(`launchpad ${verb} failed: ${describe32(e)}`);
15349
+ io.err(`launchpad ${verb} failed: ${describe33(e)}`);
14861
15350
  return 1;
14862
15351
  }
14863
15352
  }
@@ -14891,7 +15380,7 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
14891
15380
  nonThrowingStatuses: [409]
14892
15381
  });
14893
15382
  }
14894
- function describe32(e) {
15383
+ function describe33(e) {
14895
15384
  return e instanceof Error ? e.message : String(e);
14896
15385
  }
14897
15386
  var grantEditorCommand = makeEditorCommand("grant");
@@ -15097,7 +15586,7 @@ async function runOwner(action, args, io, deps) {
15097
15586
  io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
15098
15587
  return 1;
15099
15588
  }
15100
- io.err(`launchpad ${verb} failed: ${describe33(e)}`);
15589
+ io.err(`launchpad ${verb} failed: ${describe34(e)}`);
15101
15590
  return 1;
15102
15591
  }
15103
15592
  }
@@ -15131,12 +15620,92 @@ async function sendMutation2(action, slug, target, version, cfg, deps) {
15131
15620
  nonThrowingStatuses: [409]
15132
15621
  });
15133
15622
  }
15134
- function describe33(e) {
15623
+ function describe34(e) {
15135
15624
  return e instanceof Error ? e.message : String(e);
15136
15625
  }
15137
15626
  var addOwnerCommand = makeOwnerCommand("add");
15138
15627
  var removeOwnerCommand = makeOwnerCommand("remove");
15139
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
+
15140
15709
  // src/commands/list-owners.ts
15141
15710
  function realDeps4() {
15142
15711
  return { loadConfig, apiJson };
@@ -15212,7 +15781,7 @@ var listOwnersCommand = makeListOwnersCommand();
15212
15781
  // src/update-notifier.ts
15213
15782
  import { spawn as spawn6 } from "node:child_process";
15214
15783
  import { homedir as homedir4 } from "node:os";
15215
- import { join as join17 } from "node:path";
15784
+ import { join as join18 } from "node:path";
15216
15785
  import {
15217
15786
  existsSync as existsSync17,
15218
15787
  mkdirSync as mkdirSync4,
@@ -15222,7 +15791,7 @@ import {
15222
15791
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
15223
15792
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
15224
15793
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
15225
- var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
15794
+ var CACHE_FILE = join18(homedir4(), ".launchpad", "update-check.json");
15226
15795
  function readCache2() {
15227
15796
  try {
15228
15797
  const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
@@ -15238,12 +15807,12 @@ function readCache2() {
15238
15807
  }
15239
15808
  function writeCache2(state) {
15240
15809
  try {
15241
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15810
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15242
15811
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
15243
15812
  `, { mode: 384 });
15244
15813
  } catch {}
15245
15814
  }
15246
- var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
15815
+ var SKILLS_HINT_MARKER = join18(homedir4(), ".launchpad", "skills-hint-shown");
15247
15816
  function absentHintShown() {
15248
15817
  try {
15249
15818
  return existsSync17(SKILLS_HINT_MARKER);
@@ -15253,7 +15822,7 @@ function absentHintShown() {
15253
15822
  }
15254
15823
  function markAbsentHintShown() {
15255
15824
  try {
15256
- mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
15825
+ mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
15257
15826
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
15258
15827
  `, { mode: 384 });
15259
15828
  } catch {}
@@ -15356,7 +15925,7 @@ var refreshUpdateCacheCommand = {
15356
15925
  // src/telemetry.ts
15357
15926
  import { spawn as spawn7 } from "node:child_process";
15358
15927
  import { homedir as homedir5 } from "node:os";
15359
- import { join as join18 } from "node:path";
15928
+ import { join as join19 } from "node:path";
15360
15929
  import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
15361
15930
  import { randomUUID } from "node:crypto";
15362
15931
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
@@ -15365,8 +15934,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
15365
15934
  var CAPTURE_PATH = "/i/v0/e/";
15366
15935
  var SEND_TIMEOUT_MS = 3000;
15367
15936
  var APP = "launchpad-cli";
15368
- var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
15369
- var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
15937
+ var DEVICE_ID_FILE = join19(homedir5(), ".launchpad", "telemetry-id.json");
15938
+ var FIRST_RUN_MARKER = join19(homedir5(), ".launchpad", "telemetry-notice-shown");
15370
15939
  function buildEvent(ctx, identity, nowMs) {
15371
15940
  const properties = {
15372
15941
  app: APP,
@@ -15440,7 +16009,7 @@ function getOrCreateDeviceId() {
15440
16009
  } catch {}
15441
16010
  const id = randomUUID();
15442
16011
  try {
15443
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16012
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15444
16013
  writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
15445
16014
  `, {
15446
16015
  mode: 384
@@ -15518,7 +16087,7 @@ function firstRunNoticeShown() {
15518
16087
  }
15519
16088
  function markFirstRunNotice() {
15520
16089
  try {
15521
- mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
16090
+ mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
15522
16091
  writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
15523
16092
  `, { mode: 384 });
15524
16093
  } catch {}
@@ -15586,14 +16155,14 @@ import { platform } from "node:os";
15586
16155
  // src/report/breadcrumb.ts
15587
16156
  import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
15588
16157
  import { homedir as homedir6 } from "node:os";
15589
- import { join as join19, dirname as dirname12 } from "node:path";
16158
+ import { join as join20, dirname as dirname13 } from "node:path";
15590
16159
  function breadcrumbPath() {
15591
- return join19(homedir6(), ".launchpad", "last-run.json");
16160
+ return join20(homedir6(), ".launchpad", "last-run.json");
15592
16161
  }
15593
16162
  function writeBreadcrumb(verb, exit) {
15594
16163
  try {
15595
16164
  const p = breadcrumbPath();
15596
- mkdirSync6(dirname12(p), { recursive: true });
16165
+ mkdirSync6(dirname13(p), { recursive: true });
15597
16166
  writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
15598
16167
  } catch {}
15599
16168
  }
@@ -15793,6 +16362,7 @@ var COMMANDS = [
15793
16362
  addOwnerCommand,
15794
16363
  removeOwnerCommand,
15795
16364
  listOwnersCommand,
16365
+ adminCommand,
15796
16366
  makeReportCommand("bug"),
15797
16367
  makeReportCommand("feature"),
15798
16368
  refreshUpdateCacheCommand,
@@ -15800,7 +16370,7 @@ var COMMANDS = [
15800
16370
  ];
15801
16371
  async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
15802
16372
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
15803
- printHelp6(io, commands);
16373
+ printHelp7(io, commands);
15804
16374
  return 0;
15805
16375
  }
15806
16376
  if (argv[0] === "--version" || argv[0] === "-v") {
@@ -15863,9 +16433,11 @@ function requiresCliFloorPreflight(verb, args) {
15863
16433
  return args[0] === "set" || args[0] === "rm";
15864
16434
  if (verb === "secrets")
15865
16435
  return args[0] === "push";
16436
+ if (verb === "admin")
16437
+ return args[0] === "status-revoke";
15866
16438
  return false;
15867
16439
  }
15868
- function printHelp6(io, commands) {
16440
+ function printHelp7(io, commands) {
15869
16441
  io.out("launchpad — manage Launchpad-deployed apps from the command line.");
15870
16442
  io.out("");
15871
16443
  io.out("Usage:");