@m-kopa/launchpad-cli 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/auth/flow.d.ts +7 -3
  3. package/dist/auth/flow.d.ts.map +1 -1
  4. package/dist/auth/gateway-flow.d.ts +76 -0
  5. package/dist/auth/gateway-flow.d.ts.map +1 -0
  6. package/dist/auth/session.d.ts +35 -2
  7. package/dist/auth/session.d.ts.map +1 -1
  8. package/dist/cli.js +871 -331
  9. package/dist/commands/deploy.d.ts +10 -13
  10. package/dist/commands/deploy.d.ts.map +1 -1
  11. package/dist/commands/destroy.d.ts +1 -1
  12. package/dist/commands/destroy.d.ts.map +1 -1
  13. package/dist/commands/envvars.d.ts +2 -2
  14. package/dist/commands/envvars.d.ts.map +1 -1
  15. package/dist/commands/infer-slug.d.ts +37 -0
  16. package/dist/commands/infer-slug.d.ts.map +1 -0
  17. package/dist/commands/login.d.ts +10 -0
  18. package/dist/commands/login.d.ts.map +1 -1
  19. package/dist/commands/logout.d.ts +7 -0
  20. package/dist/commands/logout.d.ts.map +1 -1
  21. package/dist/commands/logs.d.ts +2 -2
  22. package/dist/commands/logs.d.ts.map +1 -1
  23. package/dist/commands/merge.d.ts +2 -2
  24. package/dist/commands/merge.d.ts.map +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.d.ts.map +1 -1
  27. package/dist/commands/recover.d.ts +12 -0
  28. package/dist/commands/recover.d.ts.map +1 -0
  29. package/dist/commands/review.d.ts +2 -2
  30. package/dist/commands/review.d.ts.map +1 -1
  31. package/dist/commands/status.d.ts +13 -3
  32. package/dist/commands/status.d.ts.map +1 -1
  33. package/dist/config.d.ts +11 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/dispatcher.d.ts.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/package.json +1 -1
  38. package/skills/launchpad-content-pr/SKILL.md +1 -1
  39. package/skills/launchpad-deploy/SKILL.md +1 -1
  40. package/skills/launchpad-deploy-status/SKILL.md +6 -4
  41. package/skills/launchpad-destroy/SKILL.md +1 -1
  42. package/skills/launchpad-onboard/SKILL.md +1 -1
  43. package/skills/launchpad-status/SKILL.md +13 -6
package/dist/cli.js CHANGED
@@ -19,21 +19,33 @@ 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.27.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
26
26
  import * as path from "node:path";
27
27
  var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
28
+ var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
28
29
  function loadConfig(env = process.env) {
29
30
  const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
30
31
  const botUrl = rawBotUrl.replace(/\/+$/, "");
32
+ const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
33
+ const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
34
+ const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
31
35
  const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
32
36
  const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
33
37
  const stateDir = env.LAUNCHPAD_STATE_DIR ?? path.join(os.homedir(), ".launchpad", "state");
34
38
  const rawPlatformRepo = env.LAUNCHPAD_PLATFORM_REPO;
35
39
  const platformRepoPath = rawPlatformRepo !== undefined && rawPlatformRepo.length > 0 ? path.resolve(rawPlatformRepo) : null;
36
- return { botUrl, sessionPath, cacheDir, stateDir, platformRepoPath };
40
+ return {
41
+ botUrl,
42
+ authGatewayUrl,
43
+ authLegacy,
44
+ sessionPath,
45
+ cacheDir,
46
+ stateDir,
47
+ platformRepoPath
48
+ };
37
49
  }
38
50
 
39
51
  // src/http/errors.ts
@@ -64,7 +76,7 @@ class TransportError extends Error {
64
76
  }
65
77
 
66
78
  // src/auth/flow.ts
67
- import { randomBytes as randomBytes3 } from "node:crypto";
79
+ import { randomBytes as randomBytes4 } from "node:crypto";
68
80
 
69
81
  // src/auth/callback-server.ts
70
82
  import { createServer } from "node:http";
@@ -371,6 +383,10 @@ import * as fs from "node:fs/promises";
371
383
  import * as path2 from "node:path";
372
384
  import { randomBytes as randomBytes2 } from "node:crypto";
373
385
  var SESSION_VERSION = 1;
386
+ var GATEWAY_SESSION_VERSION = 2;
387
+ function isGatewaySession(s) {
388
+ return s.version === GATEWAY_SESSION_VERSION;
389
+ }
374
390
 
375
391
  class SessionParseError extends Error {
376
392
  code = "session_parse_error";
@@ -394,8 +410,11 @@ async function readSession(sessionPath) {
394
410
  throw new SessionParseError(`session file at ${sessionPath} is not an object`);
395
411
  }
396
412
  const obj = parsed;
413
+ if (obj.version === GATEWAY_SESSION_VERSION) {
414
+ return parseGatewaySession(obj, sessionPath);
415
+ }
397
416
  if (obj.version !== SESSION_VERSION) {
398
- throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION}`);
417
+ throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION} or ${GATEWAY_SESSION_VERSION}`);
399
418
  }
400
419
  for (const k of [
401
420
  "accessToken",
@@ -416,6 +435,20 @@ async function readSession(sessionPath) {
416
435
  }
417
436
  return obj;
418
437
  }
438
+ function parseGatewaySession(obj, sessionPath) {
439
+ if (obj.kind !== "gateway") {
440
+ throw new SessionParseError(`session file at ${sessionPath}: version 2 with unknown kind ${String(obj.kind)}`);
441
+ }
442
+ for (const k of ["accessToken", "refreshToken", "gatewayUrl", "issuedAt"]) {
443
+ if (typeof obj[k] !== "string") {
444
+ throw new SessionParseError(`session file at ${sessionPath}: missing or non-string ${k}`);
445
+ }
446
+ }
447
+ if (typeof obj.accessTokenExpiresAt !== "number") {
448
+ throw new SessionParseError(`session file at ${sessionPath}: missing or non-number accessTokenExpiresAt`);
449
+ }
450
+ return obj;
451
+ }
419
452
  async function writeSession(sessionPath, session) {
420
453
  const dir = path2.dirname(sessionPath);
421
454
  await fs.mkdir(dir, { recursive: true, mode: 448 });
@@ -453,6 +486,9 @@ function describe4(e) {
453
486
  return e instanceof Error ? e.message : String(e);
454
487
  }
455
488
 
489
+ // src/auth/gateway-flow.ts
490
+ import { randomBytes as randomBytes3 } from "node:crypto";
491
+
456
492
  // src/auth/browser.ts
457
493
  import { spawn } from "node:child_process";
458
494
 
@@ -487,16 +523,185 @@ function chooseOpener(platform, url) {
487
523
  }
488
524
  }
489
525
 
526
+ // src/auth/gateway-flow.ts
527
+ var CLI_SESSION_AUTH_PATH = "/__cli_session_auth";
528
+ var CLI_SESSION_TOKEN_PATH = "/__cli_session_token";
529
+ var CLI_LOGOUT_PATH = "/__cli_logout";
530
+
531
+ class GatewayUnavailableError extends Error {
532
+ code = "gateway_unavailable";
533
+ }
534
+
535
+ class GatewayTokenError extends Error {
536
+ code = "gateway_token_error";
537
+ httpStatus;
538
+ constructor(message, httpStatus) {
539
+ super(message);
540
+ this.name = "GatewayTokenError";
541
+ if (httpStatus !== undefined)
542
+ this.httpStatus = httpStatus;
543
+ }
544
+ }
545
+
546
+ class GatewayRateLimitError extends Error {
547
+ code = "gateway_rate_limited";
548
+ retryAfterSec;
549
+ constructor(retryAfterSec) {
550
+ super(`gateway rate-limited the request — retry in ${retryAfterSec}s (your session is intact)`);
551
+ this.name = "GatewayRateLimitError";
552
+ this.retryAfterSec = retryAfterSec;
553
+ }
554
+ }
555
+ async function gatewayLogin(opts) {
556
+ const fetcher = opts.fetcher ?? fetch;
557
+ const opener = opts.browserOpener ?? ((url) => openBrowser(url));
558
+ await probeGateway(opts.gatewayUrl, fetcher);
559
+ const state = randomBytes3(16).toString("hex");
560
+ const server = await bindCallbackServer(state);
561
+ try {
562
+ const redirectUri = `http://127.0.0.1:${server.port}/callback`;
563
+ const pkce = generatePkcePair();
564
+ const u = new URL(`${opts.gatewayUrl}${CLI_SESSION_AUTH_PATH}`);
565
+ u.searchParams.set("cb", redirectUri);
566
+ u.searchParams.set("code_challenge", pkce.challenge);
567
+ u.searchParams.set("state", state);
568
+ const authUrl = u.toString();
569
+ opts.onAuthUrl?.(authUrl);
570
+ try {
571
+ await opener(authUrl);
572
+ } catch (e) {
573
+ opts.onAuthUrl?.(`(could not auto-open browser: ${describe5(e)} — copy the URL above into a browser instead)`);
574
+ }
575
+ const callback = await server.result;
576
+ const pair = await postSessionTokenForm(opts.gatewayUrl, new URLSearchParams({
577
+ grant_type: "authorization_code",
578
+ code: callback.code,
579
+ code_verifier: pkce.verifier
580
+ }), fetcher);
581
+ const session = {
582
+ version: GATEWAY_SESSION_VERSION,
583
+ kind: "gateway",
584
+ accessToken: pair.accessToken,
585
+ refreshToken: pair.refreshToken,
586
+ accessTokenExpiresAt: Date.now() + pair.expiresInSec * 1000,
587
+ gatewayUrl: opts.gatewayUrl,
588
+ issuedAt: new Date().toISOString()
589
+ };
590
+ await writeSession(opts.sessionPath, session);
591
+ return session;
592
+ } finally {
593
+ await server.close();
594
+ }
595
+ }
596
+ async function refreshGatewayTokens(params, fetcher = fetch) {
597
+ return postSessionTokenForm(params.gatewayUrl, new URLSearchParams({
598
+ grant_type: "refresh_token",
599
+ refresh_token: params.refreshToken
600
+ }), fetcher);
601
+ }
602
+ async function revokeGatewaySession(params, fetcher = fetch) {
603
+ const url = `${params.gatewayUrl}${CLI_LOGOUT_PATH}`;
604
+ let res;
605
+ try {
606
+ res = await fetcher(url, {
607
+ method: "POST",
608
+ headers: { "content-type": "application/x-www-form-urlencoded" },
609
+ body: new URLSearchParams({ refresh_token: params.refreshToken }).toString()
610
+ });
611
+ } catch (e) {
612
+ throw new GatewayTokenError(`logout endpoint: network error: ${describe5(e)}`);
613
+ }
614
+ if (res.status === 429) {
615
+ throw new GatewayRateLimitError(readRetryAfter(res));
616
+ }
617
+ if (res.status !== 204 && !res.ok) {
618
+ const detail = await res.text().catch(() => "");
619
+ throw new GatewayTokenError(`logout endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
620
+ }
621
+ }
622
+ async function probeGateway(gatewayUrl, fetcher) {
623
+ const url = `${gatewayUrl}${CLI_SESSION_AUTH_PATH}`;
624
+ let res;
625
+ try {
626
+ res = await fetcher(url, { method: "GET" });
627
+ } catch (e) {
628
+ throw new GatewayUnavailableError(`auth gateway unreachable at ${gatewayUrl}: ${describe5(e)}`);
629
+ }
630
+ if (res.status === 429) {
631
+ throw new GatewayRateLimitError(readRetryAfter(res));
632
+ }
633
+ if (res.status !== 400) {
634
+ throw new GatewayUnavailableError(`auth gateway at ${gatewayUrl} is not serving the cli-session grant (HTTP ${res.status} from ${CLI_SESSION_AUTH_PATH})`);
635
+ }
636
+ }
637
+ async function postSessionTokenForm(gatewayUrl, form, fetcher) {
638
+ const url = `${gatewayUrl}${CLI_SESSION_TOKEN_PATH}`;
639
+ let res;
640
+ try {
641
+ res = await fetcher(url, {
642
+ method: "POST",
643
+ headers: {
644
+ "content-type": "application/x-www-form-urlencoded",
645
+ accept: "application/json"
646
+ },
647
+ body: form.toString()
648
+ });
649
+ } catch (e) {
650
+ throw new GatewayTokenError(`session token endpoint: network error: ${describe5(e)}`);
651
+ }
652
+ if (res.status === 429) {
653
+ throw new GatewayRateLimitError(readRetryAfter(res));
654
+ }
655
+ if (!res.ok) {
656
+ const detail = await res.text().catch(() => "");
657
+ throw new GatewayTokenError(`session token endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
658
+ }
659
+ let parsed;
660
+ try {
661
+ parsed = await res.json();
662
+ } catch (e) {
663
+ throw new GatewayTokenError(`session token endpoint: non-JSON response: ${describe5(e)}`);
664
+ }
665
+ if (parsed === null || typeof parsed !== "object") {
666
+ throw new GatewayTokenError(`session token endpoint: response is not an object`);
667
+ }
668
+ const obj = parsed;
669
+ if (typeof obj.access_token !== "string") {
670
+ throw new GatewayTokenError(`session token endpoint: response missing access_token`);
671
+ }
672
+ if (typeof obj.refresh_token !== "string") {
673
+ throw new GatewayTokenError(`session token endpoint: response missing refresh_token`);
674
+ }
675
+ const expiresIn = obj.expires_in;
676
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
677
+ throw new GatewayTokenError(`session token endpoint: response missing positive finite numeric expires_in (got ${String(expiresIn)})`);
678
+ }
679
+ return {
680
+ accessToken: obj.access_token,
681
+ refreshToken: obj.refresh_token,
682
+ expiresInSec: expiresIn
683
+ };
684
+ }
685
+ function readRetryAfter(res) {
686
+ const raw = res.headers.get("retry-after");
687
+ const n = raw === null ? NaN : Number.parseInt(raw, 10);
688
+ return Number.isFinite(n) && n > 0 ? n : 60;
689
+ }
690
+ function describe5(e) {
691
+ return e instanceof Error ? e.message : String(e);
692
+ }
693
+
490
694
  // src/auth/flow.ts
491
695
  class LoginRequiredError extends Error {
492
696
  code = "login_required";
493
697
  }
494
698
  var REFRESH_SKEW_MS = 30000;
699
+ var MAX_ABSORBED_RETRY_AFTER_SEC = 10;
495
700
  async function login(opts) {
496
701
  const fetcher = opts.fetcher ?? fetch;
497
702
  const opener = opts.browserOpener ?? ((url) => openBrowser(url));
498
703
  const endpoints = await discoverOauthEndpoints(opts.botUrl, fetcher);
499
- const state = randomBytes3(16).toString("hex");
704
+ const state = randomBytes4(16).toString("hex");
500
705
  const server = await bindCallbackServer(state);
501
706
  try {
502
707
  const redirectUri = `http://127.0.0.1:${server.port}/callback`;
@@ -514,7 +719,7 @@ async function login(opts) {
514
719
  try {
515
720
  await opener(authUrl);
516
721
  } catch (e) {
517
- opts.onAuthUrl?.(`(could not auto-open browser: ${describe5(e)} — copy the URL above into a browser instead)`);
722
+ opts.onAuthUrl?.(`(could not auto-open browser: ${describe6(e)} — copy the URL above into a browser instead)`);
518
723
  }
519
724
  const callback = await server.result;
520
725
  const tokens = await exchangeCodeForTokens({
@@ -541,7 +746,7 @@ async function login(opts) {
541
746
  await server.close();
542
747
  }
543
748
  }
544
- async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now) {
749
+ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
545
750
  const session = await readSession(sessionPath);
546
751
  if (session === null) {
547
752
  throw new LoginRequiredError("no session — run `launchpad login`");
@@ -549,6 +754,9 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
549
754
  if (session.accessTokenExpiresAt - REFRESH_SKEW_MS > now()) {
550
755
  return { accessToken: session.accessToken, session };
551
756
  }
757
+ if (isGatewaySession(session)) {
758
+ return refreshGatewaySession(session, sessionPath, fetcher, now, sleep);
759
+ }
552
760
  if (session.resource === undefined) {
553
761
  throw new LoginRequiredError("session was written by a pre-0.7.1 CLI and is missing the resource indicator — run `launchpad login`");
554
762
  }
@@ -576,7 +784,39 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
576
784
  await writeSession(sessionPath, refreshed);
577
785
  return { accessToken: refreshed.accessToken, session: refreshed };
578
786
  }
579
- function describe5(e) {
787
+ async function refreshGatewaySession(session, sessionPath, fetcher, now, sleep) {
788
+ let pair;
789
+ try {
790
+ try {
791
+ pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
792
+ } catch (e) {
793
+ if (e instanceof GatewayRateLimitError && e.retryAfterSec <= MAX_ABSORBED_RETRY_AFTER_SEC) {
794
+ await sleep(e.retryAfterSec * 1000);
795
+ pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
796
+ } else {
797
+ throw e;
798
+ }
799
+ }
800
+ } catch (e) {
801
+ if (e instanceof GatewayTokenError && (e.httpStatus === 400 || e.httpStatus === 401)) {
802
+ await clearSession(sessionPath).catch(() => {
803
+ return;
804
+ });
805
+ throw new LoginRequiredError(`session revoked or expired — run \`launchpad login\` (gateway said: ${e.message})`);
806
+ }
807
+ throw e;
808
+ }
809
+ const refreshed = {
810
+ ...session,
811
+ accessToken: pair.accessToken,
812
+ refreshToken: pair.refreshToken,
813
+ accessTokenExpiresAt: now() + pair.expiresInSec * 1000,
814
+ issuedAt: new Date(now()).toISOString()
815
+ };
816
+ await writeSession(sessionPath, refreshed);
817
+ return { accessToken: refreshed.accessToken, session: refreshed };
818
+ }
819
+ function describe6(e) {
580
820
  return e instanceof Error ? e.message : String(e);
581
821
  }
582
822
  function buildAuthorizationUrl(params) {
@@ -598,7 +838,7 @@ async function apiJson(cfg, opts, fetcher = fetch) {
598
838
  try {
599
839
  parsed = await res.json();
600
840
  } catch (e) {
601
- throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe6(e)}`);
841
+ throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe7(e)}`);
602
842
  }
603
843
  return parsed;
604
844
  }
@@ -640,7 +880,7 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
640
880
  try {
641
881
  res = await fetcher(url, init);
642
882
  } catch (e) {
643
- throw new TransportError(`network error calling ${url}: ${describe6(e)}`);
883
+ throw new TransportError(`network error calling ${url}: ${describe7(e)}`);
644
884
  }
645
885
  if (res.status === 401) {
646
886
  const detail = await peek(res);
@@ -671,7 +911,7 @@ async function peek(res) {
671
911
  return "";
672
912
  }
673
913
  }
674
- function describe6(e) {
914
+ function describe7(e) {
675
915
  return e instanceof Error ? e.message : String(e);
676
916
  }
677
917
 
@@ -701,7 +941,7 @@ async function runApps(_args, io) {
701
941
  io.err(e.message);
702
942
  return 1;
703
943
  }
704
- io.err(`launchpad apps failed: ${describe7(e)}`);
944
+ io.err(`launchpad apps failed: ${describe8(e)}`);
705
945
  return 1;
706
946
  }
707
947
  }
@@ -749,7 +989,7 @@ function formatRelative(iso) {
749
989
  return `${hr}h ago`;
750
990
  return `${Math.floor(hr / 24)}d ago`;
751
991
  }
752
- function describe7(e) {
992
+ function describe8(e) {
753
993
  return e instanceof Error ? e.message : String(e);
754
994
  }
755
995
 
@@ -1046,7 +1286,7 @@ async function runClone(args, io) {
1046
1286
  io.err(`launchpad clone: ${e.message}`);
1047
1287
  return 1;
1048
1288
  }
1049
- io.err(`launchpad clone failed: ${describe8(e)}`);
1289
+ io.err(`launchpad clone failed: ${describe9(e)}`);
1050
1290
  return 1;
1051
1291
  }
1052
1292
  }
@@ -1063,7 +1303,7 @@ function formatBytes(n) {
1063
1303
  return `${(n / 1024).toFixed(1)}KB`;
1064
1304
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
1065
1305
  }
1066
- function describe8(e) {
1306
+ function describe9(e) {
1067
1307
  return e instanceof Error ? e.message : String(e);
1068
1308
  }
1069
1309
 
@@ -1129,7 +1369,7 @@ async function runCreate(args, io) {
1129
1369
  io.err(`launchpad create: ${e.message}`);
1130
1370
  return 1;
1131
1371
  }
1132
- io.err(`launchpad create failed: ${describe9(e)}`);
1372
+ io.err(`launchpad create failed: ${describe10(e)}`);
1133
1373
  return 1;
1134
1374
  }
1135
1375
  }
@@ -1214,13 +1454,13 @@ function printUsage(io) {
1214
1454
  ].join(`
1215
1455
  `));
1216
1456
  }
