@m-kopa/launchpad-cli 0.24.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/bundle/upload.d.ts +34 -2
  3. package/dist/bundle/upload.d.ts.map +1 -1
  4. package/dist/cli.js +585 -266
  5. package/dist/commands/deploy-flags.d.ts +6 -0
  6. package/dist/commands/deploy-flags.d.ts.map +1 -1
  7. package/dist/commands/deploy.d.ts +10 -13
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/destroy.d.ts +1 -1
  10. package/dist/commands/destroy.d.ts.map +1 -1
  11. package/dist/commands/envvars.d.ts +2 -2
  12. package/dist/commands/envvars.d.ts.map +1 -1
  13. package/dist/commands/infer-slug.d.ts +37 -0
  14. package/dist/commands/infer-slug.d.ts.map +1 -0
  15. package/dist/commands/logs.d.ts +2 -2
  16. package/dist/commands/logs.d.ts.map +1 -1
  17. package/dist/commands/merge.d.ts +2 -2
  18. package/dist/commands/merge.d.ts.map +1 -1
  19. package/dist/commands/pull.d.ts +1 -1
  20. package/dist/commands/pull.d.ts.map +1 -1
  21. package/dist/commands/recover.d.ts +12 -0
  22. package/dist/commands/recover.d.ts.map +1 -0
  23. package/dist/commands/review.d.ts +2 -2
  24. package/dist/commands/review.d.ts.map +1 -1
  25. package/dist/commands/status.d.ts +18 -4
  26. package/dist/commands/status.d.ts.map +1 -1
  27. package/dist/deploy/apply.d.ts +5 -2
  28. package/dist/deploy/apply.d.ts.map +1 -1
  29. package/dist/deploy/deployment-status.d.ts +18 -0
  30. package/dist/deploy/deployment-status.d.ts.map +1 -1
  31. package/dist/deploy/rollback.d.ts.map +1 -1
  32. package/dist/dispatcher.d.ts.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/skills/launchpad-content-pr/SKILL.md +1 -1
  36. package/skills/launchpad-deploy/SKILL.md +1 -1
  37. package/skills/launchpad-deploy-status/SKILL.md +6 -4
  38. package/skills/launchpad-destroy/SKILL.md +1 -1
  39. package/skills/launchpad-onboard/SKILL.md +1 -1
  40. package/skills/launchpad-status/SKILL.md +13 -6
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.24.0";
22
+ var CLI_VERSION = "0.26.1";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -1219,8 +1219,8 @@ function describe9(e) {
1219
1219
  }
1220
1220
 
1221
1221
  // src/commands/deploy.ts
1222
- import { existsSync as existsSync4 } from "node:fs";
1223
- import * as path6 from "node:path";
1222
+ import { existsSync as existsSync5 } from "node:fs";
1223
+ import * as path7 from "node:path";
1224
1224
 
1225
1225
  // src/bundle/orchestrate.ts
1226
1226
  import { readFileSync as readFileSync4 } from "node:fs";
@@ -1432,6 +1432,9 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
1432
1432
  if (res.status === 202) {
1433
1433
  return { kind: "ok", response: body };
1434
1434
  }
