@m-kopa/launchpad-cli 0.25.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.
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.25.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";
@@ -3306,8 +3306,8 @@ async function bundleAndDeploy(args) {
3306
3306
  }
3307
3307
 
3308
3308
  // src/commands/deploy.ts
3309
- import { parse as parseYaml4 } from "yaml";
3310
- import { readFileSync as readFileSync6 } from "node:fs";
3309
+ import { parse as parseYaml5 } from "yaml";
3310
+ import { readFileSync as readFileSync7 } from "node:fs";
3311
3311
 
3312
3312
  // src/deploy/git-files.ts
3313
3313
  import { spawn as spawn3 } from "node:child_process";
@@ -4367,6 +4367,46 @@ function handleNetworkError(e, io, slug, verb) {
4367
4367
  return 1;
4368
4368
  }
4369
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
+
4370
4410
  // src/commands/deploy.ts
4371
4411
  var deployCommand = {
4372
4412
  name: "deploy",
@@ -4374,27 +4414,17 @@ var deployCommand = {
4374
4414
  run: runDeploy
4375
4415
  };
4376
4416
  var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4377
- var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4378
4417
  async function runDeploy(args, io) {
4379
4418
  const flags = parseDeployFlags(args);
4380
4419
  if (typeof flags === "string") {
4381
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
+ }
4382
4426
  io.err("");
4383
- io.err(`usage: launchpad deploy [--message <text>] [--slug <slug>]
4384
- ` + " (slug defaults to the current directory's `launchpad-app-<slug>` suffix)\n" + `
4385
- ` + `M-892 modes:
4386
- ` + ` launchpad deploy --resume <slug>
4387
- ` + ` launchpad deploy --abandon <slug>
4388
- ` + ` launchpad deploy --new --slug <slug> --display-name <name>
4389
- ` + ` --app-type <${APP_TYPES.join("|")}>
4390
- ` + ` --allowed-group <G_KEY> [--allowed-group ...]
4391
- ` + `
4392
- ` + `Scope 6 dry-run (manifest-driven preview, read-only):
4393
- ` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
4394
- ` + `
4395
- ` + `Scope 6 apply (manifest-driven, writes TF + runs terraform apply):
4396
- ` + ` launchpad deploy --apply --platform-repo <path>
4397
- ` + " [--file <manifest>] [--re-pin] [--yes]");
4427
+ io.err(deployUsage());
4398
4428
  return 64;
4399
4429
  }
4400
4430
  if (flags.mode.kind === "dry-run") {
@@ -4428,8 +4458,8 @@ async function runDeploy(args, io) {
4428
4458
  }, io);
4429
4459
  }
4430
4460
  const cwd = process.cwd();
4431
- const manifestPath = path6.join(cwd, "launchpad.yaml");
4432
- if (existsSync4(manifestPath)) {
4461
+ const manifestPath = path7.join(cwd, "launchpad.yaml");
4462
+ if (existsSync5(manifestPath)) {
4433
4463
  return runModelADeploy({ cwd, manifestPath, argv: args, io });
4434
4464
  }
4435
4465
  const parsed = parseArgs2(args);
@@ -4441,9 +4471,9 @@ async function runDeploy(args, io) {
4441
4471
  if (parsed.slug !== null) {
4442
4472
  slug = parsed.slug;
4443
4473
  } else {
4444
- const inferred = inferSlugFromCwd(process.cwd());
4474
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4445
4475
  if (inferred === null) {
4446
- 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())});
4447
4477
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4448
4478
  return 64;
4449
4479
  }
@@ -4463,10 +4493,10 @@ async function runDeploy(args, io) {
4463
4493
  return 1;
4464
4494
  }
4465
4495
  let contentManifestYaml = null;
4466
- const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
4467
- if (existsSync4(contentManifestPath)) {
4496
+ const contentManifestPath = path7.join(process.cwd(), "launchpad.yaml");
4497
+ if (existsSync5(contentManifestPath)) {
4468
4498
  try {
4469
- contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
4499
+ contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
4470
4500
  } catch {
4471
4501
  contentManifestYaml = null;
4472
4502
  }
@@ -4544,6 +4574,24 @@ async function runDeploy(args, io) {
4544
4574
  return 1;
4545
4575
  }
4546
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
+ }
4547
4595
  function parseArgs2(args) {
4548
4596
  let message = null;
4549
4597
  let slug = null;
@@ -4570,11 +4618,6 @@ function parseArgs2(args) {
4570
4618
  }
4571
4619
  return { slug, message };
4572
4620
  }
4573
- function inferSlugFromCwd(cwd) {
4574
- const base = path6.basename(cwd);
4575
- const m = base.match(DIRNAME_RE);
4576
- return m === null ? null : m[1];
4577
- }
4578
4621
  function makeBytesFetcher(bytes, pathSuffix) {
4579
4622
  return async (input, init) => {
4580
4623
  const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -4624,22 +4667,11 @@ function surfaceDeployExtras(body, io, slug) {
4624
4667
  io.out(` Full list: \`launchpad status ${slug}\`.`);
4625
4668
  }
4626
4669
  }