1217
- function describe9(e) {
1457
+ function describe10(e) {
1218
1458
  return e instanceof Error ? e.message : String(e);
1219
1459
  }
1220
1460
 
1221
1461
  // src/commands/deploy.ts
1222
- import { existsSync as existsSync4 } from "node:fs";
1223
- import * as path6 from "node:path";
1462
+ import { existsSync as existsSync5 } from "node:fs";
1463
+ import * as path7 from "node:path";
1224
1464
 
1225
1465
  // src/bundle/orchestrate.ts
1226
1466
  import { readFileSync as readFileSync4 } from "node:fs";
@@ -3306,8 +3546,8 @@ async function bundleAndDeploy(args) {
3306
3546
  }
3307
3547
 
3308
3548
  // src/commands/deploy.ts
3309
- import { parse as parseYaml4 } from "yaml";
3310
- import { readFileSync as readFileSync6 } from "node:fs";
3549
+ import { parse as parseYaml5 } from "yaml";
3550
+ import { readFileSync as readFileSync7 } from "node:fs";
3311
3551
 
3312
3552
  // src/deploy/git-files.ts
3313
3553
  import { spawn as spawn3 } from "node:child_process";
@@ -3882,7 +4122,7 @@ async function pollUntilApplied(args) {
3882
4122
  path: `/apps/${args.slug}/manifest/state`
3883
4123
  }, args.fetcher);
3884
4124
  } catch (e) {
3885
- args.io.err(`! state fetch failed (will retry): ${describe10(e)}`);
4125
+ args.io.err(`! state fetch failed (will retry): ${describe11(e)}`);
3886
4126
  await sleep(args.pollIntervalSec * 1000);
3887
4127
  continue;
3888
4128
  }
@@ -3942,7 +4182,7 @@ function deletePinIfPresent(cfg, slug, io) {
3942
4182
  rmSync(pinPath, { force: true });
3943
4183
  io.out(` (cleaned up obsolete pin file ${pinPath})`);
3944
4184
  } catch (e) {
3945
- io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe10(e)}`);
4185
+ io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe11(e)}`);
3946
4186
  }
3947
4187
  }
3948
4188
  async function loadSlugForResume(opts, io) {
@@ -3966,7 +4206,7 @@ async function defaultPrompt(question) {
3966
4206
  rl.close();
3967
4207
  }
3968
4208
  }