1435
+ if (res.status === 200 && typeof body === "object" && body !== null && body.outcome === "nothing-to-deploy") {
1436
+ return { kind: "ok", response: body };
1437
+ }
1435
1438
  return {
1436
1439
  kind: "error",
1437
1440
  status: res.status,
@@ -3303,8 +3306,8 @@ async function bundleAndDeploy(args) {
3303
3306
  }
3304
3307
 
3305
3308
  // src/commands/deploy.ts
3306
- import { parse as parseYaml4 } from "yaml";
3307
- import { readFileSync as readFileSync6 } from "node:fs";
3309
+ import { parse as parseYaml5 } from "yaml";
3310
+ import { readFileSync as readFileSync7 } from "node:fs";
3308
3311
 
3309
3312
  // src/deploy/git-files.ts
3310
3313
  import { spawn as spawn3 } from "node:child_process";
@@ -3380,6 +3383,7 @@ function parseDeployFlags(args) {
3380
3383
  let yes = false;
3381
3384
  let resumePrNumber = null;
3382
3385
  let timeoutMinutes = null;
3386
+ let atSha = null;
3383
3387
  let modeFlagsSeen = 0;
3384
3388
  let i = 0;
3385
3389
  while (i < args.length) {
@@ -3532,6 +3536,17 @@ function parseDeployFlags(args) {
3532
3536
  i += 2;
3533
3537
  continue;
3534
3538
  }
3539
+ if (a === "--at") {
3540
+ const v = args[i + 1];
3541
+ if (v === undefined)
3542
+ return `missing value for ${a}`;
3543
+ if (!/^[0-9a-f]{40}$/.test(v)) {
3544
+ return `invalid --at "${v}" — expected a 40-char lowercase hex git SHA from the MANAGED repo`;
3545
+ }
3546
+ atSha = v;
3547
+ i += 2;
3548
+ continue;
3549
+ }
3535
3550
  if (a === "--resume-pr") {
3536
3551
  const v = args[i + 1];
3537
3552
  if (v === undefined)
@@ -3601,6 +3616,7 @@ function parseDeployFlags(args) {
3601
3616
  rePin,
3602
3617
  yes,
3603
3618
  resumePrNumber,
3619
+ atSha,
3604
3620
  timeoutMinutes
3605
3621
  },
3606
3622
  message: null,
@@ -3610,6 +3626,9 @@ function parseDeployFlags(args) {
3610
3626
  if (resumePrNumber !== null) {
3611
3627
  return "--resume-pr is only valid with --apply";
3612
3628
  }
3629
+ if (atSha !== null) {
3630
+ return "--at is only valid with --apply";
3631
+ }
3613
3632
  if (timeoutMinutes !== null) {
3614
3633
  return "--timeout-minutes is only valid with --apply";
3615
3634
  }
@@ -3687,7 +3706,6 @@ function parseDeployFlags(args) {
3687
3706
  // src/deploy/apply.ts
3688
3707
  import { existsSync as existsSync3, rmSync } from "node:fs";
3689
3708
  import { resolve as resolvePath, join as join7 } from "node:path";
3690
- import { execFileSync } from "node:child_process";
3691
3709
 
3692
3710
  // src/manifest/load.ts
3693
3711
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -3780,29 +3798,23 @@ async function runDeployApply(opts, io, deps = {}) {
3780
3798
  io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
3781
3799
  return 1;
3782
3800
  }
3783
- let manifestSha;
3784
- try {
3785
- manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
3786
- } catch (e) {
3787
- io.err(`launchpad deploy --apply: failed to resolve git HEAD in ${process.cwd()}: ${describe10(e)}`);
3788
- io.err(" Apply needs a clean commit so portal-bot can fetch launchpad.yaml at that sha.");
3789
- return 2;
3790
- }
3791
- if (!MANIFEST_SHA_REGEX.test(manifestSha)) {
3792
- io.err(`launchpad deploy --apply: git HEAD returned an unexpected sha shape: '${manifestSha}'`);
3793
- return 2;
3801
+ if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
3802
+ io.err(`launchpad deploy --apply: --at expects a 40-char lowercase hex git SHA, got '${opts.atSha}'`);
3803
+ return 64;
3794
3804
  }
3795
- io.out(`Planning apply for ${slug} @ ${manifestSha.slice(0, 7)} …`);
3805
+ io.out(opts.atSha !== undefined ? `Planning apply for ${slug} @ ${opts.atSha.slice(0, 7)} (managed repo) …` : `Planning apply for ${slug} (bot resolves managed main HEAD) …`);
3796
3806
  let plan;
3797
3807
  try {
3798
3808
  plan = await apiJson(cfg, {
3799
3809
  method: "POST",
3800
3810
  path: `/apps/${slug}/manifest/plan`,
3801
- jsonBody: { manifestSha }
3811
+ jsonBody: opts.atSha !== undefined ? { manifestSha: opts.atSha } : {}
3802
3812
  }, fetcher);
3803
3813
  } catch (e) {
3804
3814
  return mapHttpError(e, slug, io);
3805
3815
  }
3816
+ const manifestSha = plan.manifestSha;
3817
+ io.out(`Manifest pinned @ ${manifestSha.slice(0, 7)} (managed repo provenance).`);
3806
3818
  for (const w of plan.warnings)
3807
3819
  io.err(`! ${w}`);
3808
3820
  io.out("");
@@ -3922,14 +3934,6 @@ function sleep(ms) {
3922
3934
  return Promise.resolve();
3923
3935
  return new Promise((res) => setTimeout(res, ms));
3924
3936
  }
3925
- function defaultGitHeadSha(cwd) {
3926
- const out = execFileSync("git", ["rev-parse", "HEAD"], {
3927
- cwd,
3928
- encoding: "utf8",
3929
- stdio: ["ignore", "pipe", "pipe"]
3930
- });
3931
- return out.trim();
3932
- }
3933
3937
  function deletePinIfPresent(cfg, slug, io) {
3934
3938
  const pinPath = join7(cfg.stateDir, slug, "group.json");
3935
3939
  if (!existsSync3(pinPath))
@@ -4012,7 +4016,7 @@ function renderManifestError(loaded, io) {
4012
4016
  }
4013
4017
 
4014
4018
  // src/deploy/dry-run.ts
4015
- import { execFileSync as execFileSync2 } from "node:child_process";
4019
+ import { execFileSync } from "node:child_process";
4016
4020
  import { resolve as resolve5 } from "node:path";
4017
4021
  var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
4018
4022
  async function runDeployDryRun(opts, io, deps = {}) {
@@ -4026,7 +4030,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
4026
4030
  const slug = loaded.manifest.metadata.name;
4027
4031
  let manifestSha;
4028
4032
  try {
4029
- manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha2)(process.cwd());
4033
+ manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
4030
4034
  } catch (e) {
4031
4035
  const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe11(e)}`;
4032
4036
  if (opts.json) {
@@ -4069,8 +4073,8 @@ async function runDeployDryRun(opts, io, deps = {}) {
4069
4073
  }
4070
4074
  return 0;
4071
4075
  }
4072
- function defaultGitHeadSha2(cwd) {
4073
- const out = execFileSync2("git", ["rev-parse", "HEAD"], {
4076
+ function defaultGitHeadSha(cwd) {
4077
+ const out = execFileSync("git", ["rev-parse", "HEAD"], {
4074
4078
  cwd,
4075
4079
  encoding: "utf8",
4076
4080
  stdio: ["ignore", "pipe", "pipe"]
@@ -4363,6 +4367,46 @@ function handleNetworkError(e, io, slug, verb) {
4363
4367
  return 1;
4364
4368
  }
4365
4369
 
4370
+ // src/commands/infer-slug.ts
4371
+ import * as path6 from "node:path";
4372
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
4373
+ import { parse as parseYaml4 } from "yaml";
4374
+ var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4375
+ function inferSlugFromCwd(cwd) {
4376
+ const base = path6.basename(cwd);
4377
+ const m = base.match(DIRNAME_RE);
4378
+ return m === null ? null : m[1];
4379
+ }
4380
+ function resolveManifestSlug(parsed) {
4381
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
4382
+ return null;
4383
+ }
4384
+ const meta = parsed.metadata;
4385
+ if (typeof meta.slug === "string")
4386
+ return meta.slug;
4387
+ if (typeof meta.name === "string")
4388
+ return meta.name;
4389
+ return null;
4390
+ }
4391
+ function inferSlugFromManifestFile(manifestPath) {
4392
+ if (!existsSync4(manifestPath))
4393
+ return null;
4394
+ try {
4395
+ return resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4396
+ } catch {
4397
+ return null;
4398
+ }
4399
+ }
4400
+ function inferSlug(opts) {
4401
+ const manifestPath = path6.resolve(opts.cwd, opts.file ?? "launchpad.yaml");
4402
+ const fromManifest = inferSlugFromManifestFile(manifestPath);
4403
+ const fromDir = inferSlugFromCwd(opts.cwd);
4404
+ if (fromManifest !== null && fromDir !== null && fromManifest !== fromDir) {
4405
+ opts.warn?.(`note: directory name suggests "${fromDir}" but ${path6.basename(manifestPath)} ` + `declares "${fromManifest}" — using the manifest. ` + `(Pass <slug> or --slug to override.)`);
4406
+ }
4407
+ return fromManifest ?? fromDir;
4408
+ }
4409
+
4366
4410
  // src/commands/deploy.ts
4367
4411
  var deployCommand = {
4368
4412
  name: "deploy",
@@ -4370,27 +4414,17 @@ var deployCommand = {
4370
4414
  run: runDeploy
4371
4415
  };
4372
4416
  var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4373
- var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4374
4417
  async function runDeploy(args, io) {
4375
4418
  const flags = parseDeployFlags(args);
4376
4419
  if (typeof flags === "string") {
4377
4420
  io.err(`launchpad deploy: ${flags}`);
4421
+ const isFlagMisuse = flags.includes("is only valid with") || flags.includes("does not accept") || flags.includes("mutually exclusive");
4422
+ if (isFlagMisuse) {
4423
+ io.err(" (run `launchpad deploy` with no flags from your app directory for a content deploy)");
4424
+ return 64;
4425
+ }
4378
4426
  io.err("");
4379
- io.err(`usage: launchpad deploy [--message <text>] [--slug <slug>]
4380
- ` + " (slug defaults to the current directory's `launchpad-app-<slug>` suffix)\n" + `
4381
- ` + `M-892 modes:
4382
- ` + ` launchpad deploy --resume <slug>
4383
- ` + ` launchpad deploy --abandon <slug>
4384
- ` + ` launchpad deploy --new --slug <slug> --display-name <name>
4385
- ` + ` --app-type <${APP_TYPES.join("|")}>
4386
- ` + ` --allowed-group <G_KEY> [--allowed-group ...]
4387
- ` + `
4388
- ` + `Scope 6 dry-run (manifest-driven preview, read-only):
4389
- ` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
4390
- ` + `
4391
- ` + `Scope 6 apply (manifest-driven, writes TF + runs terraform apply):
4392
- ` + ` launchpad deploy --apply --platform-repo <path>
4393
- ` + " [--file <manifest>] [--re-pin] [--yes]");
4427
+ io.err(deployUsage());
4394
4428
  return 64;
4395
4429
  }
4396
4430
  if (flags.mode.kind === "dry-run") {
@@ -4402,6 +4436,7 @@ async function runDeploy(args, io) {
4402
4436
  platformRepo: flags.mode.platformRepo,
4403
4437
  rePin: flags.mode.rePin,
4404
4438
  yes: flags.mode.yes,
4439
+ ...flags.mode.atSha !== null ? { atSha: flags.mode.atSha } : {},
4405
4440
  ...flags.mode.resumePrNumber !== null ? { resumePrNumber: flags.mode.resumePrNumber } : {},
4406
4441
  ...flags.mode.timeoutMinutes !== null ? { timeoutMinutes: flags.mode.timeoutMinutes } : {}
4407
4442
  }, io);
@@ -4423,8 +4458,8 @@ async function runDeploy(args, io) {
4423
4458
  }, io);
4424
4459
  }
4425
4460
  const cwd = process.cwd();
4426
- const manifestPath = path6.join(cwd, "launchpad.yaml");
4427
- if (existsSync4(manifestPath)) {
4461
+ const manifestPath = path7.join(cwd, "launchpad.yaml");
4462
+ if (existsSync5(manifestPath)) {
4428
4463
  return runModelADeploy({ cwd, manifestPath, argv: args, io });
4429
4464
  }
4430
4465
  const parsed = parseArgs2(args);
@@ -4436,9 +4471,9 @@ async function runDeploy(args, io) {
4436
4471
  if (parsed.slug !== null) {
4437
4472
  slug = parsed.slug;
4438
4473
  } else {
4439
- const inferred = inferSlugFromCwd(process.cwd());
4474
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4440
4475
  if (inferred === null) {
4441
- io.err(`launchpad deploy: could not infer slug from cwd (${path6.basename(process.cwd())});
4476
+ io.err(`launchpad deploy: could not infer slug from cwd (${path7.basename(process.cwd())});
4442
4477
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4443
4478
  return 64;
4444
4479
  }
@@ -4458,10 +4493,10 @@ async function runDeploy(args, io) {
4458
4493
  return 1;
4459
4494
  }
4460
4495
  let contentManifestYaml = null;
4461
- const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
4462
- if (existsSync4(contentManifestPath)) {
4496
+ const contentManifestPath = path7.join(process.cwd(), "launchpad.yaml");
4497
+ if (existsSync5(contentManifestPath)) {
4463
4498
  try {
4464
- contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
4499
+ contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
4465
4500
  } catch {
4466
4501
  contentManifestYaml = null;
4467
4502
  }
@@ -4539,6 +4574,24 @@ async function runDeploy(args, io) {
4539
4574
  return 1;
4540
4575
  }
4541
4576
  }
4577
+ function deployUsage() {
4578
+ return `usage: launchpad deploy [--message <text>] [--slug <slug>]
4579
+ ` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
4580
+ ` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
4581
+ ` + `provisioning modes:
4582
+ ` + ` launchpad deploy --resume <slug>
4583
+ ` + ` launchpad deploy --abandon <slug>
4584
+ ` + ` launchpad deploy --new --slug <slug> --display-name <name>
4585
+ ` + ` --app-type <${APP_TYPES.join("|")}>
4586
+ ` + ` --allowed-group <G_KEY> [--allowed-group ...]
4587
+ ` + `
4588
+ ` + `manifest-driven dry-run (read-only preview):
4589
+ ` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
4590
+ ` + `
4591
+ ` + `manifest-driven apply (runs server-side via portal-bot):
4592
+ ` + ` launchpad deploy --apply [--file <manifest>] [--at <sha>] [--re-pin]
4593
+ ` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
4594
+ }
4542
4595
  function parseArgs2(args) {
4543
4596
  let message = null;
4544
4597
  let slug = null;
@@ -4565,11 +4618,6 @@ function parseArgs2(args) {
4565
4618
  }
4566
4619
  return { slug, message };
4567
4620
  }
4568
- function inferSlugFromCwd(cwd) {
4569
- const base = path6.basename(cwd);
4570
- const m = base.match(DIRNAME_RE);
4571
- return m === null ? null : m[1];
4572
- }
4573
4621
  function makeBytesFetcher(bytes, pathSuffix) {
4574
4622
  return async (input, init) => {
4575
4623
  const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -4597,22 +4645,33 @@ function formatBytes2(n) {
4597
4645
  function describe12(e) {
4598
4646
  return e instanceof Error ? e.message : String(e);
4599
4647
  }
4600
- function resolveManifestSlug(parsed) {
4601
- if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
4602
- return null;
4648
+ function surfaceDeployExtras(body, io, slug) {
4649
+ if (body.boundary_stripped !== undefined && body.boundary_stripped.length > 0) {
4650
+ io.err(`warning: the bot stripped ${body.boundary_stripped.length} never-shippable file(s) server-side:`);
4651
+ for (const p of body.boundary_stripped.slice(0, 10)) {
4652
+ io.err(` - ${p}`);
4653
+ }
4654
+ if (body.boundary_stripped.length > 10) {
4655
+ io.err(` … and ${body.boundary_stripped.length - 10} more`);
4656
+ }
4657
+ }
4658
+ const se = body.standing_exceptions;
4659
+ if (se !== undefined && se.count > 0) {
4660
+ io.out(` ${se.count} standing policy exception(s) observed in content already live on main (non-blocking):`);
4661
+ for (const e of se.entries.slice(0, 5)) {
4662
+ io.out(` - ${e.path} [${e.rule}]`);
4663
+ }
4664
+ if (se.entries.length > 5) {
4665
+ io.out(` … and ${se.entries.length - 5} more`);
4666
+ }
4667
+ io.out(` Full list: \`launchpad status ${slug}\`.`);
4603
4668
  }
4604
- const meta = parsed.metadata;
4605
- if (typeof meta.slug === "string")
4606
- return meta.slug;
4607
- if (typeof meta.name === "string")
4608
- return meta.name;
4609
- return null;
4610
4669
  }
4611
4670
  async function runModelADeploy(args) {
4612
4671
  const { cwd, manifestPath, io } = args;
4613
4672
  let slug;
4614
4673
  try {
4615
- const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4674
+ const metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
4616
4675
  if (metaSlug === null) {
4617
4676
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
4618
4677
  return 64;
@@ -4688,16 +4747,30 @@ async function runModelADeploy(args) {
4688
4747
  io.err(` - ${String(f.path)} [${String(f.rule)}]`);
4689
4748
  }
4690
4749
  }
4750
+ if (errorCode === "bundle_policy_violation" || errorCode === "app_boundary_violation" || errorCode === "bad_build_command") {
4751
+ io.err("");
4752
+ io.err(" Nothing was committed or claimed by this attempt — fix the");
4753
+ io.err(" listed file(s) and re-run `launchpad deploy`; the retry is clean.");
4754
+ }
4691
4755
  return 1;
4692
4756
  }
4693
4757
  case "ok": {
4694
4758
  for (const w of result.boundaryWarnings) {
4695
4759
  io.err(`warning: ${w}`);
4696
4760
  }
4697
- if (result.result.status === "provisioning_started") {
4698
- const submissionId = typeof result.result.submissionId === "string" ? result.result.submissionId : "(missing)";
4699
- const appType = typeof result.result.appType === "string" ? result.result.appType : "(missing)";
4700
- const message = typeof result.result.message === "string" ? result.result.message : "Bot accepted the upload and started provisioning.";
4761
+ const success = result.result;
4762
+ if ("outcome" in success) {
4763
+ io.out("Nothing to deploy your app is already live at this content.");
4764
+ if (typeof success.head_sha === "string") {
4765
+ io.out(` (managed main @ ${success.head_sha.slice(0, 8)} matches the bundle)`);
4766
+ }
4767
+ surfaceDeployExtras(success, io, slug);
4768
+ return 0;
4769
+ }
4770
+ if (success.status === "provisioning_started") {
4771
+ const submissionId = typeof success.submissionId === "string" ? success.submissionId : "(missing)";
4772
+ const appType = typeof success.appType === "string" ? success.appType : "(missing)";
4773
+ const message = typeof success.message === "string" ? success.message : "Bot accepted the upload and started provisioning.";
4701
4774
  io.out(`✓ First-time deploy — provisioning workflow started for ${slug}`);
4702
4775
  io.out(` submission: ${submissionId}`);
4703
4776
  io.out(` appType: ${appType}`);
@@ -4709,12 +4782,12 @@ async function runModelADeploy(args) {
4709
4782
  io.out(` launchpad deploy # re-run once lifecycle is "live"`);
4710
4783
  return 0;
4711
4784
  }
4712
- if (typeof result.result.commit_sha !== "string" || typeof result.result.repo !== "string") {
4785
+ if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
4713
4786
  io.err(`launchpad deploy: bot returned an unexpected 202 body — missing commit_sha or repo.`);
4714
- io.err(` got: ${JSON.stringify(result.result)}`);
4787
+ io.err(` got: ${JSON.stringify(success)}`);
4715
4788
  return 1;
4716
4789
  }
4717
- io.out(`✓ Bundle accepted — committed as ${result.result.commit_sha.slice(0, 8)} on ${result.result.repo}`);
4790
+ io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
4718
4791
  io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
4719
4792
  if (result.workerScript !== null) {
4720
4793
  io.out(` cron Worker '${result.workerScript}' bundled + shipped (the bot deploys it after the commit)`);
@@ -4722,9 +4795,10 @@ async function runModelADeploy(args) {
4722
4795
  if (result.walk.skipped.length > 0) {
4723
4796
  io.out(` ${result.walk.skipped.length} entries skipped (.gitignore / default-ignore / symlink)`);
4724
4797
  }
4725
- if (typeof result.result.message === "string") {
4726
- io.out(` ${result.result.message}`);
4798
+ if (typeof success.message === "string") {
4799
+ io.out(` ${success.message}`);
4727
4800
  }
4801
+ surfaceDeployExtras(success, io, slug);
4728
4802
  io.out("");
4729
4803
  io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
4730
4804
  io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
@@ -4734,14 +4808,13 @@ async function runModelADeploy(args) {
4734
4808
  }
4735
4809
 
4736
4810
  // src/commands/envvars.ts
4737
- import * as path7 from "node:path";
4811
+ import * as path8 from "node:path";
4738
4812
  var envvarsCommand = {
4739
4813
  name: "envvars",
4740
4814
  summary: "list / set / remove production env vars (slug-scoped)",
4741
4815
  run: runEnvvars
4742
4816
  };
4743
4817
  var SLUG_RE5 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4744
- var DIRNAME_RE2 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4745
4818
  var ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
4746
4819
  async function runEnvvars(args, io) {
4747
4820
  const parsed = parseArgs3(args);
@@ -4753,9 +4826,9 @@ async function runEnvvars(args, io) {
4753
4826
  if (parsed.slug !== null) {
4754
4827
  slug = parsed.slug;
4755
4828
  } else {
4756
- const inferred = inferSlugFromCwd2(process.cwd());
4829
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4757
4830
  if (inferred === null) {
4758
- io.err(`launchpad envvars: could not infer slug from cwd (${path7.basename(process.cwd())});
4831
+ io.err(`launchpad envvars: could not infer slug from cwd (${path8.basename(process.cwd())});
4759
4832
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4760
4833
  return 64;
4761
4834
  }
@@ -4901,11 +4974,6 @@ function parseArgs3(args) {
4901
4974
  }
4902
4975
  return null;
4903
4976
  }
4904
- function inferSlugFromCwd2(cwd) {
4905
- const base = path7.basename(cwd);
4906
- const m = base.match(DIRNAME_RE2);
4907
- return m === null ? null : m[1];
4908
- }
4909
4977
  function renderList(envVars, io) {
4910
4978
  if (envVars.length === 0) {
4911
4979
  io.out("(no env vars set)");
@@ -4930,8 +4998,8 @@ function describe13(e) {
4930
4998
  }
4931
4999
 
4932
5000
  // src/commands/generate.ts
4933
- import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
4934
- import { dirname as dirname4, resolve as resolve6, relative as relative3 } from "node:path";
5001
+ import { mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
5002
+ import { dirname as dirname4, resolve as resolve7, relative as relative3 } from "node:path";
4935
5003
  var generateCommand = {
4936
5004
  name: "generate",
4937
5005
  summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
@@ -4944,14 +5012,14 @@ async function runGenerate(args, io) {
4944
5012
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
4945
5013
  return 64;
4946
5014
  }
4947
- const manifestPath = resolve6(process.cwd(), flags.file ?? "launchpad.yaml");
5015
+ const manifestPath = resolve7(process.cwd(), flags.file ?? "launchpad.yaml");
4948
5016
  const result = loadManifest(manifestPath);
4949
5017
  if (result.kind !== "ok") {
4950
5018
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
4951
5019
  }
4952
5020
  const appRoot = dirname4(manifestPath);
4953
- const wranglerPath = resolve6(appRoot, "container", "wrangler.toml");
4954
- const workflowPath = resolve6(appRoot, ".github", "workflows", "deploy.yml");
5021
+ const wranglerPath = resolve7(appRoot, "container", "wrangler.toml");
5022
+ const workflowPath = resolve7(appRoot, ".github", "workflows", "deploy.yml");
4955
5023
  const wranglerOut = generateWranglerToml(result.manifest);
4956
5024
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
4957
5025
  if (flags.dryRun) {
@@ -4966,7 +5034,7 @@ async function runGenerate(args, io) {
4966
5034
  ];
4967
5035
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
4968
5036
  }
4969
- function applyOne(artefact, path8, out, force) {
5037
+ function applyOne(artefact, path9, out, force) {
4970
5038
  if (out.kind === "not-applicable") {
4971
5039
  return {
4972
5040
  artefact,
@@ -4974,32 +5042,32 @@ function applyOne(artefact, path8, out, force) {
4974
5042
  warnings: []
4975
5043
  };
4976
5044
  }
4977
- const existing = readIfExists(path8);
5045
+ const existing = readIfExists(path9);
4978
5046
  if (existing.kind === "read-error") {
4979
5047
  return {
4980
5048
  artefact,
4981
- action: { kind: "write-error", path: path8, message: existing.message },
5049
+ action: { kind: "write-error", path: path9, message: existing.message },
4982
5050
  warnings: out.warnings
4983
5051
  };
4984
5052
  }
4985
5053
  if (existing.kind === "ok" && existing.content === out.content) {
4986
- return { artefact, action: { kind: "unchanged", path: path8 }, warnings: out.warnings };
5054
+ return { artefact, action: { kind: "unchanged", path: path9 }, warnings: out.warnings };
4987
5055
  }
4988
5056
  if (existing.kind === "ok" && existing.content !== out.content && !force) {
4989
5057
  return {
4990
5058
  artefact,
4991
- action: { kind: "would-overwrite", path: path8 },
5059
+ action: { kind: "would-overwrite", path: path9 },
4992
5060
  warnings: out.warnings
4993
5061
  };
4994
5062
  }
4995
5063
  try {
4996
- mkdirSync(dirname4(path8), { recursive: true });
4997
- writeFileSync(path8, out.content, "utf8");
5064
+ mkdirSync(dirname4(path9), { recursive: true });
5065
+ writeFileSync(path9, out.content, "utf8");
4998
5066
  return {
4999
5067
  artefact,
5000
5068
  action: {
5001
5069
  kind: "written",
5002
- path: path8,
5070
+ path: path9,
5003
5071
  bytes: Buffer.byteLength(out.content, "utf8")
5004
5072
  },
5005
5073
  warnings: out.warnings
@@ -5009,16 +5077,16 @@ function applyOne(artefact, path8, out, force) {
5009
5077
  artefact,
5010
5078
  action: {
5011
5079
  kind: "write-error",
5012
- path: path8,
5080
+ path: path9,
5013
5081
  message: err.message ?? String(err)
5014
5082
  },
5015
5083
  warnings: out.warnings
5016
5084
  };
5017
5085
  }
5018
5086
  }
5019
- function readIfExists(path8) {
5087
+ function readIfExists(path9) {
5020
5088
  try {
5021
- return { kind: "ok", content: readFileSync7(path8, "utf8") };
5089
+ return { kind: "ok", content: readFileSync8(path9, "utf8") };
5022
5090
  } catch (err) {
5023
5091
  const e = err;
5024
5092
  if (e.code === "ENOENT")
@@ -5229,7 +5297,7 @@ function parseFlags(args) {
5229
5297
  }
5230
5298
 
5231
5299
  // src/groups/client.ts
5232
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
5300
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
5233
5301
  import { dirname as dirname5, join as join9 } from "node:path";
5234
5302
  var CACHE_TTL_MS = 60 * 60 * 1000;
5235
5303
  var CACHE_FILENAME = "groups.json";
@@ -5270,10 +5338,10 @@ async function fetchGroups(cfg, opts = {}) {
5270
5338
  writeCache(cachePath, { fetchedAt, groups });
5271
5339
  return { kind: "ok", source: "fresh", fetchedAt, groups };
5272
5340
  }
5273
- function readCache(path8) {
5341
+ function readCache(path9) {
5274
5342
  let raw;
5275
5343
  try {
5276
- raw = readFileSync8(path8, "utf8");
5344
+ raw = readFileSync9(path9, "utf8");
5277
5345
  } catch {
5278
5346
  return null;
5279
5347
  }
@@ -5301,10 +5369,10 @@ function isEntraGroup(value) {
5301
5369
  const g = value;
5302
5370
  return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
5303
5371
  }
5304
- function writeCache(path8, envelope) {
5372
+ function writeCache(path9, envelope) {
5305
5373
  try {
5306
- mkdirSync2(dirname5(path8), { recursive: true });
5307
- writeFileSync2(path8, JSON.stringify(envelope), "utf8");
5374
+ mkdirSync2(dirname5(path9), { recursive: true });
5375
+ writeFileSync2(path9, JSON.stringify(envelope), "utf8");
5308
5376
  } catch {}
5309
5377
  }
5310
5378
  function describe14(e) {
@@ -5774,17 +5842,17 @@ function describe17(e) {
5774
5842
 
5775
5843
  // src/commands/init.ts
5776
5844
  import { createInterface } from "node:readline/promises";
5777
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
5778
- import { resolve as resolve7 } from "node:path";
5845
+ import { existsSync as existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
5846
+ import { resolve as resolve8 } from "node:path";
5779
5847
  import { stringify as yamlStringify } from "yaml";
5780
5848
 
5781
5849
  // src/detect/index.ts
5782
- import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
5850
+ import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
5783
5851
  import { join as join10 } from "node:path";
5784
5852
  function detectAppShape(cwd) {
5785
- const hasPackageJson = existsSync5(join10(cwd, "package.json"));
5786
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync5(join10(cwd, file)));
5787
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync5(join10(cwd, n)));
5853
+ const hasPackageJson = existsSync6(join10(cwd, "package.json"));
5854
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join10(cwd, file)));
5855
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join10(cwd, n)));
5788
5856
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
5789
5857
  return {
5790
5858
  kind: "not-applicable",
@@ -5830,7 +5898,7 @@ var LOCKFILES = [
5830
5898
  { file: "yarn.lock", pm: "yarn" }
5831
5899
  ];
5832
5900
  function detectPackageManager(cwd) {
5833
- const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
5901
+ const present = LOCKFILES.filter(({ file }) => existsSync6(join10(cwd, file)));
5834
5902
  if (present.length === 0) {
5835
5903
  return {
5836
5904
  kind: "ambiguous",
@@ -5853,7 +5921,7 @@ function detectPackageManager(cwd) {
5853
5921
  function detectVitePresence(cwd) {
5854
5922
  for (const name of VITE_CONFIG_NAMES) {
5855
5923
  const p = join10(cwd, name);
5856
- if (existsSync5(p)) {
5924
+ if (existsSync6(p)) {
5857
5925
  return { kind: "ok", value: { path: p } };
5858
5926
  }
5859
5927
  }
@@ -5865,7 +5933,7 @@ function detectVitePresence(cwd) {
5865
5933
  function detectAppType(cwd) {
5866
5934
  const fnDir = join10(cwd, "functions");
5867
5935
  let hasFunctionsDir = false;
5868
- if (existsSync5(fnDir)) {
5936
+ if (existsSync6(fnDir)) {
5869
5937
  try {
5870
5938
  hasFunctionsDir = statSync(fnDir).isDirectory();
5871
5939
  } catch {
@@ -5876,7 +5944,7 @@ function detectAppType(cwd) {
5876
5944
  }
5877
5945
  function detectBuildCommand(cwd, pm) {
5878
5946
  const pkgJsonPath = join10(cwd, "package.json");
5879
- if (!existsSync5(pkgJsonPath)) {
5947
+ if (!existsSync6(pkgJsonPath)) {
5880
5948
  return {
5881
5949
  kind: "ambiguous",
5882
5950
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -5884,7 +5952,7 @@ function detectBuildCommand(cwd, pm) {
5884
5952
  }
5885
5953
  let pkgJson;
5886
5954
  try {
5887
- pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
5955
+ pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
5888
5956
  } catch (e) {
5889
5957
  return {
5890
5958
  kind: "ambiguous",
@@ -5919,7 +5987,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
5919
5987
  function detectDestinationDir(cwd, vite) {
5920
5988
  let text;
5921
5989
  try {
5922
- text = readFileSync9(vite.path, "utf8");
5990
+ text = readFileSync10(vite.path, "utf8");
5923
5991
  } catch (e) {
5924
5992
  return {
5925
5993
  kind: "ambiguous",
@@ -5948,8 +6016,8 @@ async function runInit(args, io, prompt) {
5948
6016
  return 64;
5949
6017
  }
5950
6018
  const { inputs, options } = parsed;
5951
- const outPath = resolve7(process.cwd(), options.out);
5952
- if (existsSync6(outPath) && !options.force) {
6019
+ const outPath = resolve8(process.cwd(), options.out);
6020
+ if (existsSync7(outPath) && !options.force) {
5953
6021
  io.err(`launchpad init: ${outPath} already exists`);
5954
6022
  io.err("Pass --force to overwrite.");
5955
6023
  return 64;
@@ -5996,7 +6064,7 @@ async function runInit(args, io, prompt) {
5996
6064
  }
5997
6065
  io.out(`✓ wrote ${outPath}`);
5998
6066
  if (options.gitignore) {
5999
- const gitignorePath = resolve7(process.cwd(), ".gitignore");
6067
+ const gitignorePath = resolve8(process.cwd(), ".gitignore");
6000
6068
  try {
6001
6069
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
6002
6070
  if (changed.length > 0) {
@@ -6365,10 +6433,10 @@ function buildManifest(inputs, detected) {
6365
6433
  function renderYaml(manifest) {
6366
6434
  return yamlStringify(manifest, { lineWidth: 0 });
6367
6435
  }
6368
- function ensureGitignoreEntries(path8, entries) {
6436
+ function ensureGitignoreEntries(path9, entries) {
6369
6437
  let current = "";
6370
- if (existsSync6(path8)) {
6371
- current = readFileSync10(path8, "utf8");
6438
+ if (existsSync7(path9)) {
6439
+ current = readFileSync11(path9, "utf8");
6372
6440
  }
6373
6441
  const lines = current.split(/\r?\n/);
6374
6442
  const present = new Set(lines.map((l) => l.trim()));
@@ -6386,7 +6454,7 @@ function ensureGitignoreEntries(path8, entries) {
6386
6454
  }
6387
6455
  }
6388
6456
  if (added.length > 0) {
6389
- writeFileSync3(path8, out, { encoding: "utf8" });
6457
+ writeFileSync3(path9, out, { encoding: "utf8" });
6390
6458
  }
6391
6459
  return added;
6392
6460
  }
@@ -6458,14 +6526,13 @@ function describe19(e) {
6458
6526
  }
6459
6527
 
6460
6528
  // src/commands/logs.ts
6461
- import * as path8 from "node:path";
6529
+ import * as path9 from "node:path";
6462
6530
  var logsCommand = {
6463
6531
  name: "logs",
6464
6532
  summary: "show recent Pages deployment history (slug-scoped)",
6465
6533
  run: runLogs
6466
6534
  };
6467
6535
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
6468
- var DIRNAME_RE3 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
6469
6536
  var DEFAULT_LINES = 10;
6470
6537
  var MAX_LINES = 25;
6471
6538
  async function runLogs(args, io) {
@@ -6479,9 +6546,9 @@ async function runLogs(args, io) {
6479
6546
  if (parsed.slug !== null) {
6480
6547
  slug = parsed.slug;
6481
6548
  } else {
6482
- const inferred = inferSlugFromCwd3(process.cwd());
6549
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6483
6550
  if (inferred === null) {
6484
- io.err(`launchpad logs: could not infer slug from cwd (${path8.basename(process.cwd())});
6551
+ io.err(`launchpad logs: could not infer slug from cwd (${path9.basename(process.cwd())});
6485
6552
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6486
6553
  return 64;
6487
6554
  }
@@ -6559,11 +6626,6 @@ function parseArgs5(args) {
6559
6626
  }
6560
6627
  return { slug, lines };
6561
6628
  }
6562
- function inferSlugFromCwd3(cwd) {
6563
- const base = path8.basename(cwd);
6564
- const m = base.match(DIRNAME_RE3);
6565
- return m === null ? null : m[1];
6566
- }
6567
6629
  function renderDeployments(deployments, io) {
6568
6630
  if (deployments.length === 0) {
6569
6631
  io.out("(no deployments yet)");
@@ -6614,14 +6676,13 @@ function describe20(e) {
6614
6676
  }
6615
6677
 
6616
6678
  // src/commands/merge.ts
6617
- import * as path9 from "node:path";
6679
+ import * as path10 from "node:path";
6618
6680
  var mergeCommand = {
6619
6681
  name: "merge",
6620
6682
  summary: "squash-merge a review-passed PR (slug-scoped)",
6621
6683
  run: runMerge
6622
6684
  };
6623
6685
  var SLUG_RE7 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
6624
- var DIRNAME_RE4 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
6625
6686
  var HANDLED_STATUSES = [409, 422, 502, 503];
6626
6687
  async function runMerge(args, io) {
6627
6688
  const parsed = parseArgs6(args);
@@ -6634,9 +6695,9 @@ async function runMerge(args, io) {
6634
6695
  if (parsed.slug !== null) {
6635
6696
  slug = parsed.slug;
6636
6697
  } else {
6637
- const inferred = inferSlugFromCwd4(process.cwd());
6698
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6638
6699
  if (inferred === null) {
6639
- io.err(`launchpad merge: could not infer slug from cwd (${path9.basename(process.cwd())});
6700
+ io.err(`launchpad merge: could not infer slug from cwd (${path10.basename(process.cwd())});
6640
6701
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6641
6702
  return 64;
6642
6703
  }
@@ -6733,11 +6794,6 @@ function parseArgs6(args) {
6733
6794
  return null;
6734
6795
  return { slug, prNumber };
6735
6796
  }
6736
- function inferSlugFromCwd4(cwd) {
6737
- const base = path9.basename(cwd);
6738
- const m = base.match(DIRNAME_RE4);
6739
- return m === null ? null : m[1];
6740
- }
6741
6797
  function renderBotError(status, env, io, prNumber = null) {
6742
6798
  const code = env?.error ?? "unknown";
6743
6799
  const detail = env?.message;
@@ -6793,7 +6849,7 @@ function describe21(e) {
6793
6849
  }
6794
6850
 
6795
6851
  // src/commands/plan.ts
6796
- import { resolve as resolve8 } from "node:path";
6852
+ import { resolve as resolve9 } from "node:path";
6797
6853
  var planCommand = {
6798
6854
  name: "plan",
6799
6855
  summary: "summarise what the manifest would deploy (offline)",
@@ -6806,7 +6862,7 @@ async function runPlan(args, io) {
6806
6862
  io.err("Usage: launchpad plan [--file <path>] [--json]");
6807
6863
  return 64;
6808
6864
  }
6809
- const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
6865
+ const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
6810
6866
  const result = loadManifest(manifestPath);
6811
6867
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
6812
6868
  }
@@ -6989,7 +7045,7 @@ var SLUG_RE8 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
6989
7045
  var SLUG_MIN_LENGTH = 3;
6990
7046
  var SLUG_MAX_LENGTH = 58;
6991
7047
  async function runDestroy(args, io, prompt, isTty) {
6992
- const parsed = parseArgs7(args);
7048
+ const parsed = parseArgs7(args, process.cwd(), (l) => io.err(l));
6993
7049
  if (typeof parsed === "string") {
6994
7050
  io.err(`launchpad destroy: ${parsed}`);
6995
7051
  printUsage3(io);
@@ -7044,7 +7100,7 @@ async function runDestroy(args, io, prompt, isTty) {
7044
7100
  return 1;
7045
7101
  }
7046
7102
  }
7047
- function parseArgs7(args, cwd = process.cwd()) {
7103
+ function parseArgs7(args, cwd = process.cwd(), warn) {
7048
7104
  let slug = null;
7049
7105
  let confirmSlug = null;
7050
7106
  let yes = false;
@@ -7091,7 +7147,7 @@ function parseArgs7(args, cwd = process.cwd()) {
7091
7147
  i += 1;
7092
7148
  }
7093
7149
  if (slug === null) {
7094
- const inferred = inferSlugFromCwd(cwd);
7150
+ const inferred = inferSlug({ cwd, warn });
7095
7151
  if (inferred === null) {
7096
7152
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7097
7153
  }
@@ -7251,8 +7307,8 @@ import { stringify as stringifyYaml } from "yaml";
7251
7307
 
7252
7308
  // src/deploy/manifest-state.ts
7253
7309
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
7254
- const path10 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
7255
- const raw = await apiJson(cfg, { path: path10 }, fetcher);
7310
+ const path11 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
7311
+ const raw = await apiJson(cfg, { path: path11 }, fetcher);
7256
7312
  return {
7257
7313
  slug: raw.slug,
7258
7314
  hasAppFile: raw.hasAppFile,
@@ -7265,11 +7321,24 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
7265
7321
 
7266
7322
  // src/deploy/manifest-status.ts
7267
7323
  async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
7268
- const path10 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7269
- return apiJson(cfg, { path: path10 }, fetcher);
7324
+ const path11 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7325
+ return apiJson(cfg, { path: path11 }, fetcher);
7270
7326
  }
7271
7327
 
7272
7328
  // src/deploy/deployment-status.ts
7329
+ async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
7330
+ let raw;
7331
+ try {
7332
+ raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/exceptions` }, fetcher);
7333
+ } catch (e) {
7334
+ if (e instanceof NotFoundError)
7335
+ return null;
7336
+ throw e;
7337
+ }
7338
+ if (!Array.isArray(raw.exceptions))
7339
+ return null;
7340
+ return raw.exceptions.filter((e) => typeof e === "object" && e !== null && typeof e.path === "string" && typeof e.rule === "string" && typeof e.detectedAt === "string" && typeof e.deployRef === "string");
7341
+ }
7273
7342
  async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
7274
7343
  let raw;
7275
7344
  try {
@@ -7297,7 +7366,7 @@ var pullCommand = {
7297
7366
  };
7298
7367
  var SLUG_RE9 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7299
7368
  async function runPull(args, io) {
7300
- const parsed = parseArgs8(args);
7369
+ const parsed = parseArgs8(args, process.cwd(), (l) => io.err(l));
7301
7370
  if (typeof parsed === "string") {
7302
7371
  io.err(`launchpad pull: ${parsed}`);
7303
7372
  printUsage4(io);
@@ -7383,7 +7452,7 @@ async function fetchLiveDeploymentBestEffort(cfg, slug) {
7383
7452
  return null;
7384
7453
  }
7385
7454
  }
7386
- function parseArgs8(args, cwd = process.cwd()) {
7455
+ function parseArgs8(args, cwd = process.cwd(), warn) {
7387
7456
  let slug = null;
7388
7457
  let out = null;
7389
7458
  let status = false;
@@ -7424,7 +7493,7 @@ function parseArgs8(args, cwd = process.cwd()) {
7424
7493
  i += 1;
7425
7494
  }
7426
7495
  if (slug === null) {
7427
- const inferred = inferSlugFromCwd(cwd);
7496
+ const inferred = inferSlug({ cwd, warn });
7428
7497
  if (inferred === null) {
7429
7498
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7430
7499
  }
@@ -7442,8 +7511,9 @@ function printUsage4(io) {
7442
7511
  " Reads the deployed launchpad.yaml for an app via the bot.",
7443
7512
  " No local platform-repo or terraform required.",
7444
7513
  "",
7445
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7446
- " the directory name. Explicit --slug or positional override.",
7514
+ " With no slug, it is inferred from the local launchpad.yaml's",
7515
+ " declared slug first, then from a launchpad-app-<slug>/",
7516
+ " directory name. Explicit --slug or positional override.",
7447
7517
  "",
7448
7518
  " --status read the role-redacted status block (what your",
7449
7519
  " role may see) instead of the spec manifest.",
@@ -7456,14 +7526,184 @@ function describe23(e) {
7456
7526
  return e instanceof Error ? e.message : String(e);
7457
7527
  }
7458
7528
 
7529
+ // src/commands/recover.ts
7530
+ var recoverCommand = {
7531
+ name: "recover",
7532
+ summary: "repair a terminal-failed app record by reconciling against live state",
7533
+ run: runRecover
7534
+ };
7535
+ var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7536
+ async function runRecover(args, io) {
7537
+ const parsed = parseRecoverArgs(args, process.cwd(), (l) => io.err(l));
7538
+ if (typeof parsed === "string") {
7539
+ io.err(`launchpad recover: ${parsed}`);
7540
+ printUsage5(io);
7541
+ return 64;
7542
+ }
7543
+ const cfg = loadConfig();
7544
+ try {
7545
+ io.out(`Reconciling "${parsed.slug}" against live Cloudflare state …`);
7546
+ const res = await apiRaw(cfg, {
7547
+ method: "POST",
7548
+ path: `/apps/${parsed.slug}/recover`,
7549
+ jsonBody: undefined,
7550
+ nonThrowingStatuses: [409, 503]
7551
+ });
7552
+ const body = await res.json().catch(() => null);
7553
+ if (parsed.json) {
7554
+ io.out(JSON.stringify({ httpStatus: res.status, ...body ?? {} }, null, 2));
7555
+ return res.status === 200 ? 0 : 1;
7556
+ }
7557
+ if (res.status === 200 && body !== null && "outcome" in body) {
7558
+ renderSuccess2(body, io);
7559
+ return 0;
7560
+ }
7561
+ if (res.status === 503) {
7562
+ const msg = body !== null && "message" in body && typeof body.message === "string" ? body.message : "live state unavailable — nothing was changed; retry shortly.";
7563
+ io.err(`launchpad recover: ${msg}`);
7564
+ return 1;
7565
+ }
7566
+ if (res.status === 409 && body !== null && "error" in body) {
7567
+ renderRefusal(parsed.slug, body, io);
7568
+ return 1;
7569
+ }
7570
+ io.err(`launchpad recover: bot returned an unexpected HTTP ${res.status} response.`);
7571
+ return 1;
7572
+ } catch (e) {
7573
+ return mapError(e, parsed.slug, io);
7574
+ }
7575
+ }
7576
+ function renderSuccess2(body, io) {
7577
+ if (body.outcome === "noop_already_healthy") {
7578
+ io.out(`${body.slug}: already healthy — nothing to recover.`);
7579
+ io.out(` ${body.message}`);
7580
+ return;
7581
+ }
7582
+ io.out(`${body.slug}: REPAIRED — registry record reconciled to live.`);
7583
+ if (body.before !== undefined) {
7584
+ io.out(` before: ${body.before.lifecycle}` + (body.before.reason !== null ? ` (${body.before.reason})` : ""));
7585
+ }
7586
+ if (body.after !== undefined) {
7587
+ io.out(` after: ${body.after.lifecycle}`);
7588
+ }
7589
+ const checked = body.checked;
7590
+ if (checked !== undefined) {
7591
+ io.out(` verified: Pages project "${checked.pagesProject}" exists` + (checked.latestDeployment !== null ? `; latest production deployment ${checked.latestDeployment.id} ` + `(${checked.latestDeployment.buildStatus}, ${checked.latestDeployment.createdOn})` : ""));
7592
+ if (checked.olderContentServing) {
7593
+ io.out(" note: the LATEST build failed — an older successful deployment is what's serving.");
7594
+ }
7595
+ }
7596
+ io.out("");
7597
+ io.out(`Run \`launchpad status ${body.slug}\` — it now reports the live deployment truth.`);
7598
+ }
7599
+ function renderRefusal(slug, body, io) {
7600
+ io.err(`launchpad recover: refused — "${slug}" was NOT repaired.`);
7601
+ if (typeof body.message === "string") {
7602
+ io.err(` ${body.message}`);
7603
+ }
7604
+ const checked = body.checked;
7605
+ if (checked !== undefined) {
7606
+ io.err(" checked:");
7607
+ io.err(` Pages project "${checked.pagesProject}": ${checked.projectExists ? "exists" : "MISSING"}`);
7608
+ if (checked.latestDeployment !== null) {
7609
+ io.err(` latest production deployment: ${checked.latestDeployment.id} (${checked.latestDeployment.buildStatus})`);
7610
+ } else if (checked.projectExists) {
7611
+ io.err(" latest production deployment: none");
7612
+ }
7613
+ }
7614
+ }
7615
+ function mapError(e, slug, io) {
7616
+ if (e instanceof UnauthenticatedError) {
7617
+ io.err(`launchpad recover: ${e.message}`);
7618
+ io.err(" session expired, run `launchpad login`");
7619
+ return 1;
7620
+ }
7621
+ if (e instanceof ForbiddenError) {
7622
+ io.err(`launchpad recover: not authorised for app "${slug}" (you must be an owner or editor).`);
7623
+ return 1;
7624
+ }
7625
+ if (e instanceof NotFoundError) {
7626
+ io.err(`launchpad recover: app "${slug}" not found.`);
7627
+ return 1;
7628
+ }
7629
+ if (e instanceof ApiError || e instanceof TransportError) {
7630
+ io.err(`launchpad recover: ${e.message}`);
7631
+ return 1;
7632
+ }
7633
+ io.err(`launchpad recover failed: ${e instanceof Error ? e.message : String(e)}`);
7634
+ return 1;
7635
+ }
7636
+ function parseRecoverArgs(args, cwd = process.cwd(), warn) {
7637
+ let slug = null;
7638
+ let json = false;
7639
+ let i = 0;
7640
+ while (i < args.length) {
7641
+ const a = args[i] ?? "";
7642
+ if (a === "--slug") {
7643
+ const v = args[i + 1];
7644
+ if (v === undefined)
7645
+ return "missing value for --slug";
7646
+ if (slug !== null)
7647
+ return "cannot mix --slug with positional slug (or pass --slug twice)";
7648
+ slug = v;
7649
+ i += 2;
7650
+ continue;
7651
+ }
7652
+ if (a === "--json") {
7653
+ json = true;
7654
+ i += 1;
7655
+ continue;
7656
+ }
7657
+ if (a.startsWith("--")) {
7658
+ return `unknown flag "${a}"`;
7659
+ }
7660
+ if (slug !== null) {
7661
+ return "cannot mix --slug with positional slug (or pass two positional slugs)";
7662
+ }
7663
+ slug = a;
7664
+ i += 1;
7665
+ }
7666
+ if (slug === null) {
7667
+ slug = inferSlug({ cwd, warn });
7668
+ }
7669
+ if (slug === null) {
7670
+ return "slug not provided + cannot infer from cwd. " + "Pass <slug> or --slug <slug>, cd into launchpad-app-<slug>/, or run from a directory with launchpad.yaml.";
7671
+ }
7672
+ if (!SLUG_RE10.test(slug)) {
7673
+ return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
7674
+ }
7675
+ return { slug, json };
7676
+ }
7677
+ function printUsage5(io) {
7678
+ io.err([
7679
+ "usage: launchpad recover [<slug>] [--slug <slug>] [--json]",
7680
+ "",
7681
+ " Repair an app whose registry record is stuck at a terminal",
7682
+ " provisioning failure although the app is actually live (e.g. a",
7683
+ " since-fixed platform bug failed the record after content shipped).",
7684
+ "",
7685
+ " The bot verifies LIVE Cloudflare state first: the record is only",
7686
+ " repaired when the Pages project exists and a successful production",
7687
+ " deployment is serving. A not-live app is refused with what was",
7688
+ " checked — recover never fabricates a live state.",
7689
+ "",
7690
+ " Recovering an already-healthy app is a no-op success.",
7691
+ "",
7692
+ "Flags:",
7693
+ " --slug <slug> override cwd inference.",
7694
+ " --json emit machine-readable JSON to stdout."
7695
+ ].join(`
7696
+ `));
7697
+ }
7698
+
7459
7699
  // src/commands/status.ts
7460
- import { readFileSync as readFileSync11 } from "node:fs";
7700
+ import { readFileSync as readFileSync12 } from "node:fs";
7461
7701
  var statusCommand = {
7462
7702
  name: "status",
7463
7703
  summary: "show drift between local launchpad.yaml and deployed state",
7464
7704
  run: runStatus
7465
7705
  };
7466
- var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7706
+ var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7467
7707
  async function fetchLifecycle(cfg, slug) {
7468
7708
  try {
7469
7709
  return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
@@ -7495,10 +7735,10 @@ function mapBotError(e, slug, io) {
7495
7735
  return 2;
7496
7736
  }
7497
7737
  async function runStatus(args, io) {
7498
- const parsed = parseArgs9(args);
7738
+ const parsed = parseArgs9(args, process.cwd(), (l) => io.err(l));
7499
7739
  if (typeof parsed === "string") {
7500
7740
  io.err(`launchpad status: ${parsed}`);
7501
- printUsage5(io);
7741
+ printUsage6(io);
7502
7742
  return 64;
7503
7743
  }
7504
7744
  const cfg = loadConfig();
@@ -7512,34 +7752,36 @@ async function runStatus(args, io) {
7512
7752
  emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
7513
7753
  return 0;
7514
7754
  }
7515
- let localYaml;
7755
+ let localYaml = null;
7516
7756
  try {
7517
- localYaml = readFileSync11(parsed.file, "utf8");
7757
+ localYaml = readFileSync12(parsed.file, "utf8");
7518
7758
  } catch (e) {
7519
- io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
7520
- if (lifecycle !== null && lifecycle.state === "live") {
7521
- io.err(` (app "${parsed.slug}" is live — cd into its launchpad-app-${parsed.slug}/ to check drift.)`);
7759
+ if (!isEnoent(e)) {
7760
+ io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
7761
+ return 2;
7522
7762
  }
7523
- return 2;
7524
- }
7525
- let localObj;
7526
- try {
7527
- const { parse: parseYaml5 } = await import("yaml");
7528
- localObj = parseYaml5(localYaml);
7529
- } catch (e) {
7530
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
7531
- return 2;
7532
7763
  }
7533
- const localParse = parseManifest(localObj);
7534
- if (localParse.kind !== "ok") {
7535
- io.err(`launchpad status: ${parsed.file} failed schema validation:`);
7536
- for (const issue of localParse.issues) {
7537
- io.err(` - ${issue.path}: ${issue.message}`);
7764
+ let local = null;
7765
+ if (localYaml !== null) {
7766
+ let localObj;
7767
+ try {
7768
+ const { parse: parseYaml6 } = await import("yaml");
7769
+ localObj = parseYaml6(localYaml);
7770
+ } catch (e) {
7771
+ io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
7772
+ return 2;
7538
7773
  }
7539
- return 2;
7774
+ const localParse = parseManifest(localObj);
7775
+ if (localParse.kind !== "ok") {
7776
+ io.err(`launchpad status: ${parsed.file} failed schema validation:`);
7777
+ for (const issue of localParse.issues) {
7778
+ io.err(` - ${issue.path}: ${issue.message}`);
7779
+ }
7780
+ return 2;
7781
+ }
7782
+ local = localParse.manifest;
7783
+ warnSecretShape(local.production_env, io);
7540
7784
  }
7541
- const local = localParse.manifest;
7542
- warnSecretShape(local.production_env, io);
7543
7785
  let state;
7544
7786
  try {
7545
7787
  state = await fetchManifestState(cfg, parsed.slug, { includeManifest: true });
@@ -7557,6 +7799,15 @@ async function runStatus(args, io) {
7557
7799
  }
7558
7800
  io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
7559
7801
  }
7802
+ let standingExceptions = null;
7803
+ try {
7804
+ standingExceptions = await fetchStandingExceptions(cfg, parsed.slug);
7805
+ } catch (e) {
7806
+ if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7807
+ return mapBotError(e, parsed.slug, io);
7808
+ }
7809
+ io.err(`launchpad status: standing-exception inventory unavailable (${describe24(e)}).`);
7810
+ }
7560
7811
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
7561
7812
  const live = deployment?.liveDeployment ?? null;
7562
7813
  const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
@@ -7564,21 +7815,47 @@ async function runStatus(args, io) {
7564
7815
  const out = {
7565
7816
  state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
7566
7817
  slug: parsed.slug,
7818
+ ...local === null ? { drift: null } : {},
7819
+ deployedSha: state.lastAppliedManifestSha,
7820
+ headSha: state.appRepoHeadSha,
7821
+ hasOpenPr: state.openPr !== null,
7822
+ openPrNumber: state.openPr?.number ?? null,
7823
+ driftFields: [],
7824
+ driftDetails: [],
7825
+ ...deploymentKnown ? { deployment } : {},
7826
+ ...standingExceptions !== null ? { standingExceptions } : {}
7827
+ };
7828
+ emit3(out, parsed.json, io);
7829
+ return 0;
7830
+ }
7831
+ if (local === null) {
7832
+ const out = {
7833
+ state: "live_drift_unknown",
7834
+ slug: parsed.slug,
7835
+ drift: null,
7567
7836
  deployedSha: state.lastAppliedManifestSha,
7568
7837
  headSha: state.appRepoHeadSha,
7569
7838
  hasOpenPr: state.openPr !== null,
7570
7839
  openPrNumber: state.openPr?.number ?? null,
7571
7840
  driftFields: [],
7572
7841
  driftDetails: [],
7573
- ...deploymentKnown ? { deployment } : {}
7842
+ ...deploymentKnown ? { deployment } : {},
7843
+ ...standingExceptions !== null ? { standingExceptions } : {}
7574
7844
  };
7575
7845
  emit3(out, parsed.json, io);
7846
+ if (parsed.strict) {
7847
+ if (deployment?.liveDeployment?.buildStatus === "failure") {
7848
+ io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
7849
+ return 1;
7850
+ }
7851
+ io.err(`launchpad status: --strict: drift not evaluated — no local ` + `launchpad.yaml here; live state looks healthy, exiting 0.`);
7852
+ }
7576
7853
  return 0;
7577
7854
  }
7578
7855
  let deployedObj;
7579
7856
  try {
7580
- const { parse: parseYaml5 } = await import("yaml");
7581
- deployedObj = parseYaml5(state.manifestYaml);
7857
+ const { parse: parseYaml6 } = await import("yaml");
7858
+ deployedObj = parseYaml6(state.manifestYaml);
7582
7859
  } catch (e) {
7583
7860
  io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
7584
7861
  return 2;
@@ -7602,7 +7879,8 @@ async function runStatus(args, io) {
7602
7879
  openPrNumber: state.openPr?.number ?? null,
7603
7880
  driftFields: drift.map((d) => d.path),
7604
7881
  driftDetails: drift,
7605
- ...deploymentKnown ? { deployment } : {}
7882
+ ...deploymentKnown ? { deployment } : {},
7883
+ ...standingExceptions !== null ? { standingExceptions } : {}
7606
7884
  };
7607
7885
  emit3(result, parsed.json, io);
7608
7886
  if (result.state === "drift" && parsed.strict) {
@@ -7612,17 +7890,17 @@ async function runStatus(args, io) {
7612
7890
  }
7613
7891
  function computeDrift(local, deployed) {
7614
7892
  const diffs = [];
7615
- const cmp = (path10, l, d) => {
7893
+ const cmp = (path11, l, d) => {
7616
7894
  const li = l ?? null;
7617
7895
  const di = d ?? null;
7618
7896
  if (typeof li === "object" || typeof di === "object") {
7619
7897
  if (JSON.stringify(li) !== JSON.stringify(di)) {
7620
- diffs.push({ path: path10, local: l, deployed: d });
7898
+ diffs.push({ path: path11, local: l, deployed: d });
7621
7899
  }
7622
7900
  return;
7623
7901
  }
7624
7902
  if (li !== di) {
7625
- diffs.push({ path: path10, local: l, deployed: d });
7903
+ diffs.push({ path: path11, local: l, deployed: d });
7626
7904
  }
7627
7905
  };
7628
7906
  cmp("metadata.name", local.metadata.name, deployed.metadata.name);
@@ -7687,23 +7965,37 @@ function emit3(out, asJson, io) {
7687
7965
  return;
7688
7966
  case "live_no_content":
7689
7967
  io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
7968
+ surfaceNoLocalManifestNote(out, io);
7690
7969
  surfaceHeadVsDeployed(out, io);
7691
7970
  surfaceDeployment(out, io);
7971
+ surfaceExceptions(out, io);
7692
7972
  return;
7693
7973
  case "no_deployed_manifest":
7694
7974
  io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
7975
+ surfaceNoLocalManifestNote(out, io);
7695
7976
  surfaceHeadVsDeployed(out, io);
7696
7977
  surfaceDeployment(out, io);
7978
+ surfaceExceptions(out, io);
7697
7979
  return;
7698
7980
  case "live_content_untracked":
7699
7981
  io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
7982
+ surfaceNoLocalManifestNote(out, io);
7700
7983
  surfaceHeadVsDeployed(out, io);
7701
7984
  surfaceDeployment(out, io);
7985
+ surfaceExceptions(out, io);
7986
+ return;
7987
+ case "live_drift_unknown":
7988
+ io.out(`${out.slug}: live` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
7989
+ surfaceNoLocalManifestNote(out, io);
7990
+ surfaceHeadVsDeployed(out, io);
7991
+ surfaceDeployment(out, io);
7992
+ surfaceExceptions(out, io);
7702
7993
  return;
7703
7994
  case "in_sync":
7704
7995
  io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
7705
7996
  surfaceHeadVsDeployed(out, io);
7706
7997
  surfaceDeployment(out, io);
7998
+ surfaceExceptions(out, io);
7707
7999
  return;
7708
8000
  case "drift":
7709
8001
  io.out(`${out.slug}: live, drift: ${out.driftFields.join(", ")}`);
@@ -7714,9 +8006,15 @@ function emit3(out, asJson, io) {
7714
8006
  }
7715
8007
  surfaceHeadVsDeployed(out, io);
7716
8008
  surfaceDeployment(out, io);
8009
+ surfaceExceptions(out, io);
7717
8010
  return;
7718
8011
  }
7719
8012
  }
8013
+ function surfaceNoLocalManifestNote(out, io) {
8014
+ if (out.drift !== null)
8015
+ return;
8016
+ io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
8017
+ }
7720
8018
  function triggerLabel(trigger) {
7721
8019
  if (trigger === "git-push")
7722
8020
  return "git push";
@@ -7763,6 +8061,16 @@ function surfaceDeployment(out, io) {
7763
8061
  }
7764
8062
  }
7765
8063
  }
8064
+ function surfaceExceptions(out, io) {
8065
+ const exceptions = out.standingExceptions;
8066
+ if (exceptions === undefined || exceptions.length === 0)
8067
+ return;
8068
+ io.out(` standing exceptions: ${exceptions.length} policy violation(s) in content already live on main (non-blocking):`);
8069
+ for (const e of exceptions) {
8070
+ io.out(` - ${e.path} [${e.rule}]`);
8071
+ }
8072
+ io.out(" these never block a deploy that doesn't change them, but they are never grandfathered silently — clean them up in a future deploy.");
8073
+ }
7766
8074
  function surfaceHeadVsDeployed(out, io) {
7767
8075
  if (out.headSha !== null && out.deployedSha !== null && out.headSha !== out.deployedSha) {
7768
8076
  const short = (s) => s.slice(0, 7);
@@ -7794,7 +8102,7 @@ function warnSecretShape(env, io) {
7794
8102
  }
7795
8103
  }
7796
8104
  }
7797
- function parseArgs9(args, cwd = process.cwd()) {
8105
+ function parseArgs9(args, cwd = process.cwd(), warn) {
7798
8106
  let slug = null;
7799
8107
  let file = "./launchpad.yaml";
7800
8108
  let json = false;
@@ -7841,54 +8149,68 @@ function parseArgs9(args, cwd = process.cwd()) {
7841
8149
  i += 1;
7842
8150
  }
7843
8151
  if (slug === null) {
7844
- const inferred = inferSlugFromCwd(cwd);
8152
+ const inferred = inferSlug({ cwd, file, warn });
7845
8153
  if (inferred === null) {
7846
8154
  return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7847
8155
  }
7848
8156
  slug = inferred;
7849
8157
  }
7850
- if (!SLUG_RE10.test(slug)) {
7851
- return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
8158
+ if (!SLUG_RE11.test(slug)) {
8159
+ return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
7852
8160
  }
7853
8161
  return { slug, file, json, strict };
7854
8162
  }
7855
- function printUsage5(io) {
8163
+ function printUsage6(io) {
7856
8164
  io.err([
7857
8165
  "usage: launchpad status [<slug>] [--slug <slug>] [--file <path>] [--json] [--strict]",
7858
8166
  "",
7859
8167
  " Compare local launchpad.yaml against the deployed state.",
7860
8168
  " No local platform-repo or terraform required.",
7861
8169
  "",
7862
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7863
- " the directory name. Explicit --slug or positional override.",
8170
+ " With no explicit slug, it is inferred from (in order): the",
8171
+ " local manifest's declared slug (./launchpad.yaml or --file),",
8172
+ " then a launchpad-app-<slug>/ directory name. An explicit",
8173
+ " --slug or positional always overrides; when the manifest and",
8174
+ " the directory name disagree, the manifest wins (with a note).",
8175
+ "",
8176
+ " With a known slug but NO local manifest, status degrades to",
8177
+ " the live-truth-only view (lifecycle + deployment; drift not",
8178
+ " checked) and exits 0.",
7864
8179
  "",
7865
8180
  "Flags:",
7866
8181
  " --file <path> local manifest path (default: ./launchpad.yaml).",
7867
- " Does NOT influence slug resolution.",
7868
- " --slug <slug> override cwd inference.",
8182
+ " Also consulted for slug inference; --slug or a",
8183
+ " positional slug still overrides.",
8184
+ " --slug <slug> override inference.",
7869
8185
  " --json emit machine-readable JSON to stdout.",
7870
8186
  " --strict exit 1 on drift. Default is report-only (exit 0).",
8187
+ " With no local manifest, drift can't be evaluated:",
8188
+ " exit 0 unless the live build itself failed.",
7871
8189
  "",
7872
8190
  "Exit codes:",
7873
- " 0 = in sync, OR drift in default (report-only) mode.",
7874
- " 1 = drift, when --strict is set.",
7875
- " 2 = error (network, auth, missing local manifest, etc.)."
8191
+ " 0 = in sync, OR drift in default (report-only) mode, OR the",
8192
+ " live-truth-only view (no local manifest).",
8193
+ " 1 = drift, when --strict is set (or a failed live build under",
8194
+ " --strict with no local manifest).",
8195
+ " 2 = error (network, auth, unreadable/invalid local manifest, etc.)."
7876
8196
  ].join(`
7877
8197
  `));
7878
8198
  }
7879
8199
  function describe24(e) {
7880
8200
  return e instanceof Error ? e.message : String(e);
7881
8201
  }
8202
+ function isEnoent(e) {
8203
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
8204
+ }
7882
8205
 
7883
8206
  // src/commands/review.ts
7884
- import * as path10 from "node:path";
8207
+ import * as path11 from "node:path";
7885
8208
  var reviewCommand = {
7886
8209
  name: "review",
7887
8210
  summary: "show the review state for a PR (slug-scoped)",
7888
8211
  run: runReview
7889
8212
  };
7890
- var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7891
- var DIRNAME_RE5 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
8213
+ var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7892
8214
  async function runReview(args, io) {
7893
8215
  const parsed = parseArgs10(args);
7894
8216
  if (parsed === null) {
@@ -7900,16 +8222,16 @@ async function runReview(args, io) {
7900
8222
  if (parsed.slug !== null) {
7901
8223
  slug = parsed.slug;
7902
8224
  } else {
7903
- const inferred = inferSlugFromCwd5(process.cwd());
8225
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
7904
8226
  if (inferred === null) {
7905
- io.err(`launchpad review: could not infer slug from cwd (${path10.basename(process.cwd())});
8227
+ io.err(`launchpad review: could not infer slug from cwd (${path11.basename(process.cwd())});
7906
8228
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
7907
8229
  return 64;
7908
8230
  }
7909
8231
  slug = inferred;
7910
8232
  }
7911
- if (!SLUG_RE11.test(slug)) {
7912
- io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE11.source}`);
8233
+ if (!SLUG_RE12.test(slug)) {
8234
+ io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE12.source}`);
7913
8235
  return 64;
7914
8236
  }
7915
8237
  try {
@@ -7984,11 +8306,6 @@ function parseArgs10(args) {
7984
8306
  }
7985
8307
  return { slug, prNumber };
7986
8308
  }
7987
- function inferSlugFromCwd5(cwd) {
7988
- const base = path10.basename(cwd);
7989
- const m = base.match(DIRNAME_RE5);
7990
- return m === null ? null : m[1];
7991
- }
7992
8309
  function renderReview(r, io) {
7993
8310
  const rv = r.review;
7994
8311
  io.out(`Review of PR #${rv.prNumber} (${rv.slug})`);
@@ -8040,7 +8357,7 @@ function describe25(e) {
8040
8357
  }
8041
8358
 
8042
8359
  // src/deploy/rollback.ts
8043
- import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
8360
+ import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
8044
8361
  import { resolve as resolvePath2 } from "node:path";
8045
8362
  import { createInterface as createInterface3 } from "node:readline/promises";
8046
8363
 
@@ -8160,19 +8477,20 @@ async function runRollback(opts, io, deps = {}) {
8160
8477
  file: opts.file,
8161
8478
  platformRepo: null,
8162
8479
  rePin: false,
8480
+ atSha: verifiedSha,
8163
8481
  yes: true
8164
8482
  }, io, applyDeps);
8165
8483
  }
8166
- function readCurrentManifest(path11) {
8167
- if (!existsSync7(path11))
8484
+ function readCurrentManifest(path12) {
8485
+ if (!existsSync8(path12))
8168
8486
  return null;
8169
8487
  let raw;
8170
8488
  try {
8171
- raw = readFileSync12(path11, "utf8");
8489
+ raw = readFileSync13(path12, "utf8");
8172
8490
  } catch {
8173
8491
  return null;
8174
8492
  }
8175
- const parsed = parseManifest2(raw, path11);
8493
+ const parsed = parseManifest2(raw, path12);
8176
8494
  if (parsed.kind !== "ok")
8177
8495
  return null;
8178
8496
  return summarise(parsed.manifest);
@@ -8324,7 +8642,7 @@ function parseArgs11(args) {
8324
8642
  }
8325
8643
 
8326
8644
  // src/secrets/push.ts
8327
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
8645
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8328
8646
  import { resolve as resolvePath3 } from "node:path";
8329
8647
 
8330
8648
  // src/secrets/env-parse.ts
@@ -8379,14 +8697,14 @@ async function runSecretsPush(opts, io, deps = {}) {
8379
8697
  return 0;
8380
8698
  }
8381
8699
  const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
8382
- if (!existsSync8(envPath)) {
8700
+ if (!existsSync9(envPath)) {
8383
8701
  io.err(`launchpad secrets push: ${envPath}`);
8384
8702
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
8385
8703
  return 2;
8386
8704
  }
8387
8705
  let envText;
8388
8706
  try {
8389
- envText = readFileSync13(envPath, "utf8");
8707
+ envText = readFileSync14(envPath, "utf8");
8390
8708
  } catch (e) {
8391
8709
  io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
8392
8710
  return 2;
@@ -8658,32 +8976,32 @@ function renderManifestError5(result, io) {
8658
8976
  }
8659
8977
 
8660
8978
  // src/secrets/set.ts
8661
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8979
+ import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
8662
8980
  import { resolve as resolvePath5 } from "node:path";
8663
- import { parse as parseYaml5 } from "yaml";
8981
+ import { parse as parseYaml6 } from "yaml";
8664
8982
  var CELL_LABEL2 = {
8665
8983
  present: "PRESENT",
8666
8984
  missing: "MISSING",
8667
8985
  not_deployed: "NOT_DEPLOYED"
8668
8986
  };
8669
8987
  function loadSet(fleetFile, setName, io) {
8670
- const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8671
- if (!existsSync9(path11)) {
8672
- io.err(`✗ ${path11}`);
8988
+ const path12 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8989
+ if (!existsSync10(path12)) {
8990
+ io.err(`✗ ${path12}`);
8673
8991
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
8674
8992
  return 2;
8675
8993
  }
8676
8994
  let obj;
8677
8995
  try {
8678
- obj = parseYaml5(readFileSync14(path11, "utf8"));
8996
+ obj = parseYaml6(readFileSync15(path12, "utf8"));
8679
8997
  } catch (e) {
8680
- io.err(`✗ ${path11}`);
8998
+ io.err(`✗ ${path12}`);
8681
8999
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
8682
9000
  return 1;
8683
9001
  }
8684
9002
  const parsed = parseFleetSecretSets(obj);
8685
9003
  if (!parsed.ok) {
8686
- io.err(`✗ ${path11}`);
9004
+ io.err(`✗ ${path12}`);
8687
9005
  io.err(` ${parsed.issues.length} schema issue(s):`);
8688
9006
  for (const i of parsed.issues)
8689
9007
  io.err(` ${i.path}: ${i.message}`);
@@ -8692,7 +9010,7 @@ function loadSet(fleetFile, setName, io) {
8692
9010
  const set = parsed.manifest.secretSets.find((s) => s.name === setName);
8693
9011
  if (set === undefined) {
8694
9012
  const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
8695
- io.err(`✗ secret-set "${setName}" not found in ${path11}`);
9013
+ io.err(`✗ secret-set "${setName}" not found in ${path12}`);
8696
9014
  io.err(` declared sets: ${names}`);
8697
9015
  return 1;
8698
9016
  }
@@ -8760,14 +9078,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
8760
9078
  if (typeof set === "number")
8761
9079
  return set;
8762
9080
  const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
8763
- if (!existsSync9(envPath)) {
9081
+ if (!existsSync10(envPath)) {
8764
9082
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
8765
9083
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
8766
9084
  return 2;
8767
9085
  }
8768
9086
  let envText;
8769
9087
  try {
8770
- envText = readFileSync14(envPath, "utf8");
9088
+ envText = readFileSync15(envPath, "utf8");
8771
9089
  } catch (e) {
8772
9090
  io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
8773
9091
  return 2;
@@ -8911,8 +9229,8 @@ function setPushExit(e) {
8911
9229
  }
8912
9230
 
8913
9231
  // src/commands/secrets-template.ts
8914
- import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
8915
- import { resolve as resolve9 } from "node:path";
9232
+ import { existsSync as existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
9233
+ import { resolve as resolve10 } from "node:path";
8916
9234
  async function runSecretsTemplate(args, io) {
8917
9235
  const flags = parseFlags4(args);
8918
9236
  if (flags.kind === "usage-error") {
@@ -8920,7 +9238,7 @@ async function runSecretsTemplate(args, io) {
8920
9238
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
8921
9239
  return 64;
8922
9240
  }
8923
- const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
9241
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
8924
9242
  const result = loadManifest(manifestPath);
8925
9243
  const renderResult = renderManifest(result, io);
8926
9244
  if (renderResult.kind !== "ok") {
@@ -8934,8 +9252,8 @@ async function runSecretsTemplate(args, io) {
8934
9252
  }
8935
9253
  return 0;
8936
9254
  }
8937
- const outPath = resolve9(process.cwd(), flags.out);
8938
- if (existsSync10(outPath) && !flags.force) {
9255
+ const outPath = resolve10(process.cwd(), flags.out);
9256
+ if (existsSync11(outPath) && !flags.force) {
8939
9257
  io.err(`launchpad secrets template: ${outPath} already exists`);
8940
9258
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
8941
9259
  return 64;
@@ -9223,8 +9541,8 @@ function printHelp2(io) {
9223
9541
 
9224
9542
  // src/commands/skills.ts
9225
9543
  import { fileURLToPath } from "node:url";
9226
- import { dirname as dirname6, join as join11, resolve as resolve10 } from "node:path";
9227
- import { promises as fs5, existsSync as existsSync11 } from "node:fs";
9544
+ import { dirname as dirname6, join as join11, resolve as resolve11 } from "node:path";
9545
+ import { promises as fs5, existsSync as existsSync12 } from "node:fs";
9228
9546
  import { homedir as homedir2 } from "node:os";
9229
9547
  var BUNDLE_PREFIX = "launchpad-";
9230
9548
  var BUNDLED_SKILLS = [
@@ -9291,11 +9609,11 @@ function resolveInstallEnv() {
9291
9609
  function defaultBundleDir() {
9292
9610
  const here = dirname6(fileURLToPath(import.meta.url));
9293
9611
  const candidates = [
9294
- resolve10(here, "..", "skills"),
9295
- resolve10(here, "..", "..", "skills")
9612
+ resolve11(here, "..", "skills"),
9613
+ resolve11(here, "..", "..", "skills")
9296
9614
  ];
9297
9615
  for (const c of candidates) {
9298
- if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
9616
+ if (existsSync12(join11(c, "launchpad-onboard", "SKILL.md"))) {
9299
9617
  return c;
9300
9618
  }
9301
9619
  }
@@ -9375,10 +9693,10 @@ async function doList(io) {
9375
9693
  }
9376
9694
  return 0;
9377
9695
  }
9378
- async function readVersion(path11) {
9696
+ async function readVersion(path12) {
9379
9697
  let text;
9380
9698
  try {
9381
- text = await fs5.readFile(path11, "utf8");
9699
+ text = await fs5.readFile(path12, "utf8");
9382
9700
  } catch {
9383
9701
  return null;
9384
9702
  }
@@ -9388,9 +9706,9 @@ async function readVersion(path11) {
9388
9706
  const m = /^version:\s*(.+?)\s*$/m.exec(front);
9389
9707
  return m === null ? null : m[1] ?? null;
9390
9708
  }
9391
- async function isDir(path11) {
9709
+ async function isDir(path12) {
9392
9710
  try {
9393
- const stat = await fs5.stat(path11);
9711
+ const stat = await fs5.stat(path12);
9394
9712
  return stat.isDirectory();
9395
9713
  } catch {
9396
9714
  return false;
@@ -9404,9 +9722,9 @@ function describe28(e) {
9404
9722
  import { execFile, spawn as spawn5 } from "node:child_process";
9405
9723
  import { promisify } from "node:util";
9406
9724
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9407
- import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
9725
+ import { dirname as dirname7, resolve as resolve12, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
9408
9726
  import { homedir as homedir3, tmpdir } from "node:os";
9409
- import { readFileSync as readFileSync15, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
9727
+ import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
9410
9728
 
9411
9729
  // src/commands/channel-auth.ts
9412
9730
  import { createServer as createServer2 } from "node:http";
@@ -9449,9 +9767,9 @@ async function startLoopback(state, timeoutMs) {
9449
9767
  resolveCode(code);
9450
9768
  }
9451
9769
  });
9452
- const bound = await new Promise((resolve11) => {
9453
- server.once("error", () => resolve11(false));
9454
- server.listen(0, "127.0.0.1", () => resolve11(true));
9770
+ const bound = await new Promise((resolve12) => {
9771
+ server.once("error", () => resolve12(false));
9772
+ server.listen(0, "127.0.0.1", () => resolve12(true));
9455
9773
  });
9456
9774
  if (!bound)
9457
9775
  return null;
@@ -9635,7 +9953,7 @@ function resolveLatestVersion() {
9635
9953
  }
9636
9954
  function detectInstallChannel() {
9637
9955
  try {
9638
- return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9956
+ return readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9639
9957
  } catch {
9640
9958
  return "github";
9641
9959
  }
@@ -9698,7 +10016,7 @@ function errStderr(e) {
9698
10016
  return "";
9699
10017
  }
9700
10018
  async function detectPackageManager2() {
9701
- const pkgRoot = resolve11(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
10019
+ const pkgRoot = resolve12(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
9702
10020
  const candidates = [];
9703
10021
  const npmRoot = await pmRoot("npm");
9704
10022
  if (npmRoot !== null)
@@ -9706,7 +10024,7 @@ async function detectPackageManager2() {
9706
10024
  const pnpmRoot = await pmRoot("pnpm");
9707
10025
  if (pnpmRoot !== null)
9708
10026
  candidates.push(["pnpm", pnpmRoot]);
9709
- candidates.push(["bun", resolve11(homedir3(), ".bun/install/global/node_modules")]);
10027
+ candidates.push(["bun", resolve12(homedir3(), ".bun/install/global/node_modules")]);
9710
10028
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
9711
10029
  return matches.length === 1 ? matches[0][0] : null;
9712
10030
  }
@@ -9842,8 +10160,8 @@ function printHelp4(io) {
9842
10160
  }
9843
10161
 
9844
10162
  // src/commands/validate.ts
9845
- import { readFileSync as readFileSync16 } from "node:fs";
9846
- import { dirname as dirname8, resolve as resolve12 } from "node:path";
10163
+ import { readFileSync as readFileSync17 } from "node:fs";
10164
+ import { dirname as dirname8, resolve as resolve13 } from "node:path";
9847
10165
  var validateCommand = {
9848
10166
  name: "validate",
9849
10167
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -9857,12 +10175,12 @@ async function runValidate(args, io) {
9857
10175
  return 64;
9858
10176
  }
9859
10177
  const { file, json, strictGroups } = parseResult;
9860
- const path11 = resolve12(process.cwd(), file ?? "launchpad.yaml");
9861
- const result = loadManifest(path11);
10178
+ const path12 = resolve13(process.cwd(), file ?? "launchpad.yaml");
10179
+ const result = loadManifest(path12);
9862
10180
  if (result.kind !== "ok") {
9863
10181
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
9864
10182
  }
9865
- const boundary = checkBoundary(path11, result.manifest.app !== undefined);
10183
+ const boundary = checkBoundary(path12, result.manifest.app !== undefined);
9866
10184
  const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
9867
10185
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
9868
10186
  }
@@ -9876,7 +10194,7 @@ function checkBoundary(manifestPath, declared) {
9876
10194
  }
9877
10195
  let manifestYaml;
9878
10196
  try {
9879
- manifestYaml = readFileSync16(manifestPath, "utf8");
10197
+ manifestYaml = readFileSync17(manifestPath, "utf8");
9880
10198
  } catch {
9881
10199
  manifestYaml = null;
9882
10200
  }
@@ -10191,14 +10509,14 @@ function describe30(e) {
10191
10509
  import { spawn as spawn6 } from "node:child_process";
10192
10510
  import { homedir as homedir4 } from "node:os";
10193
10511
  import { join as join13 } from "node:path";
10194
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
10512
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
10195
10513
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
10196
10514
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10197
10515
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
10198
10516
  var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
10199
10517
  function readCache2() {
10200
10518
  try {
10201
- const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
10519
+ const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
10202
10520
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
10203
10521
  const latest = raw.latest;
10204
10522
  return {
@@ -10314,6 +10632,7 @@ var COMMANDS = [
10314
10632
  generateCommand,
10315
10633
  pullCommand,
10316
10634
  statusCommand,
10635
+ recoverCommand,
10317
10636
  destroyCommand,
10318
10637
  rollbackCommand,
10319
10638
  groupsCommand,