4627
- function resolveManifestSlug(parsed) {
4628
- if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
4629
- return null;
4630
- }
4631
- const meta = parsed.metadata;
4632
- if (typeof meta.slug === "string")
4633
- return meta.slug;
4634
- if (typeof meta.name === "string")
4635
- return meta.name;
4636
- return null;
4637
- }
4638
4670
  async function runModelADeploy(args) {
4639
4671
  const { cwd, manifestPath, io } = args;
4640
4672
  let slug;
4641
4673
  try {
4642
- const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4674
+ const metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
4643
4675
  if (metaSlug === null) {
4644
4676
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
4645
4677
  return 64;
@@ -4715,6 +4747,11 @@ async function runModelADeploy(args) {
4715
4747
  io.err(` - ${String(f.path)} [${String(f.rule)}]`);
4716
4748
  }
4717
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
+ }
4718
4755
  return 1;
4719
4756
  }
4720
4757
  case "ok": {
@@ -4771,14 +4808,13 @@ async function runModelADeploy(args) {
4771
4808
  }
4772
4809
 
4773
4810
  // src/commands/envvars.ts
4774
- import * as path7 from "node:path";
4811
+ import * as path8 from "node:path";
4775
4812
  var envvarsCommand = {
4776
4813
  name: "envvars",
4777
4814
  summary: "list / set / remove production env vars (slug-scoped)",
4778
4815
  run: runEnvvars
4779
4816
  };
4780
4817
  var SLUG_RE5 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
4781
- var DIRNAME_RE2 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4782
4818
  var ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
4783
4819
  async function runEnvvars(args, io) {
4784
4820
  const parsed = parseArgs3(args);
@@ -4790,9 +4826,9 @@ async function runEnvvars(args, io) {
4790
4826
  if (parsed.slug !== null) {
4791
4827
  slug = parsed.slug;
4792
4828
  } else {
4793
- const inferred = inferSlugFromCwd2(process.cwd());
4829
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4794
4830
  if (inferred === null) {
4795
- 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())});
4796
4832
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4797
4833
  return 64;
4798
4834
  }
@@ -4938,11 +4974,6 @@ function parseArgs3(args) {
4938
4974
  }
4939
4975
  return null;
4940
4976
  }
4941
- function inferSlugFromCwd2(cwd) {
4942
- const base = path7.basename(cwd);
4943
- const m = base.match(DIRNAME_RE2);
4944
- return m === null ? null : m[1];
4945
- }
4946
4977
  function renderList(envVars, io) {
4947
4978
  if (envVars.length === 0) {
4948
4979
  io.out("(no env vars set)");
@@ -4967,8 +4998,8 @@ function describe13(e) {
4967
4998
  }
4968
4999
 
4969
5000
  // src/commands/generate.ts
4970
- import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
4971
- 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";
4972
5003
  var generateCommand = {
4973
5004
  name: "generate",
4974
5005
  summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
@@ -4981,14 +5012,14 @@ async function runGenerate(args, io) {
4981
5012
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
4982
5013
  return 64;
4983
5014
  }
4984
- const manifestPath = resolve6(process.cwd(), flags.file ?? "launchpad.yaml");
5015
+ const manifestPath = resolve7(process.cwd(), flags.file ?? "launchpad.yaml");
4985
5016
  const result = loadManifest(manifestPath);
4986
5017
  if (result.kind !== "ok") {
4987
5018
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
4988
5019
  }
4989
5020
  const appRoot = dirname4(manifestPath);
4990
- const wranglerPath = resolve6(appRoot, "container", "wrangler.toml");
4991
- 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");
4992
5023
  const wranglerOut = generateWranglerToml(result.manifest);
4993
5024
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
4994
5025
  if (flags.dryRun) {
@@ -5003,7 +5034,7 @@ async function runGenerate(args, io) {
5003
5034
  ];
5004
5035
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
5005
5036
  }
5006
- function applyOne(artefact, path8, out, force) {
5037
+ function applyOne(artefact, path9, out, force) {
5007
5038
  if (out.kind === "not-applicable") {
5008
5039
  return {
5009
5040
  artefact,
@@ -5011,32 +5042,32 @@ function applyOne(artefact, path8, out, force) {
5011
5042
  warnings: []
5012
5043
  };
5013
5044
  }
5014
- const existing = readIfExists(path8);
5045
+ const existing = readIfExists(path9);
5015
5046
  if (existing.kind === "read-error") {
5016
5047
  return {
5017
5048
  artefact,
5018
- action: { kind: "write-error", path: path8, message: existing.message },
5049
+ action: { kind: "write-error", path: path9, message: existing.message },
5019
5050
  warnings: out.warnings
5020
5051
  };
5021
5052
  }
5022
5053
  if (existing.kind === "ok" && existing.content === out.content) {
5023
- return { artefact, action: { kind: "unchanged", path: path8 }, warnings: out.warnings };
5054
+ return { artefact, action: { kind: "unchanged", path: path9 }, warnings: out.warnings };
5024
5055
  }
5025
5056
  if (existing.kind === "ok" && existing.content !== out.content && !force) {
5026
5057
  return {
5027
5058
  artefact,
5028
- action: { kind: "would-overwrite", path: path8 },
5059
+ action: { kind: "would-overwrite", path: path9 },
5029
5060
  warnings: out.warnings
5030
5061
  };
5031
5062
  }
5032
5063
  try {
5033
- mkdirSync(dirname4(path8), { recursive: true });
5034
- writeFileSync(path8, out.content, "utf8");
5064
+ mkdirSync(dirname4(path9), { recursive: true });
5065
+ writeFileSync(path9, out.content, "utf8");
5035
5066
  return {
5036
5067
  artefact,
5037
5068
  action: {
5038
5069
  kind: "written",
5039
- path: path8,
5070
+ path: path9,
5040
5071
  bytes: Buffer.byteLength(out.content, "utf8")
5041
5072
  },
5042
5073
  warnings: out.warnings
@@ -5046,16 +5077,16 @@ function applyOne(artefact, path8, out, force) {
5046
5077
  artefact,
5047
5078
  action: {
5048
5079
  kind: "write-error",
5049
- path: path8,
5080
+ path: path9,
5050
5081
  message: err.message ?? String(err)
5051
5082
  },
5052
5083
  warnings: out.warnings
5053
5084
  };
5054
5085
  }
5055
5086
  }
5056
- function readIfExists(path8) {
5087
+ function readIfExists(path9) {
5057
5088
  try {
5058
- return { kind: "ok", content: readFileSync7(path8, "utf8") };
5089
+ return { kind: "ok", content: readFileSync8(path9, "utf8") };
5059
5090
  } catch (err) {
5060
5091
  const e = err;
5061
5092
  if (e.code === "ENOENT")
@@ -5266,7 +5297,7 @@ function parseFlags(args) {
5266
5297
  }
5267
5298
 
5268
5299
  // src/groups/client.ts
5269
- 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";
5270
5301
  import { dirname as dirname5, join as join9 } from "node:path";
5271
5302
  var CACHE_TTL_MS = 60 * 60 * 1000;
5272
5303
  var CACHE_FILENAME = "groups.json";
@@ -5307,10 +5338,10 @@ async function fetchGroups(cfg, opts = {}) {
5307
5338
  writeCache(cachePath, { fetchedAt, groups });
5308
5339
  return { kind: "ok", source: "fresh", fetchedAt, groups };
5309
5340
  }
5310
- function readCache(path8) {
5341
+ function readCache(path9) {
5311
5342
  let raw;
5312
5343
  try {
5313
- raw = readFileSync8(path8, "utf8");
5344
+ raw = readFileSync9(path9, "utf8");
5314
5345
  } catch {
5315
5346
  return null;
5316
5347
  }
@@ -5338,10 +5369,10 @@ function isEntraGroup(value) {
5338
5369
  const g = value;
5339
5370
  return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
5340
5371
  }
5341
- function writeCache(path8, envelope) {
5372
+ function writeCache(path9, envelope) {
5342
5373
  try {
5343
- mkdirSync2(dirname5(path8), { recursive: true });
5344
- writeFileSync2(path8, JSON.stringify(envelope), "utf8");
5374
+ mkdirSync2(dirname5(path9), { recursive: true });
5375
+ writeFileSync2(path9, JSON.stringify(envelope), "utf8");
5345
5376
  } catch {}
5346
5377
  }
5347
5378
  function describe14(e) {
@@ -5811,17 +5842,17 @@ function describe17(e) {
5811
5842
 
5812
5843
  // src/commands/init.ts
5813
5844
  import { createInterface } from "node:readline/promises";
5814
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
5815
- 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";
5816
5847
  import { stringify as yamlStringify } from "yaml";
5817
5848
 
5818
5849
  // src/detect/index.ts
5819
- import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
5850
+ import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
5820
5851
  import { join as join10 } from "node:path";
5821
5852
  function detectAppShape(cwd) {
5822
- const hasPackageJson = existsSync5(join10(cwd, "package.json"));
5823
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync5(join10(cwd, file)));
5824
- 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)));
5825
5856
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
5826
5857
  return {
5827
5858
  kind: "not-applicable",
@@ -5867,7 +5898,7 @@ var LOCKFILES = [
5867
5898
  { file: "yarn.lock", pm: "yarn" }
5868
5899
  ];
5869
5900
  function detectPackageManager(cwd) {
5870
- const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
5901
+ const present = LOCKFILES.filter(({ file }) => existsSync6(join10(cwd, file)));
5871
5902
  if (present.length === 0) {
5872
5903
  return {
5873
5904
  kind: "ambiguous",
@@ -5890,7 +5921,7 @@ function detectPackageManager(cwd) {
5890
5921
  function detectVitePresence(cwd) {
5891
5922
  for (const name of VITE_CONFIG_NAMES) {
5892
5923
  const p = join10(cwd, name);
5893
- if (existsSync5(p)) {
5924
+ if (existsSync6(p)) {
5894
5925
  return { kind: "ok", value: { path: p } };
5895
5926
  }
5896
5927
  }
@@ -5902,7 +5933,7 @@ function detectVitePresence(cwd) {
5902
5933
  function detectAppType(cwd) {
5903
5934
  const fnDir = join10(cwd, "functions");
5904
5935
  let hasFunctionsDir = false;
5905
- if (existsSync5(fnDir)) {
5936
+ if (existsSync6(fnDir)) {
5906
5937
  try {
5907
5938
  hasFunctionsDir = statSync(fnDir).isDirectory();
5908
5939
  } catch {
@@ -5913,7 +5944,7 @@ function detectAppType(cwd) {
5913
5944
  }
5914
5945
  function detectBuildCommand(cwd, pm) {
5915
5946
  const pkgJsonPath = join10(cwd, "package.json");
5916
- if (!existsSync5(pkgJsonPath)) {
5947
+ if (!existsSync6(pkgJsonPath)) {
5917
5948
  return {
5918
5949
  kind: "ambiguous",
5919
5950
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -5921,7 +5952,7 @@ function detectBuildCommand(cwd, pm) {
5921
5952
  }
5922
5953
  let pkgJson;
5923
5954
  try {
5924
- pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
5955
+ pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
5925
5956
  } catch (e) {
5926
5957
  return {
5927
5958
  kind: "ambiguous",
@@ -5956,7 +5987,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
5956
5987
  function detectDestinationDir(cwd, vite) {
5957
5988
  let text;
5958
5989
  try {
5959
- text = readFileSync9(vite.path, "utf8");
5990
+ text = readFileSync10(vite.path, "utf8");
5960
5991
  } catch (e) {
5961
5992
  return {
5962
5993
  kind: "ambiguous",
@@ -5985,8 +6016,8 @@ async function runInit(args, io, prompt) {
5985
6016
  return 64;
5986
6017
  }
5987
6018
  const { inputs, options } = parsed;
5988
- const outPath = resolve7(process.cwd(), options.out);
5989
- if (existsSync6(outPath) && !options.force) {
6019
+ const outPath = resolve8(process.cwd(), options.out);
6020
+ if (existsSync7(outPath) && !options.force) {
5990
6021
  io.err(`launchpad init: ${outPath} already exists`);
5991
6022
  io.err("Pass --force to overwrite.");
5992
6023
  return 64;
@@ -6033,7 +6064,7 @@ async function runInit(args, io, prompt) {
6033
6064
  }
6034
6065
  io.out(`✓ wrote ${outPath}`);
6035
6066
  if (options.gitignore) {
6036
- const gitignorePath = resolve7(process.cwd(), ".gitignore");
6067
+ const gitignorePath = resolve8(process.cwd(), ".gitignore");
6037
6068
  try {
6038
6069
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
6039
6070
  if (changed.length > 0) {
@@ -6402,10 +6433,10 @@ function buildManifest(inputs, detected) {
6402
6433
  function renderYaml(manifest) {
6403
6434
  return yamlStringify(manifest, { lineWidth: 0 });
6404
6435
  }
6405
- function ensureGitignoreEntries(path8, entries) {
6436
+ function ensureGitignoreEntries(path9, entries) {
6406
6437
  let current = "";
6407
- if (existsSync6(path8)) {
6408
- current = readFileSync10(path8, "utf8");
6438
+ if (existsSync7(path9)) {
6439
+ current = readFileSync11(path9, "utf8");
6409
6440
  }
6410
6441
  const lines = current.split(/\r?\n/);
6411
6442
  const present = new Set(lines.map((l) => l.trim()));
@@ -6423,7 +6454,7 @@ function ensureGitignoreEntries(path8, entries) {
6423
6454
  }
6424
6455
  }
6425
6456
  if (added.length > 0) {
6426
- writeFileSync3(path8, out, { encoding: "utf8" });
6457
+ writeFileSync3(path9, out, { encoding: "utf8" });
6427
6458
  }
6428
6459
  return added;
6429
6460
  }
@@ -6495,14 +6526,13 @@ function describe19(e) {
6495
6526
  }
6496
6527
 
6497
6528
  // src/commands/logs.ts
6498
- import * as path8 from "node:path";
6529
+ import * as path9 from "node:path";
6499
6530
  var logsCommand = {
6500
6531
  name: "logs",
6501
6532
  summary: "show recent Pages deployment history (slug-scoped)",
6502
6533
  run: runLogs
6503
6534
  };
6504
6535
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
6505
- var DIRNAME_RE3 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
6506
6536
  var DEFAULT_LINES = 10;
6507
6537
  var MAX_LINES = 25;
6508
6538
  async function runLogs(args, io) {
@@ -6516,9 +6546,9 @@ async function runLogs(args, io) {
6516
6546
  if (parsed.slug !== null) {
6517
6547
  slug = parsed.slug;
6518
6548
  } else {
6519
- const inferred = inferSlugFromCwd3(process.cwd());
6549
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6520
6550
  if (inferred === null) {
6521
- 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())});
6522
6552
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6523
6553
  return 64;
6524
6554
  }
@@ -6596,11 +6626,6 @@ function parseArgs5(args) {
6596
6626
  }
6597
6627
  return { slug, lines };
6598
6628
  }
6599
- function inferSlugFromCwd3(cwd) {
6600
- const base = path8.basename(cwd);
6601
- const m = base.match(DIRNAME_RE3);
6602
- return m === null ? null : m[1];
6603
- }
6604
6629
  function renderDeployments(deployments, io) {
6605
6630
  if (deployments.length === 0) {
6606
6631
  io.out("(no deployments yet)");
@@ -6651,14 +6676,13 @@ function describe20(e) {
6651
6676
  }
6652
6677
 
6653
6678
  // src/commands/merge.ts
6654
- import * as path9 from "node:path";
6679
+ import * as path10 from "node:path";
6655
6680
  var mergeCommand = {
6656
6681
  name: "merge",
6657
6682
  summary: "squash-merge a review-passed PR (slug-scoped)",
6658
6683
  run: runMerge
6659
6684
  };
6660
6685
  var SLUG_RE7 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
6661
- var DIRNAME_RE4 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
6662
6686
  var HANDLED_STATUSES = [409, 422, 502, 503];
6663
6687
  async function runMerge(args, io) {
6664
6688
  const parsed = parseArgs6(args);
@@ -6671,9 +6695,9 @@ async function runMerge(args, io) {
6671
6695
  if (parsed.slug !== null) {
6672
6696
  slug = parsed.slug;
6673
6697
  } else {
6674
- const inferred = inferSlugFromCwd4(process.cwd());
6698
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6675
6699
  if (inferred === null) {
6676
- 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())});
6677
6701
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6678
6702
  return 64;
6679
6703
  }
@@ -6770,11 +6794,6 @@ function parseArgs6(args) {
6770
6794
  return null;
6771
6795
  return { slug, prNumber };
6772
6796
  }
6773
- function inferSlugFromCwd4(cwd) {
6774
- const base = path9.basename(cwd);
6775
- const m = base.match(DIRNAME_RE4);
6776
- return m === null ? null : m[1];
6777
- }
6778
6797
  function renderBotError(status, env, io, prNumber = null) {
6779
6798
  const code = env?.error ?? "unknown";
6780
6799
  const detail = env?.message;
@@ -6830,7 +6849,7 @@ function describe21(e) {
6830
6849
  }
6831
6850
 
6832
6851
  // src/commands/plan.ts
6833
- import { resolve as resolve8 } from "node:path";
6852
+ import { resolve as resolve9 } from "node:path";
6834
6853
  var planCommand = {
6835
6854
  name: "plan",
6836
6855
  summary: "summarise what the manifest would deploy (offline)",
@@ -6843,7 +6862,7 @@ async function runPlan(args, io) {
6843
6862
  io.err("Usage: launchpad plan [--file <path>] [--json]");
6844
6863
  return 64;
6845
6864
  }
6846
- const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
6865
+ const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
6847
6866
  const result = loadManifest(manifestPath);
6848
6867
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
6849
6868
  }
@@ -7026,7 +7045,7 @@ var SLUG_RE8 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7026
7045
  var SLUG_MIN_LENGTH = 3;
7027
7046
  var SLUG_MAX_LENGTH = 58;
7028
7047
  async function runDestroy(args, io, prompt, isTty) {
7029
- const parsed = parseArgs7(args);
7048
+ const parsed = parseArgs7(args, process.cwd(), (l) => io.err(l));
7030
7049
  if (typeof parsed === "string") {
7031
7050
  io.err(`launchpad destroy: ${parsed}`);
7032
7051
  printUsage3(io);
@@ -7081,7 +7100,7 @@ async function runDestroy(args, io, prompt, isTty) {
7081
7100
  return 1;
7082
7101
  }
7083
7102
  }
7084
- function parseArgs7(args, cwd = process.cwd()) {
7103
+ function parseArgs7(args, cwd = process.cwd(), warn) {
7085
7104
  let slug = null;
7086
7105
  let confirmSlug = null;
7087
7106
  let yes = false;
@@ -7128,7 +7147,7 @@ function parseArgs7(args, cwd = process.cwd()) {
7128
7147
  i += 1;
7129
7148
  }
7130
7149
  if (slug === null) {
7131
- const inferred = inferSlugFromCwd(cwd);
7150
+ const inferred = inferSlug({ cwd, warn });
7132
7151
  if (inferred === null) {
7133
7152
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7134
7153
  }
@@ -7288,8 +7307,8 @@ import { stringify as stringifyYaml } from "yaml";
7288
7307
 
7289
7308
  // src/deploy/manifest-state.ts
7290
7309
  async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
7291
- const path10 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
7292
- 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);
7293
7312
  return {
7294
7313
  slug: raw.slug,
7295
7314
  hasAppFile: raw.hasAppFile,
@@ -7302,8 +7321,8 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
7302
7321
 
7303
7322
  // src/deploy/manifest-status.ts
7304
7323
  async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
7305
- const path10 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7306
- return apiJson(cfg, { path: path10 }, fetcher);
7324
+ const path11 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7325
+ return apiJson(cfg, { path: path11 }, fetcher);
7307
7326
  }
7308
7327
 
7309
7328
  // src/deploy/deployment-status.ts
@@ -7347,7 +7366,7 @@ var pullCommand = {
7347
7366
  };
7348
7367
  var SLUG_RE9 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7349
7368
  async function runPull(args, io) {
7350
- const parsed = parseArgs8(args);
7369
+ const parsed = parseArgs8(args, process.cwd(), (l) => io.err(l));
7351
7370
  if (typeof parsed === "string") {
7352
7371
  io.err(`launchpad pull: ${parsed}`);
7353
7372
  printUsage4(io);
@@ -7433,7 +7452,7 @@ async function fetchLiveDeploymentBestEffort(cfg, slug) {
7433
7452
  return null;
7434
7453
  }
7435
7454
  }
7436
- function parseArgs8(args, cwd = process.cwd()) {
7455
+ function parseArgs8(args, cwd = process.cwd(), warn) {
7437
7456
  let slug = null;
7438
7457
  let out = null;
7439
7458
  let status = false;
@@ -7474,7 +7493,7 @@ function parseArgs8(args, cwd = process.cwd()) {
7474
7493
  i += 1;
7475
7494
  }
7476
7495
  if (slug === null) {
7477
- const inferred = inferSlugFromCwd(cwd);
7496
+ const inferred = inferSlug({ cwd, warn });
7478
7497
  if (inferred === null) {
7479
7498
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7480
7499
  }
@@ -7492,8 +7511,9 @@ function printUsage4(io) {
7492
7511
  " Reads the deployed launchpad.yaml for an app via the bot.",
7493
7512
  " No local platform-repo or terraform required.",
7494
7513
  "",
7495
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7496
- " 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.",
7497
7517
  "",
7498
7518
  " --status read the role-redacted status block (what your",
7499
7519
  " role may see) instead of the spec manifest.",
@@ -7506,14 +7526,184 @@ function describe23(e) {
7506
7526
  return e instanceof Error ? e.message : String(e);
7507
7527
  }
7508
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
+
7509
7699
  // src/commands/status.ts
7510
- import { readFileSync as readFileSync11 } from "node:fs";
7700
+ import { readFileSync as readFileSync12 } from "node:fs";
7511
7701
  var statusCommand = {
7512
7702
  name: "status",
7513
7703
  summary: "show drift between local launchpad.yaml and deployed state",
7514
7704
  run: runStatus
7515
7705
  };
7516
- 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]$/;
7517
7707
  async function fetchLifecycle(cfg, slug) {
7518
7708
  try {
7519
7709
  return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
@@ -7545,10 +7735,10 @@ function mapBotError(e, slug, io) {
7545
7735
  return 2;
7546
7736
  }
7547
7737
  async function runStatus(args, io) {
7548
- const parsed = parseArgs9(args);
7738
+ const parsed = parseArgs9(args, process.cwd(), (l) => io.err(l));
7549
7739
  if (typeof parsed === "string") {
7550
7740
  io.err(`launchpad status: ${parsed}`);
7551
- printUsage5(io);
7741
+ printUsage6(io);
7552
7742
  return 64;
7553
7743
  }
7554
7744
  const cfg = loadConfig();
@@ -7562,34 +7752,36 @@ async function runStatus(args, io) {
7562
7752
  emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
7563
7753
  return 0;
7564
7754
  }
7565
- let localYaml;
7755
+ let localYaml = null;
7566
7756
  try {
7567
- localYaml = readFileSync11(parsed.file, "utf8");
7757
+ localYaml = readFileSync12(parsed.file, "utf8");
7568
7758
  } catch (e) {
7569
- io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
7570
- if (lifecycle !== null && lifecycle.state === "live") {
7571
- 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;
7572
7762
  }
7573
- return 2;
7574
- }
7575
- let localObj;
7576
- try {
7577
- const { parse: parseYaml5 } = await import("yaml");
7578
- localObj = parseYaml5(localYaml);
7579
- } catch (e) {
7580
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
7581
- return 2;
7582
7763
  }
7583
- const localParse = parseManifest(localObj);
7584
- if (localParse.kind !== "ok") {
7585
- io.err(`launchpad status: ${parsed.file} failed schema validation:`);
7586
- for (const issue of localParse.issues) {
7587
- 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;
7588
7773
  }
7589
- 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);
7590
7784
  }
7591
- const local = localParse.manifest;
7592
- warnSecretShape(local.production_env, io);
7593
7785
  let state;
7594
7786
  try {
7595
7787
  state = await fetchManifestState(cfg, parsed.slug, { includeManifest: true });
@@ -7623,6 +7815,7 @@ async function runStatus(args, io) {
7623
7815
  const out = {
7624
7816
  state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
7625
7817
  slug: parsed.slug,
7818
+ ...local === null ? { drift: null } : {},
7626
7819
  deployedSha: state.lastAppliedManifestSha,
7627
7820
  headSha: state.appRepoHeadSha,
7628
7821
  hasOpenPr: state.openPr !== null,
@@ -7635,10 +7828,34 @@ async function runStatus(args, io) {
7635
7828
  emit3(out, parsed.json, io);
7636
7829
  return 0;
7637
7830
  }
7831
+ if (local === null) {
7832
+ const out = {
7833
+ state: "live_drift_unknown",
7834
+ slug: parsed.slug,
7835
+ drift: null,
7836
+ deployedSha: state.lastAppliedManifestSha,
7837
+ headSha: state.appRepoHeadSha,
7838
+ hasOpenPr: state.openPr !== null,
7839
+ openPrNumber: state.openPr?.number ?? null,
7840
+ driftFields: [],
7841
+ driftDetails: [],
7842
+ ...deploymentKnown ? { deployment } : {},
7843
+ ...standingExceptions !== null ? { standingExceptions } : {}
7844
+ };
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
+ }
7853
+ return 0;
7854
+ }
7638
7855
  let deployedObj;
7639
7856
  try {
7640
- const { parse: parseYaml5 } = await import("yaml");
7641
- deployedObj = parseYaml5(state.manifestYaml);
7857
+ const { parse: parseYaml6 } = await import("yaml");
7858
+ deployedObj = parseYaml6(state.manifestYaml);
7642
7859
  } catch (e) {
7643
7860
  io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
7644
7861
  return 2;
@@ -7673,17 +7890,17 @@ async function runStatus(args, io) {
7673
7890
  }
7674
7891
  function computeDrift(local, deployed) {
7675
7892
  const diffs = [];
7676
- const cmp = (path10, l, d) => {
7893
+ const cmp = (path11, l, d) => {
7677
7894
  const li = l ?? null;
7678
7895
  const di = d ?? null;
7679
7896
  if (typeof li === "object" || typeof di === "object") {
7680
7897
  if (JSON.stringify(li) !== JSON.stringify(di)) {
7681
- diffs.push({ path: path10, local: l, deployed: d });
7898
+ diffs.push({ path: path11, local: l, deployed: d });
7682
7899
  }
7683
7900
  return;
7684
7901
  }
7685
7902
  if (li !== di) {
7686
- diffs.push({ path: path10, local: l, deployed: d });
7903
+ diffs.push({ path: path11, local: l, deployed: d });
7687
7904
  }
7688
7905
  };
7689
7906
  cmp("metadata.name", local.metadata.name, deployed.metadata.name);
@@ -7748,18 +7965,28 @@ function emit3(out, asJson, io) {
7748
7965
  return;
7749
7966
  case "live_no_content":
7750
7967
  io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
7968
+ surfaceNoLocalManifestNote(out, io);
7751
7969
  surfaceHeadVsDeployed(out, io);
7752
7970
  surfaceDeployment(out, io);
7753
7971
  surfaceExceptions(out, io);
7754
7972
  return;
7755
7973
  case "no_deployed_manifest":
7756
7974
  io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
7975
+ surfaceNoLocalManifestNote(out, io);
7757
7976
  surfaceHeadVsDeployed(out, io);
7758
7977
  surfaceDeployment(out, io);
7759
7978
  surfaceExceptions(out, io);
7760
7979
  return;
7761
7980
  case "live_content_untracked":
7762
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);
7983
+ surfaceHeadVsDeployed(out, io);
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);
7763
7990
  surfaceHeadVsDeployed(out, io);
7764
7991
  surfaceDeployment(out, io);
7765
7992
  surfaceExceptions(out, io);
@@ -7783,6 +8010,11 @@ function emit3(out, asJson, io) {
7783
8010
  return;
7784
8011
  }
7785
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
+ }
7786
8018
  function triggerLabel(trigger) {
7787
8019
  if (trigger === "git-push")
7788
8020
  return "git push";
@@ -7870,7 +8102,7 @@ function warnSecretShape(env, io) {
7870
8102
  }
7871
8103
  }
7872
8104
  }
7873
- function parseArgs9(args, cwd = process.cwd()) {
8105
+ function parseArgs9(args, cwd = process.cwd(), warn) {
7874
8106
  let slug = null;
7875
8107
  let file = "./launchpad.yaml";
7876
8108
  let json = false;
@@ -7917,54 +8149,68 @@ function parseArgs9(args, cwd = process.cwd()) {
7917
8149
  i += 1;
7918
8150
  }
7919
8151
  if (slug === null) {
7920
- const inferred = inferSlugFromCwd(cwd);
8152
+ const inferred = inferSlug({ cwd, file, warn });
7921
8153
  if (inferred === null) {
7922
8154
  return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7923
8155
  }
7924
8156
  slug = inferred;
7925
8157
  }
7926
- if (!SLUG_RE10.test(slug)) {
7927
- return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
8158
+ if (!SLUG_RE11.test(slug)) {
8159
+ return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
7928
8160
  }
7929
8161
  return { slug, file, json, strict };
7930
8162
  }
7931
- function printUsage5(io) {
8163
+ function printUsage6(io) {
7932
8164
  io.err([
7933
8165
  "usage: launchpad status [<slug>] [--slug <slug>] [--file <path>] [--json] [--strict]",
7934
8166
  "",
7935
8167
  " Compare local launchpad.yaml against the deployed state.",
7936
8168
  " No local platform-repo or terraform required.",
7937
8169
  "",
7938
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7939
- " 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.",
7940
8179
  "",
7941
8180
  "Flags:",
7942
8181
  " --file <path> local manifest path (default: ./launchpad.yaml).",
7943
- " Does NOT influence slug resolution.",
7944
- " --slug <slug> override cwd inference.",
8182
+ " Also consulted for slug inference; --slug or a",
8183
+ " positional slug still overrides.",
8184
+ " --slug <slug> override inference.",
7945
8185
  " --json emit machine-readable JSON to stdout.",
7946
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.",
7947
8189
  "",
7948
8190
  "Exit codes:",
7949
- " 0 = in sync, OR drift in default (report-only) mode.",
7950
- " 1 = drift, when --strict is set.",
7951
- " 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.)."
7952
8196
  ].join(`
7953
8197
  `));
7954
8198
  }
7955
8199
  function describe24(e) {
7956
8200
  return e instanceof Error ? e.message : String(e);
7957
8201
  }
8202
+ function isEnoent(e) {
8203
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
8204
+ }
7958
8205
 
7959
8206
  // src/commands/review.ts
7960
- import * as path10 from "node:path";
8207
+ import * as path11 from "node:path";
7961
8208
  var reviewCommand = {
7962
8209
  name: "review",
7963
8210
  summary: "show the review state for a PR (slug-scoped)",
7964
8211
  run: runReview
7965
8212
  };
7966
- var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7967
- 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]$/;
7968
8214
  async function runReview(args, io) {
7969
8215
  const parsed = parseArgs10(args);
7970
8216
  if (parsed === null) {
@@ -7976,16 +8222,16 @@ async function runReview(args, io) {
7976
8222
  if (parsed.slug !== null) {
7977
8223
  slug = parsed.slug;
7978
8224
  } else {
7979
- const inferred = inferSlugFromCwd5(process.cwd());
8225
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
7980
8226
  if (inferred === null) {
7981
- 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())});
7982
8228
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
7983
8229
  return 64;
7984
8230
  }
7985
8231
  slug = inferred;
7986
8232
  }
7987
- if (!SLUG_RE11.test(slug)) {
7988
- 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}`);
7989
8235
  return 64;
7990
8236
  }
7991
8237
  try {
@@ -8060,11 +8306,6 @@ function parseArgs10(args) {
8060
8306
  }
8061
8307
  return { slug, prNumber };
8062
8308
  }
8063
- function inferSlugFromCwd5(cwd) {
8064
- const base = path10.basename(cwd);
8065
- const m = base.match(DIRNAME_RE5);
8066
- return m === null ? null : m[1];
8067
- }
8068
8309
  function renderReview(r, io) {
8069
8310
  const rv = r.review;
8070
8311
  io.out(`Review of PR #${rv.prNumber} (${rv.slug})`);
@@ -8116,7 +8357,7 @@ function describe25(e) {
8116
8357
  }
8117
8358
 
8118
8359
  // src/deploy/rollback.ts
8119
- 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";
8120
8361
  import { resolve as resolvePath2 } from "node:path";
8121
8362
  import { createInterface as createInterface3 } from "node:readline/promises";
8122
8363
 
@@ -8240,16 +8481,16 @@ async function runRollback(opts, io, deps = {}) {
8240
8481
  yes: true
8241
8482
  }, io, applyDeps);
8242
8483
  }
8243
- function readCurrentManifest(path11) {
8244
- if (!existsSync7(path11))
8484
+ function readCurrentManifest(path12) {
8485
+ if (!existsSync8(path12))
8245
8486
  return null;
8246
8487
  let raw;
8247
8488
  try {
8248
- raw = readFileSync12(path11, "utf8");
8489
+ raw = readFileSync13(path12, "utf8");
8249
8490
  } catch {
8250
8491
  return null;
8251
8492
  }
8252
- const parsed = parseManifest2(raw, path11);
8493
+ const parsed = parseManifest2(raw, path12);
8253
8494
  if (parsed.kind !== "ok")
8254
8495
  return null;
8255
8496
  return summarise(parsed.manifest);
@@ -8401,7 +8642,7 @@ function parseArgs11(args) {
8401
8642
  }
8402
8643
 
8403
8644
  // src/secrets/push.ts
8404
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
8645
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8405
8646
  import { resolve as resolvePath3 } from "node:path";
8406
8647
 
8407
8648
  // src/secrets/env-parse.ts
@@ -8456,14 +8697,14 @@ async function runSecretsPush(opts, io, deps = {}) {
8456
8697
  return 0;
8457
8698
  }
8458
8699
  const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
8459
- if (!existsSync8(envPath)) {
8700
+ if (!existsSync9(envPath)) {
8460
8701
  io.err(`launchpad secrets push: ${envPath}`);
8461
8702
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
8462
8703
  return 2;
8463
8704
  }
8464
8705
  let envText;
8465
8706
  try {
8466
- envText = readFileSync13(envPath, "utf8");
8707
+ envText = readFileSync14(envPath, "utf8");
8467
8708
  } catch (e) {
8468
8709
  io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
8469
8710
  return 2;
@@ -8735,32 +8976,32 @@ function renderManifestError5(result, io) {
8735
8976
  }
8736
8977
 
8737
8978
  // src/secrets/set.ts
8738
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8979
+ import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
8739
8980
  import { resolve as resolvePath5 } from "node:path";
8740
- import { parse as parseYaml5 } from "yaml";
8981
+ import { parse as parseYaml6 } from "yaml";
8741
8982
  var CELL_LABEL2 = {
8742
8983
  present: "PRESENT",
8743
8984
  missing: "MISSING",
8744
8985
  not_deployed: "NOT_DEPLOYED"
8745
8986
  };
8746
8987
  function loadSet(fleetFile, setName, io) {
8747
- const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8748
- if (!existsSync9(path11)) {
8749
- io.err(`✗ ${path11}`);
8988
+ const path12 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8989
+ if (!existsSync10(path12)) {
8990
+ io.err(`✗ ${path12}`);
8750
8991
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
8751
8992
  return 2;
8752
8993
  }
8753
8994
  let obj;
8754
8995
  try {
8755
- obj = parseYaml5(readFileSync14(path11, "utf8"));
8996
+ obj = parseYaml6(readFileSync15(path12, "utf8"));
8756
8997
  } catch (e) {
8757
- io.err(`✗ ${path11}`);
8998
+ io.err(`✗ ${path12}`);
8758
8999
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
8759
9000
  return 1;
8760
9001
  }
8761
9002
  const parsed = parseFleetSecretSets(obj);
8762
9003
  if (!parsed.ok) {
8763
- io.err(`✗ ${path11}`);
9004
+ io.err(`✗ ${path12}`);
8764
9005
  io.err(` ${parsed.issues.length} schema issue(s):`);
8765
9006
  for (const i of parsed.issues)
8766
9007
  io.err(` ${i.path}: ${i.message}`);
@@ -8769,7 +9010,7 @@ function loadSet(fleetFile, setName, io) {
8769
9010
  const set = parsed.manifest.secretSets.find((s) => s.name === setName);
8770
9011
  if (set === undefined) {
8771
9012
  const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
8772
- io.err(`✗ secret-set "${setName}" not found in ${path11}`);
9013
+ io.err(`✗ secret-set "${setName}" not found in ${path12}`);
8773
9014
  io.err(` declared sets: ${names}`);
8774
9015
  return 1;
8775
9016
  }
@@ -8837,14 +9078,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
8837
9078
  if (typeof set === "number")
8838
9079
  return set;
8839
9080
  const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
8840
- if (!existsSync9(envPath)) {
9081
+ if (!existsSync10(envPath)) {
8841
9082
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
8842
9083
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
8843
9084
  return 2;
8844
9085
  }
8845
9086
  let envText;
8846
9087
  try {
8847
- envText = readFileSync14(envPath, "utf8");
9088
+ envText = readFileSync15(envPath, "utf8");
8848
9089
  } catch (e) {
8849
9090
  io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
8850
9091
  return 2;
@@ -8988,8 +9229,8 @@ function setPushExit(e) {
8988
9229
  }
8989
9230
 
8990
9231
  // src/commands/secrets-template.ts
8991
- import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
8992
- 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";
8993
9234
  async function runSecretsTemplate(args, io) {
8994
9235
  const flags = parseFlags4(args);
8995
9236
  if (flags.kind === "usage-error") {
@@ -8997,7 +9238,7 @@ async function runSecretsTemplate(args, io) {
8997
9238
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
8998
9239
  return 64;
8999
9240
  }
9000
- const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
9241
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
9001
9242
  const result = loadManifest(manifestPath);
9002
9243
  const renderResult = renderManifest(result, io);
9003
9244
  if (renderResult.kind !== "ok") {
@@ -9011,8 +9252,8 @@ async function runSecretsTemplate(args, io) {
9011
9252
  }
9012
9253
  return 0;
9013
9254
  }
9014
- const outPath = resolve9(process.cwd(), flags.out);
9015
- if (existsSync10(outPath) && !flags.force) {
9255
+ const outPath = resolve10(process.cwd(), flags.out);
9256
+ if (existsSync11(outPath) && !flags.force) {
9016
9257
  io.err(`launchpad secrets template: ${outPath} already exists`);
9017
9258
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
9018
9259
  return 64;
@@ -9300,8 +9541,8 @@ function printHelp2(io) {
9300
9541
 
9301
9542
  // src/commands/skills.ts
9302
9543
  import { fileURLToPath } from "node:url";
9303
- import { dirname as dirname6, join as join11, resolve as resolve10 } from "node:path";
9304
- 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";
9305
9546
  import { homedir as homedir2 } from "node:os";
9306
9547
  var BUNDLE_PREFIX = "launchpad-";
9307
9548
  var BUNDLED_SKILLS = [
@@ -9368,11 +9609,11 @@ function resolveInstallEnv() {
9368
9609
  function defaultBundleDir() {
9369
9610
  const here = dirname6(fileURLToPath(import.meta.url));
9370
9611
  const candidates = [
9371
- resolve10(here, "..", "skills"),
9372
- resolve10(here, "..", "..", "skills")
9612
+ resolve11(here, "..", "skills"),
9613
+ resolve11(here, "..", "..", "skills")
9373
9614
  ];
9374
9615
  for (const c of candidates) {
9375
- if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
9616
+ if (existsSync12(join11(c, "launchpad-onboard", "SKILL.md"))) {
9376
9617
  return c;
9377
9618
  }
9378
9619
  }
@@ -9452,10 +9693,10 @@ async function doList(io) {
9452
9693
  }
9453
9694
  return 0;
9454
9695
  }
9455
- async function readVersion(path11) {
9696
+ async function readVersion(path12) {
9456
9697
  let text;
9457
9698
  try {
9458
- text = await fs5.readFile(path11, "utf8");
9699
+ text = await fs5.readFile(path12, "utf8");
9459
9700
  } catch {
9460
9701
  return null;
9461
9702
  }
@@ -9465,9 +9706,9 @@ async function readVersion(path11) {
9465
9706
  const m = /^version:\s*(.+?)\s*$/m.exec(front);
9466
9707
  return m === null ? null : m[1] ?? null;
9467
9708
  }
9468
- async function isDir(path11) {
9709
+ async function isDir(path12) {
9469
9710
  try {
9470
- const stat = await fs5.stat(path11);
9711
+ const stat = await fs5.stat(path12);
9471
9712
  return stat.isDirectory();
9472
9713
  } catch {
9473
9714
  return false;
@@ -9481,9 +9722,9 @@ function describe28(e) {
9481
9722
  import { execFile, spawn as spawn5 } from "node:child_process";
9482
9723
  import { promisify } from "node:util";
9483
9724
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9484
- 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";
9485
9726
  import { homedir as homedir3, tmpdir } from "node:os";
9486
- 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";
9487
9728
 
9488
9729
  // src/commands/channel-auth.ts
9489
9730
  import { createServer as createServer2 } from "node:http";
@@ -9526,9 +9767,9 @@ async function startLoopback(state, timeoutMs) {
9526
9767
  resolveCode(code);
9527
9768
  }
9528
9769
  });
9529
- const bound = await new Promise((resolve11) => {
9530
- server.once("error", () => resolve11(false));
9531
- 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));
9532
9773
  });
9533
9774
  if (!bound)
9534
9775
  return null;
@@ -9712,7 +9953,7 @@ function resolveLatestVersion() {
9712
9953
  }
9713
9954
  function detectInstallChannel() {
9714
9955
  try {
9715
- return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9956
+ return readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9716
9957
  } catch {
9717
9958
  return "github";
9718
9959
  }
@@ -9775,7 +10016,7 @@ function errStderr(e) {
9775
10016
  return "";
9776
10017
  }
9777
10018
  async function detectPackageManager2() {
9778
- const pkgRoot = resolve11(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
10019
+ const pkgRoot = resolve12(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
9779
10020
  const candidates = [];
9780
10021
  const npmRoot = await pmRoot("npm");
9781
10022
  if (npmRoot !== null)
@@ -9783,7 +10024,7 @@ async function detectPackageManager2() {
9783
10024
  const pnpmRoot = await pmRoot("pnpm");
9784
10025
  if (pnpmRoot !== null)
9785
10026
  candidates.push(["pnpm", pnpmRoot]);
9786
- candidates.push(["bun", resolve11(homedir3(), ".bun/install/global/node_modules")]);
10027
+ candidates.push(["bun", resolve12(homedir3(), ".bun/install/global/node_modules")]);
9787
10028
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
9788
10029
  return matches.length === 1 ? matches[0][0] : null;
9789
10030
  }
@@ -9919,8 +10160,8 @@ function printHelp4(io) {
9919
10160
  }
9920
10161
 
9921
10162
  // src/commands/validate.ts
9922
- import { readFileSync as readFileSync16 } from "node:fs";
9923
- 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";
9924
10165
  var validateCommand = {
9925
10166
  name: "validate",
9926
10167
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -9934,12 +10175,12 @@ async function runValidate(args, io) {
9934
10175
  return 64;
9935
10176
  }
9936
10177
  const { file, json, strictGroups } = parseResult;
9937
- const path11 = resolve12(process.cwd(), file ?? "launchpad.yaml");
9938
- const result = loadManifest(path11);
10178
+ const path12 = resolve13(process.cwd(), file ?? "launchpad.yaml");
10179
+ const result = loadManifest(path12);
9939
10180
  if (result.kind !== "ok") {
9940
10181
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
9941
10182
  }
9942
- const boundary = checkBoundary(path11, result.manifest.app !== undefined);
10183
+ const boundary = checkBoundary(path12, result.manifest.app !== undefined);
9943
10184
  const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
9944
10185
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
9945
10186
  }
@@ -9953,7 +10194,7 @@ function checkBoundary(manifestPath, declared) {
9953
10194
  }
9954
10195
  let manifestYaml;
9955
10196
  try {
9956
- manifestYaml = readFileSync16(manifestPath, "utf8");
10197
+ manifestYaml = readFileSync17(manifestPath, "utf8");
9957
10198
  } catch {
9958
10199
  manifestYaml = null;
9959
10200
  }
@@ -10268,14 +10509,14 @@ function describe30(e) {
10268
10509
  import { spawn as spawn6 } from "node:child_process";
10269
10510
  import { homedir as homedir4 } from "node:os";
10270
10511
  import { join as join13 } from "node:path";
10271
- 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";
10272
10513
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
10273
10514
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10274
10515
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
10275
10516
  var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
10276
10517
  function readCache2() {
10277
10518
  try {
10278
- const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
10519
+ const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
10279
10520
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
10280
10521
  const latest = raw.latest;
10281
10522
  return {
@@ -10391,6 +10632,7 @@ var COMMANDS = [
10391
10632
  generateCommand,
10392
10633
  pullCommand,
10393
10634
  statusCommand,
10635
+ recoverCommand,
10394
10636
  destroyCommand,
10395
10637
  rollbackCommand,
10396
10638
  groupsCommand,