3969
- function describe10(e) {
4209
+ function describe11(e) {
3970
4210
  return e instanceof Error ? e.message : String(e);
3971
4211
  }
3972
4212
  function mapHttpError(e, slug, io) {
@@ -3990,7 +4230,7 @@ function mapHttpError(e, slug, io) {
3990
4230
  io.err(`launchpad deploy --apply: ${e.message}`);
3991
4231
  return 1;
3992
4232
  }
3993
- io.err(`launchpad deploy --apply failed: ${describe10(e)}`);
4233
+ io.err(`launchpad deploy --apply failed: ${describe11(e)}`);
3994
4234
  return 2;
3995
4235
  }
3996
4236
  function renderManifestError(loaded, io) {
@@ -4032,7 +4272,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
4032
4272
  try {
4033
4273
  manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
4034
4274
  } catch (e) {
4035
- const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe11(e)}`;
4275
+ const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe12(e)}`;
4036
4276
  if (opts.json) {
4037
4277
  io.out(JSON.stringify({ ok: false, kind: "git-error", message: msg }));
4038
4278
  } else {
@@ -4081,7 +4321,7 @@ function defaultGitHeadSha(cwd) {
4081
4321
  });
4082
4322
  return out.trim();
4083
4323
  }
4084
- function describe11(e) {
4324
+ function describe12(e) {
4085
4325
  return e instanceof Error ? e.message : String(e);
4086
4326
  }
4087
4327
  function mapHttpError2(e, slug, json, io) {
@@ -4367,6 +4607,46 @@ function handleNetworkError(e, io, slug, verb) {
4367
4607
  return 1;
4368
4608
  }
4369
4609
 
4610
+ // src/commands/infer-slug.ts
4611
+ import * as path6 from "node:path";
4612
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
4613
+ import { parse as parseYaml4 } from "yaml";
4614
+ var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
4615
+ function inferSlugFromCwd(cwd) {
4616
+ const base = path6.basename(cwd);
4617
+ const m = base.match(DIRNAME_RE);
4618
+ return m === null ? null : m[1];
4619
+ }
4620
+ function resolveManifestSlug(parsed) {
4621
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
4622
+ return null;
4623
+ }
4624
+ const meta = parsed.metadata;
4625
+ if (typeof meta.slug === "string")
4626
+ return meta.slug;
4627
+ if (typeof meta.name === "string")
4628
+ return meta.name;
4629
+ return null;
4630
+ }
4631
+ function inferSlugFromManifestFile(manifestPath) {
4632
+ if (!existsSync4(manifestPath))
4633
+ return null;
4634
+ try {
4635
+ return resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4636
+ } catch {
4637
+ return null;
4638
+ }
4639
+ }
4640
+ function inferSlug(opts) {
4641
+ const manifestPath = path6.resolve(opts.cwd, opts.file ?? "launchpad.yaml");
4642
+ const fromManifest = inferSlugFromManifestFile(manifestPath);
4643
+ const fromDir = inferSlugFromCwd(opts.cwd);
4644
+ if (fromManifest !== null && fromDir !== null && fromManifest !== fromDir) {
4645
+ opts.warn?.(`note: directory name suggests "${fromDir}" but ${path6.basename(manifestPath)} ` + `declares "${fromManifest}" — using the manifest. ` + `(Pass <slug> or --slug to override.)`);
4646
+ }
4647
+ return fromManifest ?? fromDir;
4648
+ }
4649
+
4370
4650
  // src/commands/deploy.ts
4371
4651
  var deployCommand = {
4372
4652
  name: "deploy",
@@ -4374,27 +4654,17 @@ var deployCommand = {
4374
4654
  run: runDeploy
4375
4655
  };
4376
4656
  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
4657
  async function runDeploy(args, io) {
4379
4658
  const flags = parseDeployFlags(args);
4380
4659
  if (typeof flags === "string") {
4381
4660
  io.err(`launchpad deploy: ${flags}`);
4661
+ const isFlagMisuse = flags.includes("is only valid with") || flags.includes("does not accept") || flags.includes("mutually exclusive");
4662
+ if (isFlagMisuse) {
4663
+ io.err(" (run `launchpad deploy` with no flags from your app directory for a content deploy)");
4664
+ return 64;
4665
+ }
4382
4666
  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]");
4667
+ io.err(deployUsage());
4398
4668
  return 64;
4399
4669
  }
4400
4670
  if (flags.mode.kind === "dry-run") {
@@ -4428,8 +4698,8 @@ async function runDeploy(args, io) {
4428
4698
  }, io);
4429
4699
  }
4430
4700
  const cwd = process.cwd();
4431
- const manifestPath = path6.join(cwd, "launchpad.yaml");
4432
- if (existsSync4(manifestPath)) {
4701
+ const manifestPath = path7.join(cwd, "launchpad.yaml");
4702
+ if (existsSync5(manifestPath)) {
4433
4703
  return runModelADeploy({ cwd, manifestPath, argv: args, io });
4434
4704
  }
4435
4705
  const parsed = parseArgs2(args);
@@ -4441,9 +4711,9 @@ async function runDeploy(args, io) {
4441
4711
  if (parsed.slug !== null) {
4442
4712
  slug = parsed.slug;
4443
4713
  } else {
4444
- const inferred = inferSlugFromCwd(process.cwd());
4714
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4445
4715
  if (inferred === null) {
4446
- io.err(`launchpad deploy: could not infer slug from cwd (${path6.basename(process.cwd())});
4716
+ io.err(`launchpad deploy: could not infer slug from cwd (${path7.basename(process.cwd())});
4447
4717
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4448
4718
  return 64;
4449
4719
  }
@@ -4463,10 +4733,10 @@ async function runDeploy(args, io) {
4463
4733
  return 1;
4464
4734
  }
4465
4735
  let contentManifestYaml = null;
4466
- const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
4467
- if (existsSync4(contentManifestPath)) {
4736
+ const contentManifestPath = path7.join(process.cwd(), "launchpad.yaml");
4737
+ if (existsSync5(contentManifestPath)) {
4468
4738
  try {
4469
- contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
4739
+ contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
4470
4740
  } catch {
4471
4741
  contentManifestYaml = null;
4472
4742
  }
@@ -4540,10 +4810,28 @@ async function runDeploy(args, io) {
4540
4810
  io.err(`launchpad deploy: ${e.message}`);
4541
4811
  return 1;
4542
4812
  }
4543
- io.err(`launchpad deploy failed: ${describe12(e)}`);
4813
+ io.err(`launchpad deploy failed: ${describe13(e)}`);
4544
4814
  return 1;
4545
4815
  }
4546
4816
  }
4817
+ function deployUsage() {
4818
+ return `usage: launchpad deploy [--message <text>] [--slug <slug>]
4819
+ ` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
4820
+ ` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
4821
+ ` + `provisioning modes:
4822
+ ` + ` launchpad deploy --resume <slug>
4823
+ ` + ` launchpad deploy --abandon <slug>
4824
+ ` + ` launchpad deploy --new --slug <slug> --display-name <name>
4825
+ ` + ` --app-type <${APP_TYPES.join("|")}>
4826
+ ` + ` --allowed-group <G_KEY> [--allowed-group ...]
4827
+ ` + `
4828
+ ` + `manifest-driven dry-run (read-only preview):
4829
+ ` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
4830
+ ` + `
4831
+ ` + `manifest-driven apply (runs server-side via portal-bot):
4832
+ ` + ` launchpad deploy --apply [--file <manifest>] [--at <sha>] [--re-pin]
4833
+ ` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
4834
+ }
4547
4835
  function parseArgs2(args) {
4548
4836
  let message = null;
4549
4837
  let slug = null;
@@ -4570,11 +4858,6 @@ function parseArgs2(args) {
4570
4858
  }
4571
4859
  return { slug, message };
4572
4860
  }
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
4861
  function makeBytesFetcher(bytes, pathSuffix) {
4579
4862
  return async (input, init) => {
4580
4863
  const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
@@ -4599,7 +4882,7 @@ function formatBytes2(n) {
4599
4882
  return `${(n / 1024).toFixed(1)}KB`;
4600
4883
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
4601
4884
  }
4602
- function describe12(e) {
4885
+ function describe13(e) {
4603
4886
  return e instanceof Error ? e.message : String(e);
4604
4887
  }
4605
4888
  function surfaceDeployExtras(body, io, slug) {
@@ -4624,29 +4907,18 @@ function surfaceDeployExtras(body, io, slug) {
4624
4907
  io.out(` Full list: \`launchpad status ${slug}\`.`);
4625
4908
  }
4626
4909
  }
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
4910
  async function runModelADeploy(args) {
4639
4911
  const { cwd, manifestPath, io } = args;
4640
4912
  let slug;
4641
4913
  try {
4642
- const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
4914
+ const metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
4643
4915
  if (metaSlug === null) {
4644
4916
  io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
4645
4917
  return 64;
4646
4918
  }
4647
4919
  slug = metaSlug;
4648
4920
  } catch (e) {
4649
- io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe12(e)}`);
4921
+ io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe13(e)}`);
4650
4922
  return 1;
4651
4923
  }
4652
4924
  if (!SLUG_RE4.test(slug)) {
@@ -4658,7 +4930,7 @@ async function runModelADeploy(args) {
4658
4930
  try {
4659
4931
  cfg = loadConfig();
4660
4932
  } catch (e) {
4661
- io.err(`launchpad deploy: ${describe12(e)}`);
4933
+ io.err(`launchpad deploy: ${describe13(e)}`);
4662
4934
  return 1;
4663
4935
  }
4664
4936
  let result;
@@ -4670,7 +4942,7 @@ async function runModelADeploy(args) {
4670
4942
  io.err(" run `launchpad login` to refresh your session.");
4671
4943
  return 1;
4672
4944
  }
4673
- io.err(`launchpad deploy: unexpected error: ${describe12(e)}`);
4945
+ io.err(`launchpad deploy: unexpected error: ${describe13(e)}`);
4674
4946
  return 1;
4675
4947
  }
4676
4948
  switch (result.kind) {
@@ -4715,6 +4987,11 @@ async function runModelADeploy(args) {
4715
4987
  io.err(` - ${String(f.path)} [${String(f.rule)}]`);
4716
4988
  }
4717
4989
  }
4990
+ if (errorCode === "bundle_policy_violation" || errorCode === "app_boundary_violation" || errorCode === "bad_build_command") {
4991
+ io.err("");
4992
+ io.err(" Nothing was committed or claimed by this attempt — fix the");
4993
+ io.err(" listed file(s) and re-run `launchpad deploy`; the retry is clean.");
4994
+ }
4718
4995
  return 1;
4719
4996
  }
4720
4997
  case "ok": {
@@ -4740,9 +5017,12 @@ async function runModelADeploy(args) {
4740
5017
  io.out("");
4741
5018
  io.out(message);
4742
5019
  io.out("");
5020
+ io.out(`Your bundle ships with this provisioning run — when lifecycle reaches "live",`);
5021
+ io.out("this deploy's content is what's serving. No second deploy needed.");
5022
+ io.out("");
4743
5023
  io.out("Next steps:");
4744
5024
  io.out(` launchpad status ${slug} # watch lifecycle (provisioning → live)`);
4745
- io.out(` launchpad deploy # re-run once lifecycle is "live"`);
5025
+ io.out(` launchpad deploy # only if the app comes up live WITHOUT your content (rare)`);
4746
5026
  return 0;
4747
5027
  }
4748
5028
  if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
@@ -4771,14 +5051,13 @@ async function runModelADeploy(args) {
4771
5051
  }
4772
5052
 
4773
5053
  // src/commands/envvars.ts
4774
- import * as path7 from "node:path";
5054
+ import * as path8 from "node:path";
4775
5055
  var envvarsCommand = {
4776
5056
  name: "envvars",
4777
5057
  summary: "list / set / remove production env vars (slug-scoped)",
4778
5058
  run: runEnvvars
4779
5059
  };
4780
5060
  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
5061
  var ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
4783
5062
  async function runEnvvars(args, io) {
4784
5063
  const parsed = parseArgs3(args);
@@ -4790,9 +5069,9 @@ async function runEnvvars(args, io) {
4790
5069
  if (parsed.slug !== null) {
4791
5070
  slug = parsed.slug;
4792
5071
  } else {
4793
- const inferred = inferSlugFromCwd2(process.cwd());
5072
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
4794
5073
  if (inferred === null) {
4795
- io.err(`launchpad envvars: could not infer slug from cwd (${path7.basename(process.cwd())});
5074
+ io.err(`launchpad envvars: could not infer slug from cwd (${path8.basename(process.cwd())});
4796
5075
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
4797
5076
  return 64;
4798
5077
  }
@@ -4860,7 +5139,7 @@ async function runEnvvars(args, io) {
4860
5139
  io.err(`launchpad envvars: ${e.message}`);
4861
5140
  return 1;
4862
5141
  }
4863
- io.err(`launchpad envvars failed: ${describe13(e)}`);
5142
+ io.err(`launchpad envvars failed: ${describe14(e)}`);
4864
5143
  return 1;
4865
5144
  }
4866
5145
  }
@@ -4938,11 +5217,6 @@ function parseArgs3(args) {
4938
5217
  }
4939
5218
  return null;
4940
5219
  }
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
5220
  function renderList(envVars, io) {
4947
5221
  if (envVars.length === 0) {
4948
5222
  io.out("(no env vars set)");
@@ -4962,13 +5236,13 @@ function renderList(envVars, io) {
4962
5236
  io.out(fmt(row));
4963
5237
  }
4964
5238
  }
4965
- function describe13(e) {
5239
+ function describe14(e) {
4966
5240
  return e instanceof Error ? e.message : String(e);
4967
5241
  }
4968
5242
 
4969
5243
  // 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";
5244
+ import { mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
5245
+ import { dirname as dirname4, resolve as resolve7, relative as relative3 } from "node:path";
4972
5246
  var generateCommand = {
4973
5247
  name: "generate",
4974
5248
  summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
@@ -4981,14 +5255,14 @@ async function runGenerate(args, io) {
4981
5255
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
4982
5256
  return 64;
4983
5257
  }
4984
- const manifestPath = resolve6(process.cwd(), flags.file ?? "launchpad.yaml");
5258
+ const manifestPath = resolve7(process.cwd(), flags.file ?? "launchpad.yaml");
4985
5259
  const result = loadManifest(manifestPath);
4986
5260
  if (result.kind !== "ok") {
4987
5261
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
4988
5262
  }
4989
5263
  const appRoot = dirname4(manifestPath);
4990
- const wranglerPath = resolve6(appRoot, "container", "wrangler.toml");
4991
- const workflowPath = resolve6(appRoot, ".github", "workflows", "deploy.yml");
5264
+ const wranglerPath = resolve7(appRoot, "container", "wrangler.toml");
5265
+ const workflowPath = resolve7(appRoot, ".github", "workflows", "deploy.yml");
4992
5266
  const wranglerOut = generateWranglerToml(result.manifest);
4993
5267
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
4994
5268
  if (flags.dryRun) {
@@ -5003,7 +5277,7 @@ async function runGenerate(args, io) {
5003
5277
  ];
5004
5278
  return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
5005
5279
  }
5006
- function applyOne(artefact, path8, out, force) {
5280
+ function applyOne(artefact, path9, out, force) {
5007
5281
  if (out.kind === "not-applicable") {
5008
5282
  return {
5009
5283
  artefact,
@@ -5011,32 +5285,32 @@ function applyOne(artefact, path8, out, force) {
5011
5285
  warnings: []
5012
5286
  };
5013
5287
  }
5014
- const existing = readIfExists(path8);
5288
+ const existing = readIfExists(path9);
5015
5289
  if (existing.kind === "read-error") {
5016
5290
  return {
5017
5291
  artefact,
5018
- action: { kind: "write-error", path: path8, message: existing.message },
5292
+ action: { kind: "write-error", path: path9, message: existing.message },
5019
5293
  warnings: out.warnings
5020
5294
  };
5021
5295
  }
5022
5296
  if (existing.kind === "ok" && existing.content === out.content) {
5023
- return { artefact, action: { kind: "unchanged", path: path8 }, warnings: out.warnings };
5297
+ return { artefact, action: { kind: "unchanged", path: path9 }, warnings: out.warnings };
5024
5298
  }
5025
5299
  if (existing.kind === "ok" && existing.content !== out.content && !force) {
5026
5300
  return {
5027
5301
  artefact,
5028
- action: { kind: "would-overwrite", path: path8 },
5302
+ action: { kind: "would-overwrite", path: path9 },
5029
5303
  warnings: out.warnings
5030
5304
  };
5031
5305
  }
5032
5306
  try {
5033
- mkdirSync(dirname4(path8), { recursive: true });
5034
- writeFileSync(path8, out.content, "utf8");
5307
+ mkdirSync(dirname4(path9), { recursive: true });
5308
+ writeFileSync(path9, out.content, "utf8");
5035
5309
  return {
5036
5310
  artefact,
5037
5311
  action: {
5038
5312
  kind: "written",
5039
- path: path8,
5313
+ path: path9,
5040
5314
  bytes: Buffer.byteLength(out.content, "utf8")
5041
5315
  },
5042
5316
  warnings: out.warnings
@@ -5046,16 +5320,16 @@ function applyOne(artefact, path8, out, force) {
5046
5320
  artefact,
5047
5321
  action: {
5048
5322
  kind: "write-error",
5049
- path: path8,
5323
+ path: path9,
5050
5324
  message: err.message ?? String(err)
5051
5325
  },
5052
5326
  warnings: out.warnings
5053
5327
  };
5054
5328
  }
5055
5329
  }
5056
- function readIfExists(path8) {
5330
+ function readIfExists(path9) {
5057
5331
  try {
5058
- return { kind: "ok", content: readFileSync7(path8, "utf8") };
5332
+ return { kind: "ok", content: readFileSync8(path9, "utf8") };
5059
5333
  } catch (err) {
5060
5334
  const e = err;
5061
5335
  if (e.code === "ENOENT")
@@ -5266,7 +5540,7 @@ function parseFlags(args) {
5266
5540
  }
5267
5541
 
5268
5542
  // src/groups/client.ts
5269
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
5543
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
5270
5544
  import { dirname as dirname5, join as join9 } from "node:path";
5271
5545
  var CACHE_TTL_MS = 60 * 60 * 1000;
5272
5546
  var CACHE_FILENAME = "groups.json";
@@ -5294,7 +5568,7 @@ async function fetchGroups(cfg, opts = {}) {
5294
5568
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5295
5569
  throw e;
5296
5570
  }
5297
- return { kind: "error", message: describe14(e) };
5571
+ return { kind: "error", message: describe15(e) };
5298
5572
  }
5299
5573
  if (typeof response !== "object" || response === null || !Array.isArray(response.groups) || !response.groups.every(isEntraGroup)) {
5300
5574
  return {
@@ -5307,10 +5581,10 @@ async function fetchGroups(cfg, opts = {}) {
5307
5581
  writeCache(cachePath, { fetchedAt, groups });
5308
5582
  return { kind: "ok", source: "fresh", fetchedAt, groups };
5309
5583
  }
5310
- function readCache(path8) {
5584
+ function readCache(path9) {
5311
5585
  let raw;
5312
5586
  try {
5313
- raw = readFileSync8(path8, "utf8");
5587
+ raw = readFileSync9(path9, "utf8");
5314
5588
  } catch {
5315
5589
  return null;
5316
5590
  }
@@ -5338,13 +5612,13 @@ function isEntraGroup(value) {
5338
5612
  const g = value;
5339
5613
  return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
5340
5614
  }
5341
- function writeCache(path8, envelope) {
5615
+ function writeCache(path9, envelope) {
5342
5616
  try {
5343
- mkdirSync2(dirname5(path8), { recursive: true });
5344
- writeFileSync2(path8, JSON.stringify(envelope), "utf8");
5617
+ mkdirSync2(dirname5(path9), { recursive: true });
5618
+ writeFileSync2(path9, JSON.stringify(envelope), "utf8");
5345
5619
  } catch {}
5346
5620
  }
5347
- function describe14(e) {
5621
+ function describe15(e) {
5348
5622
  return e instanceof Error ? e.message : String(e);
5349
5623
  }
5350
5624
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -5382,13 +5656,13 @@ function decodeJwtPayload(token) {
5382
5656
  try {
5383
5657
  json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
5384
5658
  } catch (e) {
5385
- throw new JwtParseError(`could not base64-decode JWT payload: ${describe15(e)}`);
5659
+ throw new JwtParseError(`could not base64-decode JWT payload: ${describe16(e)}`);
5386
5660
  }
5387
5661
  let parsed;
5388
5662
  try {
5389
5663
  parsed = JSON.parse(json);
5390
5664
  } catch (e) {
5391
- throw new JwtParseError(`JWT payload is not JSON: ${describe15(e)}`);
5665
+ throw new JwtParseError(`JWT payload is not JSON: ${describe16(e)}`);
5392
5666
  }
5393
5667
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5394
5668
  throw new JwtParseError(`JWT payload is not an object`);
@@ -5399,7 +5673,7 @@ function b64UrlToB64(s) {
5399
5673
  const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
5400
5674
  return padded.replace(/-/g, "+").replace(/_/g, "/");
5401
5675
  }
5402
- function describe15(e) {
5676
+ function describe16(e) {
5403
5677
  return e instanceof Error ? e.message : String(e);
5404
5678
  }
5405
5679
 
@@ -5426,7 +5700,7 @@ async function runGroupsWhoami(args, io) {
5426
5700
  try {
5427
5701
  payload = decodeJwtPayload(session.accessToken);
5428
5702
  } catch (e) {
5429
- const message = e instanceof JwtParseError ? e.message : describe16(e);
5703
+ const message = e instanceof JwtParseError ? e.message : describe17(e);
5430
5704
  if (json) {
5431
5705
  io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
5432
5706
  } else {
@@ -5507,7 +5781,7 @@ function parseFlags2(args) {
5507
5781
  }
5508
5782
  return { kind: "ok", json };
5509
5783
  }
5510
- function describe16(e) {
5784
+ function describe17(e) {
5511
5785
  return e instanceof Error ? e.message : String(e);
5512
5786
  }
5513
5787
 
@@ -5610,7 +5884,7 @@ async function loadGroups(io, refresh, json) {
5610
5884
  }
5611
5885
  return { kind: "error", code: 3 };
5612
5886
  }
5613
- renderFetchError(io, describe17(e), json);
5887
+ renderFetchError(io, describe18(e), json);
5614
5888
  return { kind: "error", code: 2 };
5615
5889
  }
5616
5890
  }
@@ -5805,23 +6079,23 @@ function renderTable2(io, groups) {
5805
6079
  for (const r of rows)
5806
6080
  io.out(fmt(r));
5807
6081
  }
5808
- function describe17(e) {
6082
+ function describe18(e) {
5809
6083
  return e instanceof Error ? e.message : String(e);
5810
6084
  }
5811
6085
 
5812
6086
  // src/commands/init.ts
5813
6087
  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";
6088
+ import { existsSync as existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
6089
+ import { resolve as resolve8 } from "node:path";
5816
6090
  import { stringify as yamlStringify } from "yaml";
5817
6091
 
5818
6092
  // src/detect/index.ts
5819
- import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
6093
+ import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
5820
6094
  import { join as join10 } from "node:path";
5821
6095
  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)));
6096
+ const hasPackageJson = existsSync6(join10(cwd, "package.json"));
6097
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join10(cwd, file)));
6098
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join10(cwd, n)));
5825
6099
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
5826
6100
  return {
5827
6101
  kind: "not-applicable",
@@ -5867,7 +6141,7 @@ var LOCKFILES = [
5867
6141
  { file: "yarn.lock", pm: "yarn" }
5868
6142
  ];
5869
6143
  function detectPackageManager(cwd) {
5870
- const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
6144
+ const present = LOCKFILES.filter(({ file }) => existsSync6(join10(cwd, file)));
5871
6145
  if (present.length === 0) {
5872
6146
  return {
5873
6147
  kind: "ambiguous",
@@ -5890,7 +6164,7 @@ function detectPackageManager(cwd) {
5890
6164
  function detectVitePresence(cwd) {
5891
6165
  for (const name of VITE_CONFIG_NAMES) {
5892
6166
  const p = join10(cwd, name);
5893
- if (existsSync5(p)) {
6167
+ if (existsSync6(p)) {
5894
6168
  return { kind: "ok", value: { path: p } };
5895
6169
  }
5896
6170
  }
@@ -5902,7 +6176,7 @@ function detectVitePresence(cwd) {
5902
6176
  function detectAppType(cwd) {
5903
6177
  const fnDir = join10(cwd, "functions");
5904
6178
  let hasFunctionsDir = false;
5905
- if (existsSync5(fnDir)) {
6179
+ if (existsSync6(fnDir)) {
5906
6180
  try {
5907
6181
  hasFunctionsDir = statSync(fnDir).isDirectory();
5908
6182
  } catch {
@@ -5913,7 +6187,7 @@ function detectAppType(cwd) {
5913
6187
  }
5914
6188
  function detectBuildCommand(cwd, pm) {
5915
6189
  const pkgJsonPath = join10(cwd, "package.json");
5916
- if (!existsSync5(pkgJsonPath)) {
6190
+ if (!existsSync6(pkgJsonPath)) {
5917
6191
  return {
5918
6192
  kind: "ambiguous",
5919
6193
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -5921,7 +6195,7 @@ function detectBuildCommand(cwd, pm) {
5921
6195
  }
5922
6196
  let pkgJson;
5923
6197
  try {
5924
- pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
6198
+ pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
5925
6199
  } catch (e) {
5926
6200
  return {
5927
6201
  kind: "ambiguous",
@@ -5956,7 +6230,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
5956
6230
  function detectDestinationDir(cwd, vite) {
5957
6231
  let text;
5958
6232
  try {
5959
- text = readFileSync9(vite.path, "utf8");
6233
+ text = readFileSync10(vite.path, "utf8");
5960
6234
  } catch (e) {
5961
6235
  return {
5962
6236
  kind: "ambiguous",
@@ -5985,8 +6259,8 @@ async function runInit(args, io, prompt) {
5985
6259
  return 64;
5986
6260
  }
5987
6261
  const { inputs, options } = parsed;
5988
- const outPath = resolve7(process.cwd(), options.out);
5989
- if (existsSync6(outPath) && !options.force) {
6262
+ const outPath = resolve8(process.cwd(), options.out);
6263
+ if (existsSync7(outPath) && !options.force) {
5990
6264
  io.err(`launchpad init: ${outPath} already exists`);
5991
6265
  io.err("Pass --force to overwrite.");
5992
6266
  return 64;
@@ -6033,7 +6307,7 @@ async function runInit(args, io, prompt) {
6033
6307
  }
6034
6308
  io.out(`✓ wrote ${outPath}`);
6035
6309
  if (options.gitignore) {
6036
- const gitignorePath = resolve7(process.cwd(), ".gitignore");
6310
+ const gitignorePath = resolve8(process.cwd(), ".gitignore");
6037
6311
  try {
6038
6312
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
6039
6313
  if (changed.length > 0) {
@@ -6402,10 +6676,10 @@ function buildManifest(inputs, detected) {
6402
6676
  function renderYaml(manifest) {
6403
6677
  return yamlStringify(manifest, { lineWidth: 0 });
6404
6678
  }
6405
- function ensureGitignoreEntries(path8, entries) {
6679
+ function ensureGitignoreEntries(path9, entries) {
6406
6680
  let current = "";
6407
- if (existsSync6(path8)) {
6408
- current = readFileSync10(path8, "utf8");
6681
+ if (existsSync7(path9)) {
6682
+ current = readFileSync11(path9, "utf8");
6409
6683
  }
6410
6684
  const lines = current.split(/\r?\n/);
6411
6685
  const present = new Set(lines.map((l) => l.trim()));
@@ -6423,7 +6697,7 @@ function ensureGitignoreEntries(path8, entries) {
6423
6697
  }
6424
6698
  }
6425
6699
  if (added.length > 0) {
6426
- writeFileSync3(path8, out, { encoding: "utf8" });
6700
+ writeFileSync3(path9, out, { encoding: "utf8" });
6427
6701
  }
6428
6702
  return added;
6429
6703
  }
@@ -6441,68 +6715,122 @@ async function defaultPrompt2(question, fallback) {
6441
6715
  }
6442
6716
 
6443
6717
  // src/commands/login.ts
6444
- var loginCommand = {
6445
- name: "login",
6446
- summary: "authenticate (browser-based PKCE) and store a session",
6447
- run: runLogin
6448
- };
6449
- async function runLogin(_args, io) {
6718
+ var REAL_DEPS = { gatewayLogin, legacyLogin: login };
6719
+ var loginCommand = makeLoginCommand();
6720
+ function makeLoginCommand(deps = REAL_DEPS) {
6721
+ return {
6722
+ name: "login",
6723
+ summary: "authenticate (browser-based PKCE) and store a session",
6724
+ run: (args, io) => runLogin(args, io, deps)
6725
+ };
6726
+ }
6727
+ async function runLogin(_args, io, deps) {
6450
6728
  try {
6451
6729
  const cfg = loadConfig();
6452
- io.out("Opening browser to authenticate with Cloudflare Access…");
6730
+ if (cfg.authLegacy) {
6731
+ io.out("LAUNCHPAD_AUTH_LEGACY=1 — using the legacy Cloudflare Access flow (deprecated; this path will be removed when the dual-auth window closes).");
6732
+ return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
6733
+ }
6734
+ io.out("Opening browser to sign in via the Launchpad auth gateway…");
6453
6735
  io.out("(if it doesn't open automatically, copy the URL below)");
6454
6736
  io.out("");
6455
- const session = await login({
6456
- botUrl: cfg.botUrl,
6457
- sessionPath: cfg.sessionPath,
6458
- onAuthUrl: (url) => {
6459
- io.out(url);
6460
- io.out("");
6461
- }
6462
- });
6463
- const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6464
- io.out(`Logged in. Session stored at ${cfg.sessionPath}`);
6465
- io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6466
- return 0;
6737
+ try {
6738
+ const session = await deps.gatewayLogin({
6739
+ gatewayUrl: cfg.authGatewayUrl,
6740
+ sessionPath: cfg.sessionPath,
6741
+ onAuthUrl: (url) => {
6742
+ io.out(url);
6743
+ io.out("");
6744
+ }
6745
+ });
6746
+ const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6747
+ io.out(`Logged in. Session stored at ${cfg.sessionPath}`);
6748
+ io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6749
+ return 0;
6750
+ } catch (e) {
6751
+ io.err(`Gateway login failed: ${describe19(e)}`);
6752
+ io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
6753
+ io.err("");
6754
+ return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
6755
+ }
6467
6756
  } catch (e) {
6468
- io.err(`launchpad login failed: ${describe18(e)}`);
6757
+ io.err(`launchpad login failed: ${describe19(e)}`);
6469
6758
  return 1;
6470
6759
  }
6471
6760
  }
6472
- function describe18(e) {
6761
+ async function runLegacyLogin(io, botUrl, sessionPath, deps) {
6762
+ io.out("Opening browser to authenticate with Cloudflare Access…");
6763
+ io.out("(if it doesn't open automatically, copy the URL below)");
6764
+ io.out("");
6765
+ const session = await deps.legacyLogin({
6766
+ botUrl,
6767
+ sessionPath,
6768
+ onAuthUrl: (url) => {
6769
+ io.out(url);
6770
+ io.out("");
6771
+ }
6772
+ });
6773
+ const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6774
+ io.out(`Logged in. Session stored at ${sessionPath}`);
6775
+ io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6776
+ return 0;
6777
+ }
6778
+ function describe19(e) {
6473
6779
  return e instanceof Error ? e.message : String(e);
6474
6780
  }
6475
6781
 
6476
6782
  // src/commands/logout.ts
6477
- var logoutCommand = {
6478
- name: "logout",
6479
- summary: "clear the persisted session",
6480
- run: runLogout
6481
- };
6482
- async function runLogout(_args, io) {
6783
+ var REAL_DEPS2 = { revoke: revokeGatewaySession };
6784
+ var logoutCommand = makeLogoutCommand();
6785
+ function makeLogoutCommand(deps = REAL_DEPS2) {
6786
+ return {
6787
+ name: "logout",
6788
+ summary: "revoke the session server-side and clear it locally",
6789
+ run: (args, io) => runLogout(args, io, deps)
6790
+ };
6791
+ }
6792
+ async function runLogout(_args, io, deps) {
6483
6793
  try {
6484
6794
  const cfg = loadConfig();
6795
+ let session = null;
6796
+ try {
6797
+ session = await readSession(cfg.sessionPath);
6798
+ } catch (e) {
6799
+ if (!(e instanceof SessionParseError))
6800
+ throw e;
6801
+ io.err(`warning: session file is corrupt (${e.message}) — clearing it without a server-side revoke.`);
6802
+ }
6803
+ if (session !== null && isGatewaySession(session)) {
6804
+ try {
6805
+ await deps.revoke({
6806
+ gatewayUrl: session.gatewayUrl,
6807
+ refreshToken: session.refreshToken
6808
+ });
6809
+ io.out("Server-side session revoked.");
6810
+ } catch (e) {
6811
+ io.err(`warning: could not revoke the session server-side (${describe20(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
6812
+ }
6813
+ }
6485
6814
  const had = await clearSession(cfg.sessionPath);
6486
6815
  io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
6487
6816
  return 0;
6488
6817
  } catch (e) {
6489
- io.err(`launchpad logout failed: ${describe19(e)}`);
6818
+ io.err(`launchpad logout failed: ${describe20(e)}`);
6490
6819
  return 1;
6491
6820
  }
6492
6821
  }
6493
- function describe19(e) {
6822
+ function describe20(e) {
6494
6823
  return e instanceof Error ? e.message : String(e);
6495
6824
  }
6496
6825
 
6497
6826
  // src/commands/logs.ts
6498
- import * as path8 from "node:path";
6827
+ import * as path9 from "node:path";
6499
6828
  var logsCommand = {
6500
6829
  name: "logs",
6501
6830
  summary: "show recent Pages deployment history (slug-scoped)",
6502
6831
  run: runLogs
6503
6832
  };
6504
6833
  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
6834
  var DEFAULT_LINES = 10;
6507
6835
  var MAX_LINES = 25;
6508
6836
  async function runLogs(args, io) {
@@ -6516,9 +6844,9 @@ async function runLogs(args, io) {
6516
6844
  if (parsed.slug !== null) {
6517
6845
  slug = parsed.slug;
6518
6846
  } else {
6519
- const inferred = inferSlugFromCwd3(process.cwd());
6847
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6520
6848
  if (inferred === null) {
6521
- io.err(`launchpad logs: could not infer slug from cwd (${path8.basename(process.cwd())});
6849
+ io.err(`launchpad logs: could not infer slug from cwd (${path9.basename(process.cwd())});
6522
6850
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6523
6851
  return 64;
6524
6852
  }
@@ -6561,7 +6889,7 @@ async function runLogs(args, io) {
6561
6889
  io.err(`launchpad logs: ${e.message}`);
6562
6890
  return 1;
6563
6891
  }
6564
- io.err(`launchpad logs failed: ${describe20(e)}`);
6892
+ io.err(`launchpad logs failed: ${describe21(e)}`);
6565
6893
  return 1;
6566
6894
  }
6567
6895
  }
@@ -6596,11 +6924,6 @@ function parseArgs5(args) {
6596
6924
  }
6597
6925
  return { slug, lines };
6598
6926
  }
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
6927
  function renderDeployments(deployments, io) {
6605
6928
  if (deployments.length === 0) {
6606
6929
  io.out("(no deployments yet)");
@@ -6646,19 +6969,18 @@ function formatRelative2(iso) {
6646
6969
  return `${hr}h ago`;
6647
6970
  return `${Math.floor(hr / 24)}d ago`;
6648
6971
  }
6649
- function describe20(e) {
6972
+ function describe21(e) {
6650
6973
  return e instanceof Error ? e.message : String(e);
6651
6974
  }
6652
6975
 
6653
6976
  // src/commands/merge.ts
6654
- import * as path9 from "node:path";
6977
+ import * as path10 from "node:path";
6655
6978
  var mergeCommand = {
6656
6979
  name: "merge",
6657
6980
  summary: "squash-merge a review-passed PR (slug-scoped)",
6658
6981
  run: runMerge
6659
6982
  };
6660
6983
  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
6984
  var HANDLED_STATUSES = [409, 422, 502, 503];
6663
6985
  async function runMerge(args, io) {
6664
6986
  const parsed = parseArgs6(args);
@@ -6671,9 +6993,9 @@ async function runMerge(args, io) {
6671
6993
  if (parsed.slug !== null) {
6672
6994
  slug = parsed.slug;
6673
6995
  } else {
6674
- const inferred = inferSlugFromCwd4(process.cwd());
6996
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
6675
6997
  if (inferred === null) {
6676
- io.err(`launchpad merge: could not infer slug from cwd (${path9.basename(process.cwd())});
6998
+ io.err(`launchpad merge: could not infer slug from cwd (${path10.basename(process.cwd())});
6677
6999
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
6678
7000
  return 64;
6679
7001
  }
@@ -6696,7 +7018,7 @@ async function runMerge(args, io) {
6696
7018
  try {
6697
7019
  body = await res.json();
6698
7020
  } catch (e) {
6699
- io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe21(e)}`);
7021
+ io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe22(e)}`);
6700
7022
  return 1;
6701
7023
  }
6702
7024
  io.out("");
@@ -6736,7 +7058,7 @@ async function runMerge(args, io) {
6736
7058
  io.err(`launchpad merge: ${e.message}`);
6737
7059
  return 1;
6738
7060
  }
6739
- io.err(`launchpad merge failed: ${describe21(e)}`);
7061
+ io.err(`launchpad merge failed: ${describe22(e)}`);
6740
7062
  return 1;
6741
7063
  }
6742
7064
  }
@@ -6770,11 +7092,6 @@ function parseArgs6(args) {
6770
7092
  return null;
6771
7093
  return { slug, prNumber };
6772
7094
  }
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
7095
  function renderBotError(status, env, io, prNumber = null) {
6779
7096
  const code = env?.error ?? "unknown";
6780
7097
  const detail = env?.message;
@@ -6825,12 +7142,12 @@ function renderBotError(status, env, io, prNumber = null) {
6825
7142
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
6826
7143
  }
6827
7144
  }
6828
- function describe21(e) {
7145
+ function describe22(e) {
6829
7146
  return e instanceof Error ? e.message : String(e);
6830
7147
  }
6831
7148
 
6832
7149
  // src/commands/plan.ts
6833
- import { resolve as resolve8 } from "node:path";
7150
+ import { resolve as resolve9 } from "node:path";
6834
7151
  var planCommand = {
6835
7152
  name: "plan",
6836
7153
  summary: "summarise what the manifest would deploy (offline)",
@@ -6843,7 +7160,7 @@ async function runPlan(args, io) {
6843
7160
  io.err("Usage: launchpad plan [--file <path>] [--json]");
6844
7161
  return 64;
6845
7162
  }
6846
- const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
7163
+ const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
6847
7164
  const result = loadManifest(manifestPath);
6848
7165
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
6849
7166
  }
@@ -7026,7 +7343,7 @@ var SLUG_RE8 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7026
7343
  var SLUG_MIN_LENGTH = 3;
7027
7344
  var SLUG_MAX_LENGTH = 58;
7028
7345
  async function runDestroy(args, io, prompt, isTty) {
7029
- const parsed = parseArgs7(args);
7346
+ const parsed = parseArgs7(args, process.cwd(), (l) => io.err(l));
7030
7347
  if (typeof parsed === "string") {
7031
7348
  io.err(`launchpad destroy: ${parsed}`);
7032
7349
  printUsage3(io);
@@ -7077,11 +7394,11 @@ async function runDestroy(args, io, prompt, isTty) {
7077
7394
  io.err(`launchpad destroy: ${e.message}`);
7078
7395
  return 1;
7079
7396
  }
7080
- io.err(`launchpad destroy failed: ${describe22(e)}`);
7397
+ io.err(`launchpad destroy failed: ${describe23(e)}`);
7081
7398
  return 1;
7082
7399
  }
7083
7400
  }
7084
- function parseArgs7(args, cwd = process.cwd()) {
7401
+ function parseArgs7(args, cwd = process.cwd(), warn) {
7085
7402
  let slug = null;
7086
7403
  let confirmSlug = null;
7087
7404
  let yes = false;
@@ -7128,7 +7445,7 @@ function parseArgs7(args, cwd = process.cwd()) {
7128
7445
  i += 1;
7129
7446
  }
7130
7447
  if (slug === null) {
7131
- const inferred = inferSlugFromCwd(cwd);
7448
+ const inferred = inferSlug({ cwd, warn });
7132
7449
  if (inferred === null) {
7133
7450
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7134
7451
  }
@@ -7278,7 +7595,7 @@ function printUsage3(io) {
7278
7595
  ].join(`
7279
7596
  `));
7280
7597
  }
7281
- function describe22(e) {
7598
+ function describe23(e) {
7282
7599
  return e instanceof Error ? e.message : String(e);
7283
7600
  }
7284
7601
 
@@ -7288,8 +7605,8 @@ import { stringify as stringifyYaml } from "yaml";
7288
7605
 
7289
7606
  // src/deploy/manifest-state.ts
7290
7607
  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);
7608
+ const path11 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
7609
+ const raw = await apiJson(cfg, { path: path11 }, fetcher);
7293
7610
  return {
7294
7611
  slug: raw.slug,
7295
7612
  hasAppFile: raw.hasAppFile,
@@ -7302,8 +7619,8 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
7302
7619
 
7303
7620
  // src/deploy/manifest-status.ts
7304
7621
  async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
7305
- const path10 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7306
- return apiJson(cfg, { path: path10 }, fetcher);
7622
+ const path11 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
7623
+ return apiJson(cfg, { path: path11 }, fetcher);
7307
7624
  }
7308
7625
 
7309
7626
  // src/deploy/deployment-status.ts
@@ -7347,7 +7664,7 @@ var pullCommand = {
7347
7664
  };
7348
7665
  var SLUG_RE9 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7349
7666
  async function runPull(args, io) {
7350
- const parsed = parseArgs8(args);
7667
+ const parsed = parseArgs8(args, process.cwd(), (l) => io.err(l));
7351
7668
  if (typeof parsed === "string") {
7352
7669
  io.err(`launchpad pull: ${parsed}`);
7353
7670
  printUsage4(io);
@@ -7422,7 +7739,7 @@ async function runPull(args, io) {
7422
7739
  io.err(`launchpad pull: ${e.message}`);
7423
7740
  return 1;
7424
7741
  }
7425
- io.err(`launchpad pull failed: ${describe23(e)}`);
7742
+ io.err(`launchpad pull failed: ${describe24(e)}`);
7426
7743
  return 1;
7427
7744
  }
7428
7745
  }
@@ -7433,7 +7750,7 @@ async function fetchLiveDeploymentBestEffort(cfg, slug) {
7433
7750
  return null;
7434
7751
  }
7435
7752
  }
7436
- function parseArgs8(args, cwd = process.cwd()) {
7753
+ function parseArgs8(args, cwd = process.cwd(), warn) {
7437
7754
  let slug = null;
7438
7755
  let out = null;
7439
7756
  let status = false;
@@ -7474,7 +7791,7 @@ function parseArgs8(args, cwd = process.cwd()) {
7474
7791
  i += 1;
7475
7792
  }
7476
7793
  if (slug === null) {
7477
- const inferred = inferSlugFromCwd(cwd);
7794
+ const inferred = inferSlug({ cwd, warn });
7478
7795
  if (inferred === null) {
7479
7796
  return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7480
7797
  }
@@ -7492,8 +7809,9 @@ function printUsage4(io) {
7492
7809
  " Reads the deployed launchpad.yaml for an app via the bot.",
7493
7810
  " No local platform-repo or terraform required.",
7494
7811
  "",
7495
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7496
- " the directory name. Explicit --slug or positional override.",
7812
+ " With no slug, it is inferred from the local launchpad.yaml's",
7813
+ " declared slug first, then from a launchpad-app-<slug>/",
7814
+ " directory name. Explicit --slug or positional override.",
7497
7815
  "",
7498
7816
  " --status read the role-redacted status block (what your",
7499
7817
  " role may see) instead of the spec manifest.",
@@ -7502,18 +7820,188 @@ function printUsage4(io) {
7502
7820
  ].join(`
7503
7821
  `));
7504
7822
  }
7505
- function describe23(e) {
7823
+ function describe24(e) {
7506
7824
  return e instanceof Error ? e.message : String(e);
7507
7825
  }
7508
7826
 
7827
+ // src/commands/recover.ts
7828
+ var recoverCommand = {
7829
+ name: "recover",
7830
+ summary: "repair a terminal-failed app record by reconciling against live state",
7831
+ run: runRecover
7832
+ };
7833
+ var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7834
+ async function runRecover(args, io) {
7835
+ const parsed = parseRecoverArgs(args, process.cwd(), (l) => io.err(l));
7836
+ if (typeof parsed === "string") {
7837
+ io.err(`launchpad recover: ${parsed}`);
7838
+ printUsage5(io);
7839
+ return 64;
7840
+ }
7841
+ const cfg = loadConfig();
7842
+ try {
7843
+ io.out(`Reconciling "${parsed.slug}" against live Cloudflare state …`);
7844
+ const res = await apiRaw(cfg, {
7845
+ method: "POST",
7846
+ path: `/apps/${parsed.slug}/recover`,
7847
+ jsonBody: undefined,
7848
+ nonThrowingStatuses: [409, 503]
7849
+ });
7850
+ const body = await res.json().catch(() => null);
7851
+ if (parsed.json) {
7852
+ io.out(JSON.stringify({ httpStatus: res.status, ...body ?? {} }, null, 2));
7853
+ return res.status === 200 ? 0 : 1;
7854
+ }
7855
+ if (res.status === 200 && body !== null && "outcome" in body) {
7856
+ renderSuccess2(body, io);
7857
+ return 0;
7858
+ }
7859
+ if (res.status === 503) {
7860
+ const msg = body !== null && "message" in body && typeof body.message === "string" ? body.message : "live state unavailable — nothing was changed; retry shortly.";
7861
+ io.err(`launchpad recover: ${msg}`);
7862
+ return 1;
7863
+ }
7864
+ if (res.status === 409 && body !== null && "error" in body) {
7865
+ renderRefusal(parsed.slug, body, io);
7866
+ return 1;
7867
+ }
7868
+ io.err(`launchpad recover: bot returned an unexpected HTTP ${res.status} response.`);
7869
+ return 1;
7870
+ } catch (e) {
7871
+ return mapError(e, parsed.slug, io);
7872
+ }
7873
+ }
7874
+ function renderSuccess2(body, io) {
7875
+ if (body.outcome === "noop_already_healthy") {
7876
+ io.out(`${body.slug}: already healthy — nothing to recover.`);
7877
+ io.out(` ${body.message}`);
7878
+ return;
7879
+ }
7880
+ io.out(`${body.slug}: REPAIRED — registry record reconciled to live.`);
7881
+ if (body.before !== undefined) {
7882
+ io.out(` before: ${body.before.lifecycle}` + (body.before.reason !== null ? ` (${body.before.reason})` : ""));
7883
+ }
7884
+ if (body.after !== undefined) {
7885
+ io.out(` after: ${body.after.lifecycle}`);
7886
+ }
7887
+ const checked = body.checked;
7888
+ if (checked !== undefined) {
7889
+ io.out(` verified: Pages project "${checked.pagesProject}" exists` + (checked.latestDeployment !== null ? `; latest production deployment ${checked.latestDeployment.id} ` + `(${checked.latestDeployment.buildStatus}, ${checked.latestDeployment.createdOn})` : ""));
7890
+ if (checked.olderContentServing) {
7891
+ io.out(" note: the LATEST build failed — an older successful deployment is what's serving.");
7892
+ }
7893
+ }
7894
+ io.out("");
7895
+ io.out(`Run \`launchpad status ${body.slug}\` — it now reports the live deployment truth.`);
7896
+ }
7897
+ function renderRefusal(slug, body, io) {
7898
+ io.err(`launchpad recover: refused — "${slug}" was NOT repaired.`);
7899
+ if (typeof body.message === "string") {
7900
+ io.err(` ${body.message}`);
7901
+ }
7902
+ const checked = body.checked;
7903
+ if (checked !== undefined) {
7904
+ io.err(" checked:");
7905
+ io.err(` Pages project "${checked.pagesProject}": ${checked.projectExists ? "exists" : "MISSING"}`);
7906
+ if (checked.latestDeployment !== null) {
7907
+ io.err(` latest production deployment: ${checked.latestDeployment.id} (${checked.latestDeployment.buildStatus})`);
7908
+ } else if (checked.projectExists) {
7909
+ io.err(" latest production deployment: none");
7910
+ }
7911
+ }
7912
+ }
7913
+ function mapError(e, slug, io) {
7914
+ if (e instanceof UnauthenticatedError) {
7915
+ io.err(`launchpad recover: ${e.message}`);
7916
+ io.err(" session expired, run `launchpad login`");
7917
+ return 1;
7918
+ }
7919
+ if (e instanceof ForbiddenError) {
7920
+ io.err(`launchpad recover: not authorised for app "${slug}" (you must be an owner or editor).`);
7921
+ return 1;
7922
+ }
7923
+ if (e instanceof NotFoundError) {
7924
+ io.err(`launchpad recover: app "${slug}" not found.`);
7925
+ return 1;
7926
+ }
7927
+ if (e instanceof ApiError || e instanceof TransportError) {
7928
+ io.err(`launchpad recover: ${e.message}`);
7929
+ return 1;
7930
+ }
7931
+ io.err(`launchpad recover failed: ${e instanceof Error ? e.message : String(e)}`);
7932
+ return 1;
7933
+ }
7934
+ function parseRecoverArgs(args, cwd = process.cwd(), warn) {
7935
+ let slug = null;
7936
+ let json = false;
7937
+ let i = 0;
7938
+ while (i < args.length) {
7939
+ const a = args[i] ?? "";
7940
+ if (a === "--slug") {
7941
+ const v = args[i + 1];
7942
+ if (v === undefined)
7943
+ return "missing value for --slug";
7944
+ if (slug !== null)
7945
+ return "cannot mix --slug with positional slug (or pass --slug twice)";
7946
+ slug = v;
7947
+ i += 2;
7948
+ continue;
7949
+ }
7950
+ if (a === "--json") {
7951
+ json = true;
7952
+ i += 1;
7953
+ continue;
7954
+ }
7955
+ if (a.startsWith("--")) {
7956
+ return `unknown flag "${a}"`;
7957
+ }
7958
+ if (slug !== null) {
7959
+ return "cannot mix --slug with positional slug (or pass two positional slugs)";
7960
+ }
7961
+ slug = a;
7962
+ i += 1;
7963
+ }
7964
+ if (slug === null) {
7965
+ slug = inferSlug({ cwd, warn });
7966
+ }
7967
+ if (slug === null) {
7968
+ 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.";
7969
+ }
7970
+ if (!SLUG_RE10.test(slug)) {
7971
+ return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
7972
+ }
7973
+ return { slug, json };
7974
+ }
7975
+ function printUsage5(io) {
7976
+ io.err([
7977
+ "usage: launchpad recover [<slug>] [--slug <slug>] [--json]",
7978
+ "",
7979
+ " Repair an app whose registry record is stuck at a terminal",
7980
+ " provisioning failure although the app is actually live (e.g. a",
7981
+ " since-fixed platform bug failed the record after content shipped).",
7982
+ "",
7983
+ " The bot verifies LIVE Cloudflare state first: the record is only",
7984
+ " repaired when the Pages project exists and a successful production",
7985
+ " deployment is serving. A not-live app is refused with what was",
7986
+ " checked — recover never fabricates a live state.",
7987
+ "",
7988
+ " Recovering an already-healthy app is a no-op success.",
7989
+ "",
7990
+ "Flags:",
7991
+ " --slug <slug> override cwd inference.",
7992
+ " --json emit machine-readable JSON to stdout."
7993
+ ].join(`
7994
+ `));
7995
+ }
7996
+
7509
7997
  // src/commands/status.ts
7510
- import { readFileSync as readFileSync11 } from "node:fs";
7998
+ import { readFileSync as readFileSync12 } from "node:fs";
7511
7999
  var statusCommand = {
7512
8000
  name: "status",
7513
8001
  summary: "show drift between local launchpad.yaml and deployed state",
7514
8002
  run: runStatus
7515
8003
  };
7516
- var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
8004
+ var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7517
8005
  async function fetchLifecycle(cfg, slug) {
7518
8006
  try {
7519
8007
  return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
@@ -7541,14 +8029,14 @@ function mapBotError(e, slug, io) {
7541
8029
  io.err(`launchpad status: ${e.message}`);
7542
8030
  return 2;
7543
8031
  }
7544
- io.err(`launchpad status failed: ${describe24(e)}`);
8032
+ io.err(`launchpad status failed: ${describe25(e)}`);
7545
8033
  return 2;
7546
8034
  }
7547
8035
  async function runStatus(args, io) {
7548
- const parsed = parseArgs9(args);
8036
+ const parsed = parseArgs9(args, process.cwd(), (l) => io.err(l));
7549
8037
  if (typeof parsed === "string") {
7550
8038
  io.err(`launchpad status: ${parsed}`);
7551
- printUsage5(io);
8039
+ printUsage6(io);
7552
8040
  return 64;
7553
8041
  }
7554
8042
  const cfg = loadConfig();
@@ -7562,34 +8050,36 @@ async function runStatus(args, io) {
7562
8050
  emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
7563
8051
  return 0;
7564
8052
  }
7565
- let localYaml;
8053
+ let localYaml = null;
7566
8054
  try {
7567
- localYaml = readFileSync11(parsed.file, "utf8");
8055
+ localYaml = readFileSync12(parsed.file, "utf8");
7568
8056
  } 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.)`);
8057
+ if (!isEnoent(e)) {
8058
+ io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe25(e)}`);
8059
+ return 2;
7572
8060
  }
7573
- return 2;
7574
8061
  }
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
- }
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}`);
8062
+ let local = null;
8063
+ if (localYaml !== null) {
8064
+ let localObj;
8065
+ try {
8066
+ const { parse: parseYaml6 } = await import("yaml");
8067
+ localObj = parseYaml6(localYaml);
8068
+ } catch (e) {
8069
+ io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe25(e)}`);
8070
+ return 2;
7588
8071
  }
7589
- return 2;
8072
+ const localParse = parseManifest(localObj);
8073
+ if (localParse.kind !== "ok") {
8074
+ io.err(`launchpad status: ${parsed.file} failed schema validation:`);
8075
+ for (const issue of localParse.issues) {
8076
+ io.err(` - ${issue.path}: ${issue.message}`);
8077
+ }
8078
+ return 2;
8079
+ }
8080
+ local = localParse.manifest;
8081
+ warnSecretShape(local.production_env, io);
7590
8082
  }
7591
- const local = localParse.manifest;
7592
- warnSecretShape(local.production_env, io);
7593
8083
  let state;
7594
8084
  try {
7595
8085
  state = await fetchManifestState(cfg, parsed.slug, { includeManifest: true });
@@ -7605,7 +8095,7 @@ async function runStatus(args, io) {
7605
8095
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7606
8096
  return mapBotError(e, parsed.slug, io);
7607
8097
  }
7608
- io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
8098
+ io.err(`launchpad status: live deployment state unavailable (${describe25(e)}) — ` + `the report below is from the platform manifest view only.`);
7609
8099
  }
7610
8100
  let standingExceptions = null;
7611
8101
  try {
@@ -7614,7 +8104,7 @@ async function runStatus(args, io) {
7614
8104
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7615
8105
  return mapBotError(e, parsed.slug, io);
7616
8106
  }
7617
- io.err(`launchpad status: standing-exception inventory unavailable (${describe24(e)}).`);
8107
+ io.err(`launchpad status: standing-exception inventory unavailable (${describe25(e)}).`);
7618
8108
  }
7619
8109
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
7620
8110
  const live = deployment?.liveDeployment ?? null;
@@ -7623,6 +8113,7 @@ async function runStatus(args, io) {
7623
8113
  const out = {
7624
8114
  state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
7625
8115
  slug: parsed.slug,
8116
+ ...local === null ? { drift: null } : {},
7626
8117
  deployedSha: state.lastAppliedManifestSha,
7627
8118
  headSha: state.appRepoHeadSha,
7628
8119
  hasOpenPr: state.openPr !== null,
@@ -7635,12 +8126,36 @@ async function runStatus(args, io) {
7635
8126
  emit3(out, parsed.json, io);
7636
8127
  return 0;
7637
8128
  }
8129
+ if (local === null) {
8130
+ const out = {
8131
+ state: "live_drift_unknown",
8132
+ slug: parsed.slug,
8133
+ drift: null,
8134
+ deployedSha: state.lastAppliedManifestSha,
8135
+ headSha: state.appRepoHeadSha,
8136
+ hasOpenPr: state.openPr !== null,
8137
+ openPrNumber: state.openPr?.number ?? null,
8138
+ driftFields: [],
8139
+ driftDetails: [],
8140
+ ...deploymentKnown ? { deployment } : {},
8141
+ ...standingExceptions !== null ? { standingExceptions } : {}
8142
+ };
8143
+ emit3(out, parsed.json, io);
8144
+ if (parsed.strict) {
8145
+ if (deployment?.liveDeployment?.buildStatus === "failure") {
8146
+ io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
8147
+ return 1;
8148
+ }
8149
+ io.err(`launchpad status: --strict: drift not evaluated — no local ` + `launchpad.yaml here; live state looks healthy, exiting 0.`);
8150
+ }
8151
+ return 0;
8152
+ }
7638
8153
  let deployedObj;
7639
8154
  try {
7640
- const { parse: parseYaml5 } = await import("yaml");
7641
- deployedObj = parseYaml5(state.manifestYaml);
8155
+ const { parse: parseYaml6 } = await import("yaml");
8156
+ deployedObj = parseYaml6(state.manifestYaml);
7642
8157
  } catch (e) {
7643
- io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
8158
+ io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe25(e)}`);
7644
8159
  return 2;
7645
8160
  }
7646
8161
  const deployedParse = parseManifest(deployedObj);
@@ -7673,17 +8188,17 @@ async function runStatus(args, io) {
7673
8188
  }
7674
8189
  function computeDrift(local, deployed) {
7675
8190
  const diffs = [];
7676
- const cmp = (path10, l, d) => {
8191
+ const cmp = (path11, l, d) => {
7677
8192
  const li = l ?? null;
7678
8193
  const di = d ?? null;
7679
8194
  if (typeof li === "object" || typeof di === "object") {
7680
8195
  if (JSON.stringify(li) !== JSON.stringify(di)) {
7681
- diffs.push({ path: path10, local: l, deployed: d });
8196
+ diffs.push({ path: path11, local: l, deployed: d });
7682
8197
  }
7683
8198
  return;
7684
8199
  }
7685
8200
  if (li !== di) {
7686
- diffs.push({ path: path10, local: l, deployed: d });
8201
+ diffs.push({ path: path11, local: l, deployed: d });
7687
8202
  }
7688
8203
  };
7689
8204
  cmp("metadata.name", local.metadata.name, deployed.metadata.name);
@@ -7748,18 +8263,28 @@ function emit3(out, asJson, io) {
7748
8263
  return;
7749
8264
  case "live_no_content":
7750
8265
  io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
8266
+ surfaceNoLocalManifestNote(out, io);
7751
8267
  surfaceHeadVsDeployed(out, io);
7752
8268
  surfaceDeployment(out, io);
7753
8269
  surfaceExceptions(out, io);
7754
8270
  return;
7755
8271
  case "no_deployed_manifest":
7756
8272
  io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
8273
+ surfaceNoLocalManifestNote(out, io);
7757
8274
  surfaceHeadVsDeployed(out, io);
7758
8275
  surfaceDeployment(out, io);
7759
8276
  surfaceExceptions(out, io);
7760
8277
  return;
7761
8278
  case "live_content_untracked":
7762
8279
  io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
8280
+ surfaceNoLocalManifestNote(out, io);
8281
+ surfaceHeadVsDeployed(out, io);
8282
+ surfaceDeployment(out, io);
8283
+ surfaceExceptions(out, io);
8284
+ return;
8285
+ case "live_drift_unknown":
8286
+ io.out(`${out.slug}: live` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
8287
+ surfaceNoLocalManifestNote(out, io);
7763
8288
  surfaceHeadVsDeployed(out, io);
7764
8289
  surfaceDeployment(out, io);
7765
8290
  surfaceExceptions(out, io);
@@ -7783,6 +8308,11 @@ function emit3(out, asJson, io) {
7783
8308
  return;
7784
8309
  }
7785
8310
  }
8311
+ function surfaceNoLocalManifestNote(out, io) {
8312
+ if (out.drift !== null)
8313
+ return;
8314
+ io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
8315
+ }
7786
8316
  function triggerLabel(trigger) {
7787
8317
  if (trigger === "git-push")
7788
8318
  return "git push";
@@ -7870,7 +8400,7 @@ function warnSecretShape(env, io) {
7870
8400
  }
7871
8401
  }
7872
8402
  }
7873
- function parseArgs9(args, cwd = process.cwd()) {
8403
+ function parseArgs9(args, cwd = process.cwd(), warn) {
7874
8404
  let slug = null;
7875
8405
  let file = "./launchpad.yaml";
7876
8406
  let json = false;
@@ -7917,54 +8447,68 @@ function parseArgs9(args, cwd = process.cwd()) {
7917
8447
  i += 1;
7918
8448
  }
7919
8449
  if (slug === null) {
7920
- const inferred = inferSlugFromCwd(cwd);
8450
+ const inferred = inferSlug({ cwd, file, warn });
7921
8451
  if (inferred === null) {
7922
8452
  return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
7923
8453
  }
7924
8454
  slug = inferred;
7925
8455
  }
7926
- if (!SLUG_RE10.test(slug)) {
7927
- return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
8456
+ if (!SLUG_RE11.test(slug)) {
8457
+ return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
7928
8458
  }
7929
8459
  return { slug, file, json, strict };
7930
8460
  }
7931
- function printUsage5(io) {
8461
+ function printUsage6(io) {
7932
8462
  io.err([
7933
8463
  "usage: launchpad status [<slug>] [--slug <slug>] [--file <path>] [--json] [--strict]",
7934
8464
  "",
7935
8465
  " Compare local launchpad.yaml against the deployed state.",
7936
8466
  " No local platform-repo or terraform required.",
7937
8467
  "",
7938
- " When run inside launchpad-app-<slug>/, slug is inferred from",
7939
- " the directory name. Explicit --slug or positional override.",
8468
+ " With no explicit slug, it is inferred from (in order): the",
8469
+ " local manifest's declared slug (./launchpad.yaml or --file),",
8470
+ " then a launchpad-app-<slug>/ directory name. An explicit",
8471
+ " --slug or positional always overrides; when the manifest and",
8472
+ " the directory name disagree, the manifest wins (with a note).",
8473
+ "",
8474
+ " With a known slug but NO local manifest, status degrades to",
8475
+ " the live-truth-only view (lifecycle + deployment; drift not",
8476
+ " checked) and exits 0.",
7940
8477
  "",
7941
8478
  "Flags:",
7942
8479
  " --file <path> local manifest path (default: ./launchpad.yaml).",
7943
- " Does NOT influence slug resolution.",
7944
- " --slug <slug> override cwd inference.",
8480
+ " Also consulted for slug inference; --slug or a",
8481
+ " positional slug still overrides.",
8482
+ " --slug <slug> override inference.",
7945
8483
  " --json emit machine-readable JSON to stdout.",
7946
8484
  " --strict exit 1 on drift. Default is report-only (exit 0).",
8485
+ " With no local manifest, drift can't be evaluated:",
8486
+ " exit 0 unless the live build itself failed.",
7947
8487
  "",
7948
8488
  "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.)."
8489
+ " 0 = in sync, OR drift in default (report-only) mode, OR the",
8490
+ " live-truth-only view (no local manifest).",
8491
+ " 1 = drift, when --strict is set (or a failed live build under",
8492
+ " --strict with no local manifest).",
8493
+ " 2 = error (network, auth, unreadable/invalid local manifest, etc.)."
7952
8494
  ].join(`
7953
8495
  `));
7954
8496
  }
7955
- function describe24(e) {
8497
+ function describe25(e) {
7956
8498
  return e instanceof Error ? e.message : String(e);
7957
8499
  }
8500
+ function isEnoent(e) {
8501
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
8502
+ }
7958
8503
 
7959
8504
  // src/commands/review.ts
7960
- import * as path10 from "node:path";
8505
+ import * as path11 from "node:path";
7961
8506
  var reviewCommand = {
7962
8507
  name: "review",
7963
8508
  summary: "show the review state for a PR (slug-scoped)",
7964
8509
  run: runReview
7965
8510
  };
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])$/;
8511
+ var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
7968
8512
  async function runReview(args, io) {
7969
8513
  const parsed = parseArgs10(args);
7970
8514
  if (parsed === null) {
@@ -7976,16 +8520,16 @@ async function runReview(args, io) {
7976
8520
  if (parsed.slug !== null) {
7977
8521
  slug = parsed.slug;
7978
8522
  } else {
7979
- const inferred = inferSlugFromCwd5(process.cwd());
8523
+ const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
7980
8524
  if (inferred === null) {
7981
- io.err(`launchpad review: could not infer slug from cwd (${path10.basename(process.cwd())});
8525
+ io.err(`launchpad review: could not infer slug from cwd (${path11.basename(process.cwd())});
7982
8526
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
7983
8527
  return 64;
7984
8528
  }
7985
8529
  slug = inferred;
7986
8530
  }
7987
- if (!SLUG_RE11.test(slug)) {
7988
- io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE11.source}`);
8531
+ if (!SLUG_RE12.test(slug)) {
8532
+ io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE12.source}`);
7989
8533
  return 64;
7990
8534
  }
7991
8535
  try {
@@ -8028,7 +8572,7 @@ async function runReview(args, io) {
8028
8572
  io.err(`launchpad review: ${e.message}`);
8029
8573
  return 1;
8030
8574
  }
8031
- io.err(`launchpad review failed: ${describe25(e)}`);
8575
+ io.err(`launchpad review failed: ${describe26(e)}`);
8032
8576
  return 1;
8033
8577
  }
8034
8578
  }
@@ -8060,11 +8604,6 @@ function parseArgs10(args) {
8060
8604
  }
8061
8605
  return { slug, prNumber };
8062
8606
  }
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
8607
  function renderReview(r, io) {
8069
8608
  const rv = r.review;
8070
8609
  io.out(`Review of PR #${rv.prNumber} (${rv.slug})`);
@@ -8111,12 +8650,12 @@ function severityRank(s) {
8111
8650
  return 3;
8112
8651
  }
8113
8652
  }
8114
- function describe25(e) {
8653
+ function describe26(e) {
8115
8654
  return e instanceof Error ? e.message : String(e);
8116
8655
  }
8117
8656
 
8118
8657
  // src/deploy/rollback.ts
8119
- import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
8658
+ import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
8120
8659
  import { resolve as resolvePath2 } from "node:path";
8121
8660
  import { createInterface as createInterface3 } from "node:readline/promises";
8122
8661
 
@@ -8171,7 +8710,7 @@ async function runRollback(opts, io, deps = {}) {
8171
8710
  cwd: process.cwd()
8172
8711
  });
8173
8712
  } catch (e) {
8174
- io.err(`launchpad rollback: git rev-parse failed to start: ${describe26(e)}`);
8713
+ io.err(`launchpad rollback: git rev-parse failed to start: ${describe27(e)}`);
8175
8714
  return 2;
8176
8715
  }
8177
8716
  if (revParse.exitCode !== 0) {
@@ -8186,7 +8725,7 @@ async function runRollback(opts, io, deps = {}) {
8186
8725
  try {
8187
8726
  show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
8188
8727
  } catch (e) {
8189
- io.err(`launchpad rollback: git show failed to start: ${describe26(e)}`);
8728
+ io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
8190
8729
  return 2;
8191
8730
  }
8192
8731
  if (show.exitCode !== 0) {
@@ -8220,7 +8759,7 @@ async function runRollback(opts, io, deps = {}) {
8220
8759
  try {
8221
8760
  writeFileSync5(manifestPath, historicalYaml, "utf8");
8222
8761
  } catch (e) {
8223
- io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe26(e)}`);
8762
+ io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
8224
8763
  return 2;
8225
8764
  }
8226
8765
  io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
@@ -8240,16 +8779,16 @@ async function runRollback(opts, io, deps = {}) {
8240
8779
  yes: true
8241
8780
  }, io, applyDeps);
8242
8781
  }
8243
- function readCurrentManifest(path11) {
8244
- if (!existsSync7(path11))
8782
+ function readCurrentManifest(path12) {
8783
+ if (!existsSync8(path12))
8245
8784
  return null;
8246
8785
  let raw;
8247
8786
  try {
8248
- raw = readFileSync12(path11, "utf8");
8787
+ raw = readFileSync13(path12, "utf8");
8249
8788
  } catch {
8250
8789
  return null;
8251
8790
  }
8252
- const parsed = parseManifest2(raw, path11);
8791
+ const parsed = parseManifest2(raw, path12);
8253
8792
  if (parsed.kind !== "ok")
8254
8793
  return null;
8255
8794
  return summarise(parsed.manifest);
@@ -8302,7 +8841,7 @@ async function defaultPrompt4(question) {
8302
8841
  rl.close();
8303
8842
  }
8304
8843
  }
8305
- function describe26(e) {
8844
+ function describe27(e) {
8306
8845
  return e instanceof Error ? e.message : String(e);
8307
8846
  }
8308
8847
  function renderManifestError3(result, io) {
@@ -8401,7 +8940,7 @@ function parseArgs11(args) {
8401
8940
  }
8402
8941
 
8403
8942
  // src/secrets/push.ts
8404
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
8943
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
8405
8944
  import { resolve as resolvePath3 } from "node:path";
8406
8945
 
8407
8946
  // src/secrets/env-parse.ts
@@ -8456,16 +8995,16 @@ async function runSecretsPush(opts, io, deps = {}) {
8456
8995
  return 0;
8457
8996
  }
8458
8997
  const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
8459
- if (!existsSync8(envPath)) {
8998
+ if (!existsSync9(envPath)) {
8460
8999
  io.err(`launchpad secrets push: ${envPath}`);
8461
9000
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
8462
9001
  return 2;
8463
9002
  }
8464
9003
  let envText;
8465
9004
  try {
8466
- envText = readFileSync13(envPath, "utf8");
9005
+ envText = readFileSync14(envPath, "utf8");
8467
9006
  } catch (e) {
8468
- io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
9007
+ io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
8469
9008
  return 2;
8470
9009
  }
8471
9010
  const parsed = parseEnv(envText);
@@ -8596,10 +9135,10 @@ function mapPushError(e, slug, name, io) {
8596
9135
  io.err(`✗ secrets push ${name}: ${e.message}`);
8597
9136
  return 1;
8598
9137
  }
8599
- io.err(`✗ secrets push ${name} failed: ${describe27(e)}`);
9138
+ io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
8600
9139
  return 2;
8601
9140
  }
8602
- function describe27(e) {
9141
+ function describe28(e) {
8603
9142
  return e instanceof Error ? e.message : String(e);
8604
9143
  }
8605
9144
  function renderManifestError4(result, io) {
@@ -8735,32 +9274,32 @@ function renderManifestError5(result, io) {
8735
9274
  }
8736
9275
 
8737
9276
  // src/secrets/set.ts
8738
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
9277
+ import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
8739
9278
  import { resolve as resolvePath5 } from "node:path";
8740
- import { parse as parseYaml5 } from "yaml";
9279
+ import { parse as parseYaml6 } from "yaml";
8741
9280
  var CELL_LABEL2 = {
8742
9281
  present: "PRESENT",
8743
9282
  missing: "MISSING",
8744
9283
  not_deployed: "NOT_DEPLOYED"
8745
9284
  };
8746
9285
  function loadSet(fleetFile, setName, io) {
8747
- const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
8748
- if (!existsSync9(path11)) {
8749
- io.err(`✗ ${path11}`);
9286
+ const path12 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
9287
+ if (!existsSync10(path12)) {
9288
+ io.err(`✗ ${path12}`);
8750
9289
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
8751
9290
  return 2;
8752
9291
  }
8753
9292
  let obj;
8754
9293
  try {
8755
- obj = parseYaml5(readFileSync14(path11, "utf8"));
9294
+ obj = parseYaml6(readFileSync15(path12, "utf8"));
8756
9295
  } catch (e) {
8757
- io.err(`✗ ${path11}`);
9296
+ io.err(`✗ ${path12}`);
8758
9297
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
8759
9298
  return 1;
8760
9299
  }
8761
9300
  const parsed = parseFleetSecretSets(obj);
8762
9301
  if (!parsed.ok) {
8763
- io.err(`✗ ${path11}`);
9302
+ io.err(`✗ ${path12}`);
8764
9303
  io.err(` ${parsed.issues.length} schema issue(s):`);
8765
9304
  for (const i of parsed.issues)
8766
9305
  io.err(` ${i.path}: ${i.message}`);
@@ -8769,7 +9308,7 @@ function loadSet(fleetFile, setName, io) {
8769
9308
  const set = parsed.manifest.secretSets.find((s) => s.name === setName);
8770
9309
  if (set === undefined) {
8771
9310
  const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
8772
- io.err(`✗ secret-set "${setName}" not found in ${path11}`);
9311
+ io.err(`✗ secret-set "${setName}" not found in ${path12}`);
8773
9312
  io.err(` declared sets: ${names}`);
8774
9313
  return 1;
8775
9314
  }
@@ -8837,14 +9376,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
8837
9376
  if (typeof set === "number")
8838
9377
  return set;
8839
9378
  const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
8840
- if (!existsSync9(envPath)) {
9379
+ if (!existsSync10(envPath)) {
8841
9380
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
8842
9381
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
8843
9382
  return 2;
8844
9383
  }
8845
9384
  let envText;
8846
9385
  try {
8847
- envText = readFileSync14(envPath, "utf8");
9386
+ envText = readFileSync15(envPath, "utf8");
8848
9387
  } catch (e) {
8849
9388
  io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
8850
9389
  return 2;
@@ -8988,8 +9527,8 @@ function setPushExit(e) {
8988
9527
  }
8989
9528
 
8990
9529
  // src/commands/secrets-template.ts
8991
- import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
8992
- import { resolve as resolve9 } from "node:path";
9530
+ import { existsSync as existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
9531
+ import { resolve as resolve10 } from "node:path";
8993
9532
  async function runSecretsTemplate(args, io) {
8994
9533
  const flags = parseFlags4(args);
8995
9534
  if (flags.kind === "usage-error") {
@@ -8997,7 +9536,7 @@ async function runSecretsTemplate(args, io) {
8997
9536
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
8998
9537
  return 64;
8999
9538
  }
9000
- const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
9539
+ const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
9001
9540
  const result = loadManifest(manifestPath);
9002
9541
  const renderResult = renderManifest(result, io);
9003
9542
  if (renderResult.kind !== "ok") {
@@ -9011,8 +9550,8 @@ async function runSecretsTemplate(args, io) {
9011
9550
  }
9012
9551
  return 0;
9013
9552
  }
9014
- const outPath = resolve9(process.cwd(), flags.out);
9015
- if (existsSync10(outPath) && !flags.force) {
9553
+ const outPath = resolve10(process.cwd(), flags.out);
9554
+ if (existsSync11(outPath) && !flags.force) {
9016
9555
  io.err(`launchpad secrets template: ${outPath} already exists`);
9017
9556
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
9018
9557
  return 64;
@@ -9300,8 +9839,8 @@ function printHelp2(io) {
9300
9839
 
9301
9840
  // src/commands/skills.ts
9302
9841
  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";
9842
+ import { dirname as dirname6, join as join11, resolve as resolve11 } from "node:path";
9843
+ import { promises as fs5, existsSync as existsSync12 } from "node:fs";
9305
9844
  import { homedir as homedir2 } from "node:os";
9306
9845
  var BUNDLE_PREFIX = "launchpad-";
9307
9846
  var BUNDLED_SKILLS = [
@@ -9343,7 +9882,7 @@ async function runSkills(args, io) {
9343
9882
  return 64;
9344
9883
  }
9345
9884
  } catch (e) {
9346
- io.err(`launchpad skills ${action}: ${describe28(e)}`);
9885
+ io.err(`launchpad skills ${action}: ${describe29(e)}`);
9347
9886
  return 1;
9348
9887
  }
9349
9888
  }
@@ -9368,11 +9907,11 @@ function resolveInstallEnv() {
9368
9907
  function defaultBundleDir() {
9369
9908
  const here = dirname6(fileURLToPath(import.meta.url));
9370
9909
  const candidates = [
9371
- resolve10(here, "..", "skills"),
9372
- resolve10(here, "..", "..", "skills")
9910
+ resolve11(here, "..", "skills"),
9911
+ resolve11(here, "..", "..", "skills")
9373
9912
  ];
9374
9913
  for (const c of candidates) {
9375
- if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
9914
+ if (existsSync12(join11(c, "launchpad-onboard", "SKILL.md"))) {
9376
9915
  return c;
9377
9916
  }
9378
9917
  }
@@ -9452,10 +9991,10 @@ async function doList(io) {
9452
9991
  }
9453
9992
  return 0;
9454
9993
  }
9455
- async function readVersion(path11) {
9994
+ async function readVersion(path12) {
9456
9995
  let text;
9457
9996
  try {
9458
- text = await fs5.readFile(path11, "utf8");
9997
+ text = await fs5.readFile(path12, "utf8");
9459
9998
  } catch {
9460
9999
  return null;
9461
10000
  }
@@ -9465,15 +10004,15 @@ async function readVersion(path11) {
9465
10004
  const m = /^version:\s*(.+?)\s*$/m.exec(front);
9466
10005
  return m === null ? null : m[1] ?? null;
9467
10006
  }
9468
- async function isDir(path11) {
10007
+ async function isDir(path12) {
9469
10008
  try {
9470
- const stat = await fs5.stat(path11);
10009
+ const stat = await fs5.stat(path12);
9471
10010
  return stat.isDirectory();
9472
10011
  } catch {
9473
10012
  return false;
9474
10013
  }
9475
10014
  }
9476
- function describe28(e) {
10015
+ function describe29(e) {
9477
10016
  return e instanceof Error ? e.message : String(e);
9478
10017
  }
9479
10018
 
@@ -9481,13 +10020,13 @@ function describe28(e) {
9481
10020
  import { execFile, spawn as spawn5 } from "node:child_process";
9482
10021
  import { promisify } from "node:util";
9483
10022
  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";
10023
+ import { dirname as dirname7, resolve as resolve12, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
9485
10024
  import { homedir as homedir3, tmpdir } from "node:os";
9486
- import { readFileSync as readFileSync15, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
10025
+ import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
9487
10026
 
9488
10027
  // src/commands/channel-auth.ts
9489
10028
  import { createServer as createServer2 } from "node:http";
9490
- import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
10029
+ import { createHash as createHash2, randomBytes as randomBytes5 } from "node:crypto";
9491
10030
  var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
9492
10031
  var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
9493
10032
  var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
@@ -9497,7 +10036,7 @@ function base64url(b) {
9497
10036
  return b.toString("base64url");
9498
10037
  }
9499
10038
  function pkcePair() {
9500
- const verifier = base64url(randomBytes4(32));
10039
+ const verifier = base64url(randomBytes5(32));
9501
10040
  const challenge = base64url(createHash2("sha256").update(verifier).digest());
9502
10041
  return { verifier, challenge };
9503
10042
  }
@@ -9526,9 +10065,9 @@ async function startLoopback(state, timeoutMs) {
9526
10065
  resolveCode(code);
9527
10066
  }
9528
10067
  });
9529
- const bound = await new Promise((resolve11) => {
9530
- server.once("error", () => resolve11(false));
9531
- server.listen(0, "127.0.0.1", () => resolve11(true));
10068
+ const bound = await new Promise((resolve12) => {
10069
+ server.once("error", () => resolve12(false));
10070
+ server.listen(0, "127.0.0.1", () => resolve12(true));
9532
10071
  });
9533
10072
  if (!bound)
9534
10073
  return null;
@@ -9543,7 +10082,7 @@ async function startLoopback(state, timeoutMs) {
9543
10082
  async function runChannelLoopbackUpdate(deps) {
9544
10083
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9545
10084
  const { verifier, challenge } = pkcePair();
9546
- const state = base64url(randomBytes4(16));
10085
+ const state = base64url(randomBytes5(16));
9547
10086
  const loopback = await startLoopback(state, timeoutMs);
9548
10087
  if (!loopback)
9549
10088
  return { ok: false, reason: "no-browser", detail: "could not bind a loopback port" };
@@ -9712,7 +10251,7 @@ function resolveLatestVersion() {
9712
10251
  }
9713
10252
  function detectInstallChannel() {
9714
10253
  try {
9715
- return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
10254
+ return readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
9716
10255
  } catch {
9717
10256
  return "github";
9718
10257
  }
@@ -9775,7 +10314,7 @@ function errStderr(e) {
9775
10314
  return "";
9776
10315
  }
9777
10316
  async function detectPackageManager2() {
9778
- const pkgRoot = resolve11(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
10317
+ const pkgRoot = resolve12(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
9779
10318
  const candidates = [];
9780
10319
  const npmRoot = await pmRoot("npm");
9781
10320
  if (npmRoot !== null)
@@ -9783,7 +10322,7 @@ async function detectPackageManager2() {
9783
10322
  const pnpmRoot = await pmRoot("pnpm");
9784
10323
  if (pnpmRoot !== null)
9785
10324
  candidates.push(["pnpm", pnpmRoot]);
9786
- candidates.push(["bun", resolve11(homedir3(), ".bun/install/global/node_modules")]);
10325
+ candidates.push(["bun", resolve12(homedir3(), ".bun/install/global/node_modules")]);
9787
10326
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
9788
10327
  return matches.length === 1 ? matches[0][0] : null;
9789
10328
  }
@@ -9919,8 +10458,8 @@ function printHelp4(io) {
9919
10458
  }
9920
10459
 
9921
10460
  // src/commands/validate.ts
9922
- import { readFileSync as readFileSync16 } from "node:fs";
9923
- import { dirname as dirname8, resolve as resolve12 } from "node:path";
10461
+ import { readFileSync as readFileSync17 } from "node:fs";
10462
+ import { dirname as dirname8, resolve as resolve13 } from "node:path";
9924
10463
  var validateCommand = {
9925
10464
  name: "validate",
9926
10465
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -9934,12 +10473,12 @@ async function runValidate(args, io) {
9934
10473
  return 64;
9935
10474
  }
9936
10475
  const { file, json, strictGroups } = parseResult;
9937
- const path11 = resolve12(process.cwd(), file ?? "launchpad.yaml");
9938
- const result = loadManifest(path11);
10476
+ const path12 = resolve13(process.cwd(), file ?? "launchpad.yaml");
10477
+ const result = loadManifest(path12);
9939
10478
  if (result.kind !== "ok") {
9940
10479
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
9941
10480
  }
9942
- const boundary = checkBoundary(path11, result.manifest.app !== undefined);
10481
+ const boundary = checkBoundary(path12, result.manifest.app !== undefined);
9943
10482
  const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
9944
10483
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
9945
10484
  }
@@ -9953,7 +10492,7 @@ function checkBoundary(manifestPath, declared) {
9953
10492
  }
9954
10493
  let manifestYaml;
9955
10494
  try {
9956
- manifestYaml = readFileSync16(manifestPath, "utf8");
10495
+ manifestYaml = readFileSync17(manifestPath, "utf8");
9957
10496
  } catch {
9958
10497
  manifestYaml = null;
9959
10498
  }
@@ -9994,7 +10533,7 @@ async function checkGroup(allowedGroup, cfg) {
9994
10533
  if (e instanceof ForbiddenError) {
9995
10534
  return { kind: "forbidden", message: e.message };
9996
10535
  }
9997
- return { kind: "network", message: describe29(e) };
10536
+ return { kind: "network", message: describe30(e) };
9998
10537
  }
9999
10538
  }
10000
10539
  async function checkGroups(groups) {
@@ -10207,7 +10746,7 @@ function parseArgs12(args) {
10207
10746
  }
10208
10747
  return { kind: "ok", file, json, strictGroups };
10209
10748
  }
10210
- function describe29(e) {
10749
+ function describe30(e) {
10211
10750
  return e instanceof Error ? e.message : String(e);
10212
10751
  }
10213
10752
 
@@ -10256,11 +10795,11 @@ async function runWhoami(_args, io) {
10256
10795
  io.err(e.message);
10257
10796
  return 1;
10258
10797
  }
10259
- io.err(`launchpad whoami failed: ${describe30(e)}`);
10798
+ io.err(`launchpad whoami failed: ${describe31(e)}`);
10260
10799
  return 1;
10261
10800
  }
10262
10801
  }
10263
- function describe30(e) {
10802
+ function describe31(e) {
10264
10803
  return e instanceof Error ? e.message : String(e);
10265
10804
  }
10266
10805
 
@@ -10268,14 +10807,14 @@ function describe30(e) {
10268
10807
  import { spawn as spawn6 } from "node:child_process";
10269
10808
  import { homedir as homedir4 } from "node:os";
10270
10809
  import { join as join13 } from "node:path";
10271
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
10810
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
10272
10811
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
10273
10812
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10274
10813
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
10275
10814
  var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
10276
10815
  function readCache2() {
10277
10816
  try {
10278
- const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
10817
+ const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
10279
10818
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
10280
10819
  const latest = raw.latest;
10281
10820
  return {
@@ -10391,6 +10930,7 @@ var COMMANDS = [
10391
10930
  generateCommand,
10392
10931
  pullCommand,
10393
10932
  statusCommand,
10933
+ recoverCommand,
10394
10934
  destroyCommand,
10395
10935
  rollbackCommand,
10396
10936
  groupsCommand,