@m-kopa/launchpad-cli 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.52.0";
22
+ var CLI_VERSION = "0.53.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -1342,7 +1342,7 @@ async function extractToDir(body, destDir, opts = {}) {
1342
1342
  const absDest = path3.resolve(destDir);
1343
1343
  try {
1344
1344
  const existing = await fs2.readdir(absDest);
1345
- if (existing.length > 0) {
1345
+ if (existing.length > 0 && opts.allowNonEmpty !== true) {
1346
1346
  throw new DestinationNotEmptyError(`destination ${absDest} is not empty (${existing.length} entries); refusing to extract`);
1347
1347
  }
1348
1348
  } catch (e) {
@@ -1470,8 +1470,18 @@ async function writeBaseShaMarker(cwd, sha) {
1470
1470
  await ensureExcluded(cwd);
1471
1471
  const dir = path4.resolve(cwd, MARKER_DIR);
1472
1472
  await fs3.mkdir(dir, { recursive: true });
1473
- await fs3.writeFile(path4.resolve(cwd, MARKER_RELPATH), `${sha}
1474
- `, "utf8");
1473
+ const marker = path4.resolve(cwd, MARKER_RELPATH);
1474
+ const temporary = `${marker}.tmp-${process.pid}-${crypto.randomUUID()}`;
1475
+ try {
1476
+ await fs3.writeFile(temporary, `${sha}
1477
+ `, { encoding: "utf8", mode: 384 });
1478
+ await fs3.rename(temporary, marker);
1479
+ } catch (error) {
1480
+ await fs3.rm(temporary, { force: true }).catch(() => {
1481
+ return;
1482
+ });
1483
+ throw error;
1484
+ }
1475
1485
  }
1476
1486
  async function readBaseShaMarker(cwd) {
1477
1487
  let raw;
@@ -1483,6 +1493,18 @@ async function readBaseShaMarker(cwd) {
1483
1493
  const sha = raw.trim();
1484
1494
  return isValidBaseSha(sha) ? sha : null;
1485
1495
  }
1496
+ async function readBaseShaMarkerState(cwd) {
1497
+ let raw;
1498
+ try {
1499
+ raw = await fs3.readFile(path4.resolve(cwd, MARKER_RELPATH), "utf8");
1500
+ } catch (error) {
1501
+ if (isErrno3(error) && error.code === "ENOENT")
1502
+ return { kind: "missing" };
1503
+ return { kind: "malformed" };
1504
+ }
1505
+ const sha = raw.trim();
1506
+ return isValidBaseSha(sha) ? { kind: "valid", sha } : { kind: "malformed" };
1507
+ }
1486
1508
  async function ensureExcluded(cwd) {
1487
1509
  const excludePath = path4.resolve(cwd, EXCLUDE_RELPATH);
1488
1510
  let current = "";
@@ -1501,6 +1523,9 @@ async function ensureExcluded(cwd) {
1501
1523
  await fs3.writeFile(excludePath, `${current}${sep}${EXCLUDE_LINE}
1502
1524
  `, "utf8");
1503
1525
  }
1526
+ function isErrno3(error) {
1527
+ return error instanceof Error && "code" in error;
1528
+ }
1504
1529
 
1505
1530
  // src/commands/clone.ts
1506
1531
  var cloneCommand = {
@@ -1525,12 +1550,7 @@ async function runClone(args, io) {
1525
1550
  try {
1526
1551
  cfg = loadConfig();
1527
1552
  destDir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
1528
- try {
1529
- await fs4.access(destDir);
1530
- destPreExisted = true;
1531
- } catch {
1532
- destPreExisted = false;
1533
- }
1553
+ destPreExisted = await pathExists(destDir);
1534
1554
  io.out(`Cloning ${slug} into ${destDir} …`);
1535
1555
  const res = await apiRaw(cfg, { path: `/apps/${slug}/source-bundle` });
1536
1556
  if (res.body === null) {
@@ -1538,67 +1558,93 @@ async function runClone(args, io) {
1538
1558
  return 1;
1539
1559
  }
1540
1560
  const headerSha = res.headers.get(BASE_SHA_HEADER);
1541
- const baseSha = headerSha !== null && isValidBaseSha(headerSha) ? headerSha : null;
1561
+ if (headerSha === null || !isValidBaseSha(headerSha)) {
1562
+ io.err("launchpad clone: bot did not return trustworthy source provenance; no files were extracted.");
1563
+ io.err(" Retry later or ask the Launchpad team to verify the bot version.");
1564
+ return 1;
1565
+ }
1566
+ const baseSha = headerSha;
1542
1567
  const stats = await extractToDir(res.body, destDir, { stripComponents: 1 });
1543
1568
  await initGitRepo({
1544
1569
  cwd: destDir,
1545
1570
  initialCommitMessage: `Initial baseline from launchpad clone ${slug}`
1546
1571
  });
1547
- let stamped = false;
1548
- if (baseSha !== null) {
1549
- try {
1550
- await writeBaseShaMarker(destDir, baseSha);
1551
- stamped = true;
1552
- } catch (e) {
1553
- io.err(`launchpad clone: warning — could not record base revision: ${describe9(e)}`);
1554
- io.err("(the clone is usable, but deploy cannot detect divergence)");
1555
- }
1556
- }
1572
+ const markerFailure = await persistCloneMarker({
1573
+ slug,
1574
+ destDir,
1575
+ destPreExisted,
1576
+ baseSha,
1577
+ io
1578
+ });
1579
+ if (markerFailure !== null)
1580
+ return markerFailure;
1557
1581
  io.out("");
1558
1582
  io.out(`Cloned ${stats.fileCount} files (${formatBytes(stats.byteCount)}) into ${destDir}`);
1559
- if (stamped && baseSha !== null) {
1560
- io.out(`Base revision ${baseSha.slice(0, 12)} recorded — \`launchpad deploy\` will warn if main moves past it.`);
1561
- }
1583
+ io.out(`Base revision ${baseSha.slice(0, 12)} recorded - \`launchpad pull\` can refresh this clone safely.`);
1562
1584
  io.out(`No remote configured — use \`launchpad deploy\` to ship changes back.`);
1563
1585
  return 0;
1564
- } catch (e) {
1565
- if (e instanceof UnauthenticatedError) {
1566
- io.err(e.message);
1567
- return 1;
1568
- }
1569
- if (e instanceof ForbiddenError) {
1570
- io.err(e.message);
1571
- return 1;
1572
- }
1573
- if (e instanceof NotFoundError) {
1574
- io.err(`launchpad clone: app "${slug}" not found (or you have no access)`);
1575
- return 1;
1576
- }
1577
- if (e instanceof DestinationNotEmptyError) {
1578
- io.err(`launchpad clone: ${e.message}`);
1579
- io.err("(move or delete the existing directory and re-run)");
1580
- return 1;
1581
- }
1582
- if (e instanceof TarballParseError) {
1583
- io.err(`launchpad clone: malformed tarball from bot: ${e.message}`);
1584
- if (!destPreExisted)
1585
- await cleanupBestEffort(slug);
1586
- return 1;
1587
- }
1588
- if (e instanceof PathTraversalError) {
1589
- io.err(`launchpad clone: refusing to extract — ${e.message}`);
1590
- if (!destPreExisted)
1591
- await cleanupBestEffort(slug);
1592
- return 1;
1593
- }
1594
- if (e instanceof GitInitError) {
1595
- io.err(`launchpad clone: ${e.message}`);
1596
- return 1;
1586
+ } catch (error) {
1587
+ return handleCloneError(error, slug, destPreExisted, io);
1588
+ }
1589
+ }
1590
+ async function persistCloneMarker(args) {
1591
+ try {
1592
+ await writeBaseShaMarker(args.destDir, args.baseSha);
1593
+ return null;
1594
+ } catch (error) {
1595
+ args.io.err(`launchpad clone: could not record source provenance: ${describe9(error)}`);
1596
+ if (args.destPreExisted) {
1597
+ args.io.err(` Remove the generated files from ${args.destDir}, then retry.`);
1598
+ } else {
1599
+ await cleanupBestEffort(args.slug);
1600
+ args.io.err(" The incomplete clone was removed. Retry the command.");
1597
1601
  }
1598
- io.err(`launchpad clone failed: ${describe9(e)}`);
1599
1602
  return 1;
1600
1603
  }
1601
1604
  }
1605
+ async function handleCloneError(error, slug, destPreExisted, io) {
1606
+ if (isCloneAuthError(error)) {
1607
+ io.err(error.message);
1608
+ } else if (error instanceof NotFoundError) {
1609
+ io.err(`launchpad clone: app "${slug}" not found (or you have no access)`);
1610
+ } else if (error instanceof DestinationNotEmptyError) {
1611
+ io.err(`launchpad clone: ${error.message}`);
1612
+ io.err("(move or delete the existing directory and re-run)");
1613
+ } else if (error instanceof TarballParseError) {
1614
+ io.err(`launchpad clone: malformed tarball from bot: ${error.message}`);
1615
+ if (!destPreExisted)
1616
+ await cleanupBestEffort(slug);
1617
+ } else if (error instanceof PathTraversalError) {
1618
+ io.err(`launchpad clone: refusing to extract - ${error.message}`);
1619
+ if (!destPreExisted)
1620
+ await cleanupBestEffort(slug);
1621
+ } else if (error instanceof GitInitError) {
1622
+ await handleGitInitError(error, slug, destPreExisted, io);
1623
+ } else {
1624
+ io.err(`launchpad clone failed: ${describe9(error)}`);
1625
+ }
1626
+ return 1;
1627
+ }
1628
+ async function handleGitInitError(error, slug, destPreExisted, io) {
1629
+ io.err(`launchpad clone: ${error.message}`);
1630
+ if (destPreExisted)
1631
+ return;
1632
+ await cleanupBestEffort(slug);
1633
+ io.err(" The incomplete clone was removed. Retry the command.");
1634
+ }
1635
+ function isCloneAuthError(error) {
1636
+ return error instanceof UnauthenticatedError || error instanceof ForbiddenError;
1637
+ }
1638
+ async function pathExists(target) {
1639
+ try {
1640
+ await fs4.lstat(target);
1641
+ return true;
1642
+ } catch (error) {
1643
+ if (isErrno4(error) && error.code === "ENOENT")
1644
+ return false;
1645
+ throw error;
1646
+ }
1647
+ }
1602
1648
  async function cleanupBestEffort(slug) {
1603
1649
  const dir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
1604
1650
  await fs4.rm(dir, { recursive: true, force: true }).catch(() => {
@@ -1615,6 +1661,9 @@ function formatBytes(n) {
1615
1661
  function describe9(e) {
1616
1662
  return e instanceof Error ? e.message : String(e);
1617
1663
  }
1664
+ function isErrno4(error) {
1665
+ return error instanceof Error && "code" in error;
1666
+ }
1618
1667
 
1619
1668
  // src/commands/create.ts
1620
1669
  var createCommand = {
@@ -1794,7 +1843,7 @@ async function packTarGz(cwd, files) {
1794
1843
  try {
1795
1844
  stat = await fs5.lstat(abs);
1796
1845
  } catch (e) {
1797
- if (isErrno3(e) && e.code === "ENOENT")
1846
+ if (isErrno5(e) && e.code === "ENOENT")
1798
1847
  continue;
1799
1848
  throw e;
1800
1849
  }
@@ -1919,13 +1968,14 @@ function findSplit(p, enc) {
1919
1968
  }
1920
1969
  return null;
1921
1970
  }
1922
- function isErrno3(e) {
1971
+ function isErrno5(e) {
1923
1972
  return e instanceof Error && typeof e.code === "string";
1924
1973
  }
1925
1974
 
1926
1975
  // src/bundle/upload.ts
1927
1976
  var ACCESS_RECONCILE_EXACT_SHA_V1 = "access-reconcile-exact-sha-v1";
1928
- async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact, baseSha, overrideStale) {
1977
+ var SOURCE_PROVENANCE_V1 = "source-provenance-v1";
1978
+ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact, baseSha, overrideStale, allowUnguarded = false) {
1929
1979
  const formData = new FormData;
1930
1980
  formData.append("manifest", manifestYaml);
1931
1981
  const buf = new ArrayBuffer(bundleTarGz.byteLength);
@@ -1948,12 +1998,14 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
1948
1998
  const headers = {
1949
1999
  authorization: `Bearer ${accessToken}`,
1950
2000
  accept: "application/json",
1951
- "x-launchpad-capabilities": ACCESS_RECONCILE_EXACT_SHA_V1
2001
+ "x-launchpad-capabilities": `${ACCESS_RECONCILE_EXACT_SHA_V1},${SOURCE_PROVENANCE_V1}`
1952
2002
  };
1953
2003
  if (baseSha)
1954
2004
  headers["x-launchpad-base-sha"] = baseSha;
1955
2005
  if (overrideStale)
1956
2006
  headers["x-launchpad-stale-override"] = overrideStale;
2007
+ if (allowUnguarded)
2008
+ headers["x-launchpad-allow-unguarded"] = "true";
1957
2009
  let res;
1958
2010
  try {
1959
2011
  res = await fetch(url, { method: "POST", headers, body: formData });
@@ -2104,6 +2156,7 @@ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2104
2156
  import { join as join4, relative as relative2, sep } from "node:path";
2105
2157
  var DEFAULT_IGNORE = [
2106
2158
  ".git",
2159
+ ".launchpad",
2107
2160
  "node_modules",
2108
2161
  "dist",
2109
2162
  "build",
@@ -2467,9 +2520,9 @@ var AccessSchema = z.object({
2467
2520
  allowed_entra_group: z.string().min(1).optional(),
2468
2521
  allowed_entra_groups: z.array(z.string().min(1)).min(1).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
2469
2522
  session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
2470
- }).strict().superRefine((access2, ctx) => {
2471
- const hasSingular = access2.allowed_entra_group !== undefined;
2472
- const hasPlural = access2.allowed_entra_groups !== undefined;
2523
+ }).strict().superRefine((access, ctx) => {
2524
+ const hasSingular = access.allowed_entra_group !== undefined;
2525
+ const hasPlural = access.allowed_entra_groups !== undefined;
2473
2526
  if (!hasSingular && !hasPlural) {
2474
2527
  ctx.addIssue({
2475
2528
  code: z.ZodIssueCode.custom,
@@ -2485,11 +2538,11 @@ var AccessSchema = z.object({
2485
2538
  });
2486
2539
  }
2487
2540
  });
2488
- function allowedEntraGroups(access2) {
2489
- if (access2.allowed_entra_groups !== undefined) {
2490
- return access2.allowed_entra_groups;
2541
+ function allowedEntraGroups(access) {
2542
+ if (access.allowed_entra_groups !== undefined) {
2543
+ return access.allowed_entra_groups;
2491
2544
  }
2492
- return [access2.allowed_entra_group];
2545
+ return [access.allowed_entra_group];
2493
2546
  }
2494
2547
  var ContainerSchema = z.object({
2495
2548
  name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
@@ -2957,6 +3010,13 @@ function checkDenyList(path7) {
2957
3010
  if (segments.includes(".claude")) {
2958
3011
  return { path: path7, rule: "claude-dir", message: `'${path7}' is under a .claude/ directory (AI-workspace state) — never shippable` };
2959
3012
  }
3013
+ if (segments.includes(".launchpad")) {
3014
+ return {
3015
+ path: path7,
3016
+ rule: "launchpad-dir",
3017
+ message: `'${path7}' is under .launchpad/ (clone-local state) - never shippable`
3018
+ };
3019
+ }
2960
3020
  if (segments.includes(".git")) {
2961
3021
  return { path: path7, rule: "git-dir", message: `'${path7}' is under .git/ — never shippable` };
2962
3022
  }
@@ -4006,7 +4066,7 @@ ${lines}`
4006
4066
 
4007
4067
  // src/bundle/orchestrate.ts
4008
4068
  async function bundleAndDeploy(args) {
4009
- const { cfg, cwd, slug, overrideStale } = args;
4069
+ const { cfg, cwd, slug, overrideStale, allowUnguarded = false } = args;
4010
4070
  const manifestPath = join7(cwd, "launchpad.yaml");
4011
4071
  let manifestYaml;
4012
4072
  try {
@@ -4052,7 +4112,7 @@ async function bundleAndDeploy(args) {
4052
4112
  }
4053
4113
  const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
4054
4114
  const baseSha = await readBaseShaMarker(cwd);
4055
- const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact, baseSha, overrideStale);
4115
+ const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact, baseSha, overrideStale, allowUnguarded);
4056
4116
  if (uploadResult.kind !== "ok") {
4057
4117
  return {
4058
4118
  kind: "upload-error",
@@ -6319,6 +6379,7 @@ function parseDeployFlags(args) {
6319
6379
  let allowStale = false;
6320
6380
  let rebaseOntoHead = false;
6321
6381
  let overrideStale = null;
6382
+ let allowUnguarded = false;
6322
6383
  let i = 0;
6323
6384
  while (i < args.length) {
6324
6385
  const a = args[i] ?? "";
@@ -6451,6 +6512,11 @@ function parseDeployFlags(args) {
6451
6512
  i += 1;
6452
6513
  continue;
6453
6514
  }
6515
+ if (a === "--allow-unguarded") {
6516
+ allowUnguarded = true;
6517
+ i += 1;
6518
+ continue;
6519
+ }
6454
6520
  if (a === "--rebase-onto-head") {
6455
6521
  rebaseOntoHead = true;
6456
6522
  i += 1;
@@ -6532,8 +6598,8 @@ function parseDeployFlags(args) {
6532
6598
  if (modeFlagsSeen > 1) {
6533
6599
  return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
6534
6600
  }
6535
- if (mode !== "content" && (allowStale || rebaseOntoHead || overrideStale !== null)) {
6536
- return "--allow-stale / --rebase-onto-head / --override-stale are only valid for a content deploy";
6601
+ if (mode !== "content" && (allowStale || rebaseOntoHead || overrideStale !== null || allowUnguarded)) {
6602
+ return "--allow-stale / --rebase-onto-head / --override-stale / --allow-unguarded are only valid for a content deploy";
6537
6603
  }
6538
6604
  if (rebaseOntoHead && overrideStale !== null) {
6539
6605
  return "--rebase-onto-head and --override-stale are mutually exclusive — re-stamp to head, or override in place, not both";
@@ -6626,7 +6692,14 @@ function parseDeployFlags(args) {
6626
6692
  return `invalid slug "${slug}" — expected ${SLUG_RE4.source}`;
6627
6693
  }
6628
6694
  return {
6629
- mode: { kind: "content", slug, allowStale, rebaseOntoHead, overrideStale },
6695
+ mode: {
6696
+ kind: "content",
6697
+ slug,
6698
+ allowStale,
6699
+ rebaseOntoHead,
6700
+ overrideStale,
6701
+ allowUnguarded
6702
+ },
6630
6703
  message,
6631
6704
  timeoutSeconds
6632
6705
  };
@@ -7599,7 +7672,12 @@ async function runDeploy(args, io) {
7599
7672
  const cwd = process.cwd();
7600
7673
  const manifestPath = path8.join(cwd, "launchpad.yaml");
7601
7674
  if (existsSync6(manifestPath)) {
7602
- const contentMode = flags.mode.kind === "content" ? flags.mode : { allowStale: false, rebaseOntoHead: false, overrideStale: null };
7675
+ const contentMode = flags.mode.kind === "content" ? flags.mode : {
7676
+ allowStale: false,
7677
+ rebaseOntoHead: false,
7678
+ overrideStale: null,
7679
+ allowUnguarded: false
7680
+ };
7603
7681
  return runModelADeploy({
7604
7682
  cwd,
7605
7683
  manifestPath,
@@ -7607,9 +7685,14 @@ async function runDeploy(args, io) {
7607
7685
  io,
7608
7686
  allowStale: contentMode.allowStale,
7609
7687
  rebaseOntoHead: contentMode.rebaseOntoHead,
7610
- overrideStale: contentMode.overrideStale
7688
+ overrideStale: contentMode.overrideStale,
7689
+ allowUnguarded: contentMode.allowUnguarded
7611
7690
  });
7612
7691
  }
7692
+ if (flags.mode.kind === "content" && flags.mode.allowUnguarded) {
7693
+ io.err("launchpad deploy: --allow-unguarded requires launchpad.yaml (Model A); legacy content deploys do not support this override.");
7694
+ return 64;
7695
+ }
7613
7696
  const parsed = parseArgs3(args);
7614
7697
  if (parsed === null) {
7615
7698
  io.err("launchpad deploy: malformed argv (run with --help)");
@@ -7723,7 +7806,7 @@ async function runDeploy(args, io) {
7723
7806
  }
7724
7807
  }
7725
7808
  function deployUsage() {
7726
- return `usage: launchpad deploy [--message <text>] [--slug <slug>]
7809
+ return `usage: launchpad deploy [--message <text>] [--slug <slug>] [--allow-unguarded]
7727
7810
  ` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
7728
7811
  ` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
7729
7812
  ` + `provisioning modes:
@@ -7851,8 +7934,9 @@ function surfaceStaleBlock(body, io) {
7851
7934
  io.err(`launchpad deploy: BLOCKED — main moved since you cloned and the overlap cannot be proven` + (body.reason === undefined ? "" : ` (${body.reason})`) + `; treating as unsafe.`);
7852
7935
  }
7853
7936
  io.err("");
7854
- io.err(" To recover (after reconciling the listed file(s) with main):");
7855
- io.err(" launchpad deploy --rebase-onto-head");
7937
+ io.err(" To recover safely:");
7938
+ io.err(" launchpad pull");
7939
+ io.err(" launchpad deploy");
7856
7940
  if (head !== null) {
7857
7941
  io.err(" Or, to deliberately overwrite them in place:");
7858
7942
  io.err(` launchpad deploy --override-stale ${head.slice(0, 8)}`);
@@ -7970,12 +8054,26 @@ async function runModelADeploy(args) {
7970
8054
  const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
7971
8055
  let result;
7972
8056
  try {
8057
+ if (args.allowUnguarded) {
8058
+ io.err("warning: --allow-unguarded disables clone staleness protection for this deploy.");
8059
+ }
7973
8060
  result = await bundleAndDeploy({
7974
8061
  cwd,
7975
8062
  cfg,
7976
8063
  slug,
7977
- overrideStale: args.overrideStale
8064
+ overrideStale: args.overrideStale,
8065
+ allowUnguarded: args.allowUnguarded
7978
8066
  });
8067
+ if (result.kind === "upload-error" && result.status === 409 && result.body.error === "main_advanced") {
8068
+ io.out("Managed main advanced during validation; recomputing and retrying once ...");
8069
+ result = await bundleAndDeploy({
8070
+ cwd,
8071
+ cfg,
8072
+ slug,
8073
+ overrideStale: args.overrideStale,
8074
+ allowUnguarded: args.allowUnguarded
8075
+ });
8076
+ }
7979
8077
  if (args.rebaseOntoHead && result.kind === "upload-error" && result.status === 409 && isStaleBlock(result.body)) {
7980
8078
  const head = result.body.head_sha;
7981
8079
  if (typeof head === "string" && isValidBaseSha(head)) {
@@ -7983,7 +8081,7 @@ async function runModelADeploy(args) {
7983
8081
  io.out(`Re-stamped base revision to current head ${head.slice(0, 12)}; retrying deploy …`);
7984
8082
  result = await bundleAndDeploy({ cwd, cfg, slug });
7985
8083
  } else {
7986
- io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block; re-clone the app instead.");
8084
+ io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block; run `launchpad pull` and retry.");
7987
8085
  return 1;
7988
8086
  }
7989
8087
  }
@@ -8034,6 +8132,12 @@ async function runModelADeploy(args) {
8034
8132
  surfaceStaleBlock(body, io);
8035
8133
  return 1;
8036
8134
  }
8135
+ if (result.status === 409 && errorCode === "untrusted_base") {
8136
+ io.err("launchpad deploy: this working tree has no trusted source baseline.");
8137
+ io.err(" Run `launchpad pull --bootstrap` to establish provenance for an unchanged legacy clone,");
8138
+ io.err(" use a fresh Launchpad clone, or pass --allow-unguarded only for an intentional non-clone deploy.");
8139
+ return 1;
8140
+ }
8037
8141
  if (result.status === 409 && errorCode === "access_drift") {
8038
8142
  return handleAccessDrift({
8039
8143
  slug,
@@ -9950,16 +10054,16 @@ function buildManifest(inputs, detected) {
9950
10054
  metadata.description = inputs.description;
9951
10055
  }
9952
10056
  const deployment = { type: inputs.type };
9953
- const access2 = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
10057
+ const access = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
9954
10058
  if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
9955
- access2.session_duration = inputs.sessionDuration;
10059
+ access.session_duration = inputs.sessionDuration;
9956
10060
  }
9957
10061
  const manifest = {
9958
10062
  apiVersion: "launchpad.m-kopa.us/v1alpha1",
9959
10063
  kind: "App",
9960
10064
  metadata,
9961
10065
  deployment,
9962
- access: access2
10066
+ access
9963
10067
  };
9964
10068
  const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
9965
10069
  if (resolvedAuth === "gateway") {
@@ -11007,10 +11111,474 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
11007
11111
  return apiJson(cfg, { path: path12 }, fetcher);
11008
11112
  }
11009
11113
 
11114
+ // src/pull/source-sync.ts
11115
+ import * as fs6 from "node:fs/promises";
11116
+ import * as path12 from "node:path";
11117
+ import { constants as fsConstants } from "node:fs";
11118
+ import { createHash as createHash3 } from "node:crypto";
11119
+ import { execFile } from "node:child_process";
11120
+ var CONFLICT_REPORT = `${MARKER_DIR}/pull-conflicts.json`;
11121
+ var CONFLICT_DIR = `${MARKER_DIR}/pull-conflicts`;
11122
+ var DELETE_NOTE = new TextEncoder().encode(`Upstream deleted this file.
11123
+ `);
11124
+ var UNSUPPORTED_NOTE = new TextEncoder().encode(`Upstream or baseline uses a Git entry type that launchpad pull cannot reconcile.
11125
+ `);
11126
+ async function runSourceSync(args) {
11127
+ await assertSafeCloneRoot(args.cwd);
11128
+ const marker = await readBaseShaMarkerState(args.cwd);
11129
+ if (marker.kind !== "valid") {
11130
+ if (args.bootstrap === true)
11131
+ return bootstrapProvenance(args);
11132
+ args.io.err(`launchpad pull: this directory has ${marker.kind} source provenance and cannot be reconciled safely.`);
11133
+ args.io.err(" Preserve this directory, then clone the app into a separate empty parent directory to recover a trusted baseline.");
11134
+ return 1;
11135
+ }
11136
+ const compare = await apiJson(args.cfg, {
11137
+ path: `/apps/${args.slug}/source/compare?base=${marker.sha}`
11138
+ });
11139
+ if (compare.baseSha !== marker.sha) {
11140
+ throw new Error("source compare returned a different base SHA");
11141
+ }
11142
+ const paths = [
11143
+ ...new Set([
11144
+ ...compare.changed,
11145
+ ...compare.deleted,
11146
+ ...compare.unsupported ?? []
11147
+ ])
11148
+ ].sort();
11149
+ if (paths.length === 0) {
11150
+ await cleanupConflictState(args.cwd);
11151
+ await writeBaseShaMarker(args.cwd, compare.headSha);
11152
+ args.io.out(`Already current at ${compare.headSha.slice(0, 12)}.`);
11153
+ return 0;
11154
+ }
11155
+ const stageRoot = path12.resolve(args.cwd, MARKER_DIR, `pull-stage-${process.pid}-${crypto.randomUUID()}`);
11156
+ const baseDir = path12.join(stageRoot, "base");
11157
+ const headDir = path12.join(stageRoot, "head");
11158
+ await fs6.mkdir(baseDir, { recursive: true });
11159
+ await fs6.mkdir(headDir, { recursive: true });
11160
+ try {
11161
+ await Promise.all([
11162
+ fetchSnapshot(args.cfg, args.slug, compare.baseSha, baseDir),
11163
+ fetchSnapshot(args.cfg, args.slug, compare.headSha, headDir)
11164
+ ]);
11165
+ return await reconcile({ ...args, compare, paths, baseDir, headDir });
11166
+ } finally {
11167
+ await fs6.rm(stageRoot, { recursive: true, force: true }).catch(() => {
11168
+ return;
11169
+ });
11170
+ }
11171
+ }
11172
+ async function fetchSnapshot(cfg, slug, sha, destination) {
11173
+ await fetchSnapshotRef(cfg, slug, sha, destination, sha);
11174
+ }
11175
+ async function fetchSnapshotRef(cfg, slug, ref, destination, expectedSha) {
11176
+ const response = await apiRaw(cfg, {
11177
+ path: `/apps/${slug}/source-bundle?at=${ref}`
11178
+ });
11179
+ const pinnedSha = response.headers.get(BASE_SHA_HEADER);
11180
+ if (pinnedSha === null || !/^[0-9a-f]{40}$/.test(pinnedSha) || expectedSha !== undefined && pinnedSha !== expectedSha || response.body === null) {
11181
+ throw new Error(`source snapshot provenance mismatch at ${ref}`);
11182
+ }
11183
+ await extractToDir(response.body, destination, {
11184
+ stripComponents: 1,
11185
+ allowNonEmpty: true
11186
+ });
11187
+ return pinnedSha;
11188
+ }
11189
+ async function bootstrapProvenance(args) {
11190
+ let status;
11191
+ let tracked;
11192
+ try {
11193
+ status = await gitOutput(args.cwd, [
11194
+ "status",
11195
+ "--porcelain=v1",
11196
+ "-z",
11197
+ "--untracked-files=all"
11198
+ ]);
11199
+ tracked = splitNull(await gitOutput(args.cwd, ["ls-files", "-z"]));
11200
+ } catch {
11201
+ args.io.err("launchpad pull: --bootstrap requires a legacy Launchpad clone with its synthetic Git baseline.");
11202
+ return 1;
11203
+ }
11204
+ if (status.length > 0) {
11205
+ args.io.err("launchpad pull: --bootstrap refused because the legacy clone has local changes or untracked files.");
11206
+ args.io.err(" Preserve this directory and obtain a separate current clone for comparison.");
11207
+ return 1;
11208
+ }
11209
+ const stageRoot = path12.resolve(args.cwd, MARKER_DIR, `pull-bootstrap-${process.pid}-${crypto.randomUUID()}`);
11210
+ await fs6.mkdir(stageRoot, { recursive: true });
11211
+ try {
11212
+ const headSha = await fetchSnapshotRef(args.cfg, args.slug, "main", stageRoot);
11213
+ const headPaths = await listFiles(stageRoot);
11214
+ if (!await treesEqual(args.cwd, tracked, stageRoot, headPaths)) {
11215
+ args.io.err("launchpad pull: --bootstrap could not prove this legacy baseline matches current managed main.");
11216
+ args.io.err(" The directory was not changed. Preserve it and obtain a separate current clone.");
11217
+ return 1;
11218
+ }
11219
+ await writeBaseShaMarker(args.cwd, headSha);
11220
+ args.io.out(`Trusted source provenance established at ${headSha.slice(0, 12)}.`);
11221
+ return 0;
11222
+ } finally {
11223
+ await fs6.rm(stageRoot, { recursive: true, force: true }).catch(() => {
11224
+ return;
11225
+ });
11226
+ }
11227
+ }
11228
+ async function treesEqual(cwd, tracked, headDir, headPaths) {
11229
+ if (tracked.length !== headPaths.length)
11230
+ return false;
11231
+ const expected = new Set(headPaths);
11232
+ for (const relative5 of tracked) {
11233
+ if (!expected.has(relative5))
11234
+ return false;
11235
+ if (!equal(await readEntry(path12.resolve(cwd, relative5)), await readEntry(path12.resolve(headDir, relative5)))) {
11236
+ return false;
11237
+ }
11238
+ }
11239
+ return true;
11240
+ }
11241
+ async function listFiles(root, relative5 = "") {
11242
+ const files = [];
11243
+ const entries = await fs6.readdir(path12.resolve(root, relative5), {
11244
+ withFileTypes: true
11245
+ });
11246
+ for (const entry of entries) {
11247
+ const child = relative5.length === 0 ? entry.name : `${relative5}/${entry.name}`;
11248
+ if (entry.isDirectory())
11249
+ files.push(...await listFiles(root, child));
11250
+ else if (entry.isFile())
11251
+ files.push(child);
11252
+ }
11253
+ return files.sort();
11254
+ }
11255
+ function gitOutput(cwd, args) {
11256
+ return new Promise((resolve12, reject) => {
11257
+ execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
11258
+ if (error !== null)
11259
+ reject(error);
11260
+ else
11261
+ resolve12(stdout);
11262
+ });
11263
+ });
11264
+ }
11265
+ function splitNull(value) {
11266
+ return value.split("\x00").filter((entry) => entry.length > 0).sort();
11267
+ }
11268
+ async function reconcile(args) {
11269
+ const removals = await cleanAncestorRemovals(args);
11270
+ const planned = await Promise.all(args.paths.map((relative5) => planPath(args, relative5, removals)));
11271
+ const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
11272
+ if (conflicts.length > 0) {
11273
+ const report = {
11274
+ version: 1,
11275
+ slug: args.slug,
11276
+ baseSha: args.compare.baseSha,
11277
+ headSha: args.compare.headSha,
11278
+ conflicts
11279
+ };
11280
+ await writeJsonAtomic(args.cwd, CONFLICT_REPORT, report);
11281
+ await resetConflictArtifacts(args.cwd);
11282
+ for (const entry of planned) {
11283
+ if (entry.conflict !== undefined) {
11284
+ await writeConflictArtifact(args.cwd, entry);
11285
+ }
11286
+ }
11287
+ }
11288
+ let applied = 0;
11289
+ for (const entry of planned) {
11290
+ if (!entry.apply)
11291
+ continue;
11292
+ await applyUpstream(args.cwd, entry);
11293
+ applied += 1;
11294
+ }
11295
+ if (conflicts.length > 0) {
11296
+ args.io.err(`launchpad pull: ${conflicts.length} conflict(s); ${applied} clean upstream change(s) applied.`);
11297
+ for (const conflict of conflicts.slice(0, 20)) {
11298
+ args.io.err(` - ${conflict.path}: ${conflict.reason}`);
11299
+ args.io.err(` upstream copy: ${conflict.artifact}`);
11300
+ }
11301
+ if (conflicts.length > 20) {
11302
+ args.io.err(` ... and ${conflicts.length - 20} more in ${CONFLICT_REPORT}`);
11303
+ }
11304
+ args.io.err("Resolve each original path using its upstream artifact, then run `launchpad pull` again.");
11305
+ return 1;
11306
+ }
11307
+ await cleanupConflictState(args.cwd);
11308
+ await writeBaseShaMarker(args.cwd, args.compare.headSha);
11309
+ args.io.out(`Updated ${applied} path(s) to managed main ${args.compare.headSha.slice(0, 12)}; local-only work was preserved.`);
11310
+ return 0;
11311
+ }
11312
+ async function cleanAncestorRemovals(args) {
11313
+ const removals = new Set;
11314
+ for (const relative5 of args.compare.deleted) {
11315
+ assertSafePath(relative5);
11316
+ const checked = await checkParents(args.cwd, relative5, removals);
11317
+ if (checked.kind === "blocked")
11318
+ continue;
11319
+ const base = await readEntry(path12.resolve(args.baseDir, relative5));
11320
+ const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11321
+ if (local.kind !== "missing" && cleanLocal(base, local))
11322
+ removals.add(relative5);
11323
+ }
11324
+ return removals;
11325
+ }
11326
+ async function planPath(args, relative5, removals) {
11327
+ assertSafePath(relative5);
11328
+ const checked = await checkParents(args.cwd, relative5, removals);
11329
+ const base = await readEntry(path12.resolve(args.baseDir, relative5));
11330
+ const upstreamDeleted = args.compare.deleted.includes(relative5);
11331
+ const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative5));
11332
+ const unsupported = (args.compare.unsupported ?? []).includes(relative5);
11333
+ if (checked.kind === "blocked") {
11334
+ return conflictPlan(relative5, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
11335
+ }
11336
+ const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
11337
+ if (unsupported) {
11338
+ return conflictPlan(relative5, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
11339
+ }
11340
+ if (alreadyResolved(local, head, upstreamDeleted)) {
11341
+ return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: false };
11342
+ }
11343
+ if (unsupportedHead(head, upstreamDeleted)) {
11344
+ return conflictPlan(relative5, checked.target, local, head, true, "unsupported upstream file type");
11345
+ }
11346
+ if (cleanLocal(base, local)) {
11347
+ return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: true };
11348
+ }
11349
+ return conflictPlan(relative5, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
11350
+ }
11351
+ function conflictPlan(relative5, target, local, head, upstreamDeleted, reason) {
11352
+ const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
11353
+ const digest = createHash3("sha256").update(relative5).digest("hex");
11354
+ return {
11355
+ relative: relative5,
11356
+ target,
11357
+ local,
11358
+ head,
11359
+ upstreamDeleted,
11360
+ apply: false,
11361
+ conflict: {
11362
+ path: relative5,
11363
+ reason,
11364
+ artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
11365
+ upstreamDeleted
11366
+ }
11367
+ };
11368
+ }
11369
+ function alreadyResolved(local, head, upstreamDeleted) {
11370
+ return equal(local, head) || upstreamDeleted && local.kind === "missing";
11371
+ }
11372
+ function unsupportedHead(head, upstreamDeleted) {
11373
+ return head.kind === "other" || !upstreamDeleted && head.kind === "missing";
11374
+ }
11375
+ function cleanLocal(base, local) {
11376
+ return equal(local, base) || base.kind === "missing" && local.kind === "missing";
11377
+ }
11378
+ async function applyUpstream(cwd, entry) {
11379
+ const checked = await checkParents(cwd, entry.relative, new Set);
11380
+ if (checked.kind === "blocked")
11381
+ throw new Error(checked.reason);
11382
+ if (entry.upstreamDeleted) {
11383
+ if (entry.local.kind !== "missing")
11384
+ await fs6.unlink(checked.target);
11385
+ return;
11386
+ }
11387
+ if (entry.head.kind === "file") {
11388
+ await writeAtomic(cwd, entry.relative, entry.head.bytes, entry.head.mode);
11389
+ }
11390
+ }
11391
+ function conflictReason(base, local, upstreamDeleted) {
11392
+ if (local.kind === "other")
11393
+ return "local path is not a regular file";
11394
+ if (base.kind === "missing")
11395
+ return "local untracked file collides with an upstream addition";
11396
+ if (local.kind === "missing")
11397
+ return "local deletion conflicts with an upstream change";
11398
+ if (upstreamDeleted)
11399
+ return "locally modified file was deleted upstream";
11400
+ return "local and upstream changes differ";
11401
+ }
11402
+ async function writeConflictArtifact(cwd, entry) {
11403
+ if (entry.conflict === undefined)
11404
+ return;
11405
+ const bytes = entry.head.kind === "file" ? entry.head.bytes : entry.upstreamDeleted ? DELETE_NOTE : UNSUPPORTED_NOTE;
11406
+ await writeAtomic(cwd, entry.conflict.artifact, bytes, entry.head.kind === "file" ? entry.head.mode : 420, true);
11407
+ }
11408
+ async function readEntry(file) {
11409
+ const initial = await lstatIfPresent(file);
11410
+ if (initial === null)
11411
+ return { kind: "missing" };
11412
+ if (!initial.isFile() || initial.isSymbolicLink())
11413
+ return { kind: "other" };
11414
+ const opened = await openWithoutFollowing(file);
11415
+ if (opened.kind !== "open")
11416
+ return opened;
11417
+ try {
11418
+ const stat = await opened.handle.stat();
11419
+ if (!stat.isFile())
11420
+ return { kind: "other" };
11421
+ return {
11422
+ kind: "file",
11423
+ bytes: new Uint8Array(await opened.handle.readFile()),
11424
+ mode: stat.mode & 511
11425
+ };
11426
+ } finally {
11427
+ await opened.handle.close();
11428
+ }
11429
+ }
11430
+ async function lstatIfPresent(file) {
11431
+ try {
11432
+ return await fs6.lstat(file);
11433
+ } catch (error) {
11434
+ if (isErrno6(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
11435
+ return null;
11436
+ }
11437
+ throw error;
11438
+ }
11439
+ }
11440
+ async function openWithoutFollowing(file) {
11441
+ try {
11442
+ return {
11443
+ kind: "open",
11444
+ handle: await fs6.open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW)
11445
+ };
11446
+ } catch (error) {
11447
+ if (isErrno6(error) && error.code === "ENOENT")
11448
+ return { kind: "missing" };
11449
+ if (isErrno6(error) && error.code === "ELOOP")
11450
+ return { kind: "other" };
11451
+ throw error;
11452
+ }
11453
+ }
11454
+ function equal(left, right) {
11455
+ if (left.kind !== right.kind)
11456
+ return false;
11457
+ if (left.kind !== "file" || right.kind !== "file")
11458
+ return true;
11459
+ if (left.mode !== right.mode || left.bytes.length !== right.bytes.length)
11460
+ return false;
11461
+ return left.bytes.every((byte, index) => byte === right.bytes[index]);
11462
+ }
11463
+ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
11464
+ const checked = await checkParents(cwd, relative5, new Set, allowMetadata);
11465
+ if (checked.kind === "blocked")
11466
+ throw new Error(checked.reason);
11467
+ const file = checked.target;
11468
+ await fs6.mkdir(path12.dirname(file), { recursive: true });
11469
+ const rechecked = await checkParents(cwd, relative5, new Set, allowMetadata);
11470
+ if (rechecked.kind === "blocked")
11471
+ throw new Error(rechecked.reason);
11472
+ const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
11473
+ try {
11474
+ await fs6.writeFile(temporary, bytes, { mode });
11475
+ await fs6.chmod(temporary, mode);
11476
+ await fs6.rename(temporary, file);
11477
+ } catch (error) {
11478
+ await fs6.rm(temporary, { force: true }).catch(() => {
11479
+ return;
11480
+ });
11481
+ throw error;
11482
+ }
11483
+ }
11484
+ async function writeJsonAtomic(cwd, relative5, value) {
11485
+ await writeAtomic(cwd, relative5, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
11486
+ `), 384, true);
11487
+ }
11488
+ async function resetConflictArtifacts(cwd) {
11489
+ const checked = await checkParents(cwd, CONFLICT_DIR, new Set, true);
11490
+ if (checked.kind === "blocked")
11491
+ throw new Error(checked.reason);
11492
+ await fs6.rm(checked.target, { recursive: true, force: true });
11493
+ }
11494
+ async function cleanupConflictState(cwd) {
11495
+ await resetConflictArtifacts(cwd);
11496
+ const report = await checkParents(cwd, CONFLICT_REPORT, new Set, true);
11497
+ if (report.kind === "blocked")
11498
+ throw new Error(report.reason);
11499
+ await fs6.rm(report.target, { force: true });
11500
+ }
11501
+ function assertSafePath(relative5) {
11502
+ if (!isSafeSourcePath(relative5)) {
11503
+ throw new Error(`unsafe source path returned by bot: ${relative5}`);
11504
+ }
11505
+ }
11506
+ function isSafeSourcePath(relative5) {
11507
+ return isSafeRelativePath(relative5) && !relative5.startsWith(`${MARKER_DIR}/`);
11508
+ }
11509
+ function isSafeRelativePath(relative5) {
11510
+ const normalized = path12.posix.normalize(relative5);
11511
+ return relative5.length > 0 && !relative5.includes("\\") && !path12.isAbsolute(relative5) && normalized === relative5 && normalized !== ".." && !normalized.startsWith("../");
11512
+ }
11513
+ function resolveInsideClone(cwd, relative5) {
11514
+ const root = path12.resolve(cwd);
11515
+ const target = path12.resolve(root, relative5);
11516
+ const fromRoot = path12.relative(root, target);
11517
+ if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
11518
+ throw new Error(`unsafe source path returned by bot: ${relative5}`);
11519
+ }
11520
+ return target;
11521
+ }
11522
+ async function checkParents(cwd, relative5, projectedRemovals, allowMetadata = false) {
11523
+ if (allowMetadata) {
11524
+ if (!isSafeRelativePath(relative5)) {
11525
+ throw new Error(`unsafe metadata path: ${relative5}`);
11526
+ }
11527
+ } else {
11528
+ assertSafePath(relative5);
11529
+ }
11530
+ const target = resolveInsideClone(cwd, relative5);
11531
+ const components = relative5.split("/");
11532
+ let current = path12.resolve(cwd);
11533
+ for (let index = 0;index < components.length - 1; index += 1) {
11534
+ current = path12.join(current, components[index]);
11535
+ const ancestor = components.slice(0, index + 1).join("/");
11536
+ const stat = await lstatIfPresent(current);
11537
+ if (stat === null)
11538
+ return { kind: "safe", target, projectedMissing: true };
11539
+ if (projectedRemovals.has(ancestor)) {
11540
+ return { kind: "safe", target, projectedMissing: true };
11541
+ }
11542
+ if (stat.isSymbolicLink()) {
11543
+ return {
11544
+ kind: "blocked",
11545
+ target,
11546
+ reason: `local parent is a symbolic link: ${ancestor}`
11547
+ };
11548
+ }
11549
+ if (!stat.isDirectory()) {
11550
+ return {
11551
+ kind: "blocked",
11552
+ target,
11553
+ reason: `local parent is not a directory: ${ancestor}`
11554
+ };
11555
+ }
11556
+ }
11557
+ return { kind: "safe", target, projectedMissing: false };
11558
+ }
11559
+ async function assertSafeCloneRoot(cwd) {
11560
+ const root = await fs6.lstat(path12.resolve(cwd));
11561
+ if (!root.isDirectory() || root.isSymbolicLink()) {
11562
+ throw new Error("launchpad pull requires a real clone directory");
11563
+ }
11564
+ try {
11565
+ const metadata = await fs6.lstat(path12.resolve(cwd, MARKER_DIR));
11566
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
11567
+ throw new Error("launchpad pull refuses a non-directory or symlinked .launchpad path");
11568
+ }
11569
+ } catch (error) {
11570
+ if (!isErrno6(error) || error.code !== "ENOENT")
11571
+ throw error;
11572
+ }
11573
+ }
11574
+ function isErrno6(error) {
11575
+ return error instanceof Error && "code" in error;
11576
+ }
11577
+
11010
11578
  // src/commands/pull.ts
11011
11579
  var pullCommand = {
11012
11580
  name: "pull",
11013
- summary: "read the deployed launchpad.yaml for an app",
11581
+ summary: "sync upstream source into a clone without overwriting local work",
11014
11582
  run: runPull
11015
11583
  };
11016
11584
  var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
@@ -11023,6 +11591,15 @@ async function runPull(args, io) {
11023
11591
  }
11024
11592
  try {
11025
11593
  const cfg = loadConfig();
11594
+ if (!parsed.manifest) {
11595
+ return await runSourceSync({
11596
+ cfg,
11597
+ cwd: process.cwd(),
11598
+ slug: parsed.slug,
11599
+ io,
11600
+ bootstrap: parsed.bootstrap
11601
+ });
11602
+ }
11026
11603
  if (parsed.status) {
11027
11604
  const noStatusMsg = `launchpad pull: no status block deployed for "${parsed.slug}" yet — ` + `the bot writes it after the first apply.`;
11028
11605
  let res;
@@ -11105,6 +11682,8 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
11105
11682
  let slug = null;
11106
11683
  let out = null;
11107
11684
  let status = false;
11685
+ let manifest = false;
11686
+ let bootstrap = false;
11108
11687
  let i = 0;
11109
11688
  while (i < args.length) {
11110
11689
  const a = args[i] ?? "";
@@ -11113,6 +11692,16 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
11113
11692
  i += 1;
11114
11693
  continue;
11115
11694
  }
11695
+ if (a === "--manifest") {
11696
+ manifest = true;
11697
+ i += 1;
11698
+ continue;
11699
+ }
11700
+ if (a === "--bootstrap") {
11701
+ bootstrap = true;
11702
+ i += 1;
11703
+ continue;
11704
+ }
11116
11705
  if (a === "--slug") {
11117
11706
  const v = args[i + 1];
11118
11707
  if (v === undefined)
@@ -11151,23 +11740,38 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
11151
11740
  if (!SLUG_RE11.test(slug)) {
11152
11741
  return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
11153
11742
  }
11154
- return { slug, out, status };
11743
+ const modeError = validateModeFlags({ manifest, status, out, bootstrap });
11744
+ if (modeError !== null)
11745
+ return modeError;
11746
+ return { slug, out, status, manifest, bootstrap };
11747
+ }
11748
+ function validateModeFlags(args) {
11749
+ if (!args.manifest && args.status)
11750
+ return "--status requires --manifest";
11751
+ if (!args.manifest && args.out !== null)
11752
+ return "--out requires --manifest";
11753
+ if (args.manifest && args.bootstrap) {
11754
+ return "--bootstrap cannot be combined with --manifest";
11755
+ }
11756
+ return null;
11155
11757
  }
11156
11758
  function printUsage5(io) {
11157
11759
  io.err([
11158
- "usage: launchpad pull [<slug>] [--slug <slug>] [--out <path>] [--status]",
11760
+ "usage: launchpad pull [<slug>] [--slug <slug>] [--bootstrap]",
11761
+ " launchpad pull --manifest [<slug>] [--out <path>] [--status]",
11159
11762
  "",
11160
- " Reads the deployed launchpad.yaml for an app via the bot.",
11161
- " No local platform-repo or terraform required.",
11763
+ " By default, safely syncs managed source into the current clone.",
11764
+ " --manifest keeps the previous deployed-manifest read behavior.",
11162
11765
  "",
11163
11766
  " With no slug, it is inferred from the local launchpad.yaml's",
11164
11767
  " declared slug first, then from a launchpad-app-<slug>/",
11165
11768
  " directory name. Explicit --slug or positional override.",
11166
11769
  "",
11167
- " --status read the role-redacted status block (what your",
11770
+ " --status with --manifest, read the role-redacted status block",
11771
+ " (what your",
11168
11772
  " role may see) instead of the spec manifest.",
11169
11773
  "",
11170
- " Without --out, the output is written to stdout."
11774
+ " In manifest mode, omitting --out writes YAML to stdout."
11171
11775
  ].join(`
11172
11776
  `));
11173
11777
  }
@@ -11436,7 +12040,7 @@ var watchCommand = {
11436
12040
  };
11437
12041
 
11438
12042
  // src/commands/review.ts
11439
- import * as path12 from "node:path";
12043
+ import * as path13 from "node:path";
11440
12044
  var reviewCommand = {
11441
12045
  name: "review",
11442
12046
  summary: "show the review state for a PR (slug-scoped)",
@@ -11456,7 +12060,7 @@ async function runReview(args, io) {
11456
12060
  } else {
11457
12061
  const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
11458
12062
  if (inferred === null) {
11459
- io.err(`launchpad review: could not infer slug from cwd (${path12.basename(process.cwd())});
12063
+ io.err(`launchpad review: could not infer slug from cwd (${path13.basename(process.cwd())});
11460
12064
  ` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
11461
12065
  return 64;
11462
12066
  }
@@ -11713,16 +12317,16 @@ async function runRollback(opts, io, deps = {}) {
11713
12317
  yes: true
11714
12318
  }, io, applyDeps);
11715
12319
  }
11716
- function readCurrentManifest(path13) {
11717
- if (!existsSync10(path13))
12320
+ function readCurrentManifest(path14) {
12321
+ if (!existsSync10(path14))
11718
12322
  return null;
11719
12323
  let raw;
11720
12324
  try {
11721
- raw = readFileSync15(path13, "utf8");
12325
+ raw = readFileSync15(path14, "utf8");
11722
12326
  } catch {
11723
12327
  return null;
11724
12328
  }
11725
- const parsed = parseManifest2(raw, path13);
12329
+ const parsed = parseManifest2(raw, path14);
11726
12330
  if (parsed.kind !== "ok")
11727
12331
  return null;
11728
12332
  return summarise(parsed.manifest);
@@ -12272,23 +12876,23 @@ var CELL_LABEL2 = {
12272
12876
  not_deployed: "NOT_DEPLOYED"
12273
12877
  };
12274
12878
  function loadSet(fleetFile, setName, io) {
12275
- const path13 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
12276
- if (!existsSync12(path13)) {
12277
- io.err(`✗ ${path13}`);
12879
+ const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
12880
+ if (!existsSync12(path14)) {
12881
+ io.err(`✗ ${path14}`);
12278
12882
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
12279
12883
  return 2;
12280
12884
  }
12281
12885
  let obj;
12282
12886
  try {
12283
- obj = parseYaml8(readFileSync17(path13, "utf8"));
12887
+ obj = parseYaml8(readFileSync17(path14, "utf8"));
12284
12888
  } catch (e) {
12285
- io.err(`✗ ${path13}`);
12889
+ io.err(`✗ ${path14}`);
12286
12890
  io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
12287
12891
  return 1;
12288
12892
  }
12289
12893
  const parsed = parseFleetSecretSets(obj);
12290
12894
  if (!parsed.ok) {
12291
- io.err(`✗ ${path13}`);
12895
+ io.err(`✗ ${path14}`);
12292
12896
  io.err(` ${parsed.issues.length} schema issue(s):`);
12293
12897
  for (const i of parsed.issues)
12294
12898
  io.err(` ${i.path}: ${i.message}`);
@@ -12297,7 +12901,7 @@ function loadSet(fleetFile, setName, io) {
12297
12901
  const set = parsed.manifest.secretSets.find((s) => s.name === setName);
12298
12902
  if (set === undefined) {
12299
12903
  const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
12300
- io.err(`✗ secret-set "${setName}" not found in ${path13}`);
12904
+ io.err(`✗ secret-set "${setName}" not found in ${path14}`);
12301
12905
  io.err(` declared sets: ${names}`);
12302
12906
  return 1;
12303
12907
  }
@@ -12517,7 +13121,7 @@ function setPushExit(e) {
12517
13121
 
12518
13122
  // src/commands/secrets-template.ts
12519
13123
  import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "node:fs";
12520
- import { resolve as resolve11 } from "node:path";
13124
+ import { resolve as resolve12 } from "node:path";
12521
13125
  async function runSecretsTemplate(args, io) {
12522
13126
  const flags = parseFlags4(args);
12523
13127
  if (flags.kind === "usage-error") {
@@ -12525,7 +13129,7 @@ async function runSecretsTemplate(args, io) {
12525
13129
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
12526
13130
  return 64;
12527
13131
  }
12528
- const manifestPath = resolve11(process.cwd(), flags.file ?? "launchpad.yaml");
13132
+ const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
12529
13133
  const result = loadManifest(manifestPath);
12530
13134
  const renderResult = renderManifest(result, io);
12531
13135
  if (renderResult.kind !== "ok") {
@@ -12539,7 +13143,7 @@ async function runSecretsTemplate(args, io) {
12539
13143
  }
12540
13144
  return 0;
12541
13145
  }
12542
- const outPath = resolve11(process.cwd(), flags.out);
13146
+ const outPath = resolve12(process.cwd(), flags.out);
12543
13147
  if (existsSync13(outPath) && !flags.force) {
12544
13148
  io.err(`launchpad secrets template: ${outPath} already exists`);
12545
13149
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
@@ -12836,16 +13440,16 @@ function printHelp3(io) {
12836
13440
 
12837
13441
  // src/commands/skills.ts
12838
13442
  import { fileURLToPath } from "node:url";
12839
- import { dirname as dirname8, join as join14, resolve as resolve12 } from "node:path";
12840
- import { promises as fs6, existsSync as existsSync14 } from "node:fs";
13443
+ import { dirname as dirname9, join as join15, resolve as resolve13 } from "node:path";
13444
+ import { promises as fs7, existsSync as existsSync14 } from "node:fs";
12841
13445
 
12842
13446
  // src/skills-bundle.ts
12843
13447
  import { homedir as homedir2 } from "node:os";
12844
- import { join as join13 } from "node:path";
13448
+ import { join as join14 } from "node:path";
12845
13449
  import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
12846
13450
  var SKILL_PREFIX = "launchpad-";
12847
13451
  function skillsTargetDir() {
12848
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join13(homedir2(), ".claude", "skills");
13452
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
12849
13453
  }
12850
13454
  function readInstalledSkills() {
12851
13455
  try {
@@ -12855,15 +13459,15 @@ function readInstalledSkills() {
12855
13459
  return { present: false, version: null };
12856
13460
  return {
12857
13461
  present: true,
12858
- version: readSkillVersion(join13(dir, bundle.name, "SKILL.md"))
13462
+ version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
12859
13463
  };
12860
13464
  } catch {
12861
13465
  return { present: false, version: null };
12862
13466
  }
12863
13467
  }
12864
- function readSkillVersion(path13) {
13468
+ function readSkillVersion(path14) {
12865
13469
  try {
12866
- const text = readFileSync18(path13, "utf8");
13470
+ const text = readFileSync18(path14, "utf8");
12867
13471
  const fenceEnd = text.indexOf(`
12868
13472
  ---`, 4);
12869
13473
  const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
@@ -12940,13 +13544,13 @@ function resolveInstallEnv() {
12940
13544
  return { bundleDir, userSkillsDir };
12941
13545
  }
12942
13546
  function defaultBundleDir() {
12943
- const here = dirname8(fileURLToPath(import.meta.url));
13547
+ const here = dirname9(fileURLToPath(import.meta.url));
12944
13548
  const candidates = [
12945
- resolve12(here, "..", "skills"),
12946
- resolve12(here, "..", "..", "skills")
13549
+ resolve13(here, "..", "skills"),
13550
+ resolve13(here, "..", "..", "skills")
12947
13551
  ];
12948
13552
  for (const c of candidates) {
12949
- if (existsSync14(join14(c, "launchpad-onboard", "SKILL.md"))) {
13553
+ if (existsSync14(join15(c, "launchpad-onboard", "SKILL.md"))) {
12950
13554
  return c;
12951
13555
  }
12952
13556
  }
@@ -12962,19 +13566,19 @@ async function doInstall(io) {
12962
13566
  return 1;
12963
13567
  }
12964
13568
  if (!await isDir(env.userSkillsDir)) {
12965
- await fs6.mkdir(env.userSkillsDir, { recursive: true });
13569
+ await fs7.mkdir(env.userSkillsDir, { recursive: true });
12966
13570
  io.out(`Created ${env.userSkillsDir}.`);
12967
13571
  }
12968
13572
  let installed = 0;
12969
13573
  for (const skill of BUNDLED_SKILLS) {
12970
- const src = join14(env.bundleDir, skill);
13574
+ const src = join15(env.bundleDir, skill);
12971
13575
  if (!await isDir(src)) {
12972
13576
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
12973
13577
  return 1;
12974
13578
  }
12975
- const dest = join14(env.userSkillsDir, skill);
12976
- await fs6.rm(dest, { recursive: true, force: true });
12977
- await fs6.cp(src, dest, { recursive: true });
13579
+ const dest = join15(env.userSkillsDir, skill);
13580
+ await fs7.rm(dest, { recursive: true, force: true });
13581
+ await fs7.cp(src, dest, { recursive: true });
12978
13582
  installed++;
12979
13583
  io.out(`✓ ${skill} → ${dest}`);
12980
13584
  }
@@ -12990,15 +13594,15 @@ async function doUninstall(io) {
12990
13594
  io.out(`launchpad skills uninstall: ${env.userSkillsDir} does not exist — nothing to do.`);
12991
13595
  return 0;
12992
13596
  }
12993
- const entries = await fs6.readdir(env.userSkillsDir, { withFileTypes: true });
13597
+ const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
12994
13598
  let removed = 0;
12995
13599
  for (const entry of entries) {
12996
13600
  if (!entry.isDirectory())
12997
13601
  continue;
12998
13602
  if (!isBundleManaged(entry.name))
12999
13603
  continue;
13000
- const target = join14(env.userSkillsDir, entry.name);
13001
- await fs6.rm(target, { recursive: true, force: true });
13604
+ const target = join15(env.userSkillsDir, entry.name);
13605
+ await fs7.rm(target, { recursive: true, force: true });
13002
13606
  removed++;
13003
13607
  io.out(`✗ removed ${target}`);
13004
13608
  }
@@ -13012,7 +13616,7 @@ async function doList(io) {
13012
13616
  io.out("(none — ~/.claude/skills/ does not exist)");
13013
13617
  return 0;
13014
13618
  }
13015
- const entries = await fs6.readdir(env.userSkillsDir, { withFileTypes: true });
13619
+ const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
13016
13620
  const managedDirs = entries.filter((e) => e.isDirectory() && isBundleManaged(e.name)).map((e) => e.name).sort();
13017
13621
  if (managedDirs.length === 0) {
13018
13622
  io.out("(none — run `launchpad skills install`)");
@@ -13020,16 +13624,16 @@ async function doList(io) {
13020
13624
  }
13021
13625
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
13022
13626
  for (const name of managedDirs) {
13023
- const skillFile = join14(env.userSkillsDir, name, "SKILL.md");
13627
+ const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
13024
13628
  const version = await readVersion(skillFile);
13025
13629
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
13026
13630
  }
13027
13631
  return 0;
13028
13632
  }
13029
- async function readVersion(path13) {
13633
+ async function readVersion(path14) {
13030
13634
  let text;
13031
13635
  try {
13032
- text = await fs6.readFile(path13, "utf8");
13636
+ text = await fs7.readFile(path14, "utf8");
13033
13637
  } catch {
13034
13638
  return null;
13035
13639
  }
@@ -13039,9 +13643,9 @@ async function readVersion(path13) {
13039
13643
  const m = /^version:\s*(.+?)\s*$/m.exec(front);
13040
13644
  return m === null ? null : m[1] ?? null;
13041
13645
  }
13042
- async function isDir(path13) {
13646
+ async function isDir(path14) {
13043
13647
  try {
13044
- const stat = await fs6.stat(path13);
13648
+ const stat = await fs7.stat(path14);
13045
13649
  return stat.isDirectory();
13046
13650
  } catch {
13047
13651
  return false;
@@ -13052,16 +13656,16 @@ function describe29(e) {
13052
13656
  }
13053
13657
 
13054
13658
  // src/commands/update.ts
13055
- import { execFile, spawn as spawn5 } from "node:child_process";
13659
+ import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
13056
13660
  import { promisify } from "node:util";
13057
13661
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13058
- import { dirname as dirname9, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as join15 } from "node:path";
13662
+ import { dirname as dirname10, resolve as resolve14, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
13059
13663
  import { homedir as homedir3, tmpdir } from "node:os";
13060
13664
  import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
13061
13665
 
13062
13666
  // src/commands/channel-auth.ts
13063
13667
  import { createServer as createServer2 } from "node:http";
13064
- import { createHash as createHash3, randomBytes as randomBytes5 } from "node:crypto";
13668
+ import { createHash as createHash4, randomBytes as randomBytes5 } from "node:crypto";
13065
13669
  var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
13066
13670
  var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
13067
13671
  var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
@@ -13072,7 +13676,7 @@ function base64url(b) {
13072
13676
  }
13073
13677
  function pkcePair() {
13074
13678
  const verifier = base64url(randomBytes5(32));
13075
- const challenge = base64url(createHash3("sha256").update(verifier).digest());
13679
+ const challenge = base64url(createHash4("sha256").update(verifier).digest());
13076
13680
  return { verifier, challenge };
13077
13681
  }
13078
13682
  async function startLoopback(state, timeoutMs) {
@@ -13100,9 +13704,9 @@ async function startLoopback(state, timeoutMs) {
13100
13704
  resolveCode(code);
13101
13705
  }
13102
13706
  });
13103
- const bound = await new Promise((resolve13) => {
13104
- server.once("error", () => resolve13(false));
13105
- server.listen(0, "127.0.0.1", () => resolve13(true));
13707
+ const bound = await new Promise((resolve14) => {
13708
+ server.once("error", () => resolve14(false));
13709
+ server.listen(0, "127.0.0.1", () => resolve14(true));
13106
13710
  });
13107
13711
  if (!bound)
13108
13712
  return null;
@@ -13173,12 +13777,12 @@ async function runChannelLoopbackUpdate(deps) {
13173
13777
  }
13174
13778
 
13175
13779
  // src/commands/update.ts
13176
- var execFileAsync = promisify(execFile);
13780
+ var execFileAsync = promisify(execFile2);
13177
13781
  var PKG = "@m-kopa/launchpad-cli";
13178
13782
  var REGISTRY = "https://registry.npmjs.org";
13179
13783
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
13180
13784
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
13181
- var CHANNEL_MARKER = join15(homedir3(), ".launchpad", "channel");
13785
+ var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
13182
13786
  var EXIT_UPDATE_AVAILABLE = 10;
13183
13787
  var UPGRADE_ARGS = {
13184
13788
  npm: ["install", "-g", `${PKG}@latest`],
@@ -13307,8 +13911,8 @@ async function openSystemBrowser(url) {
13307
13911
  await execFileAsync(opener, [url]);
13308
13912
  }
13309
13913
  async function runInstallerScript(script) {
13310
- const dir = mkdtempSync(join15(tmpdir(), "launchpad-update-"));
13311
- const file = join15(dir, "install.sh");
13914
+ const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
13915
+ const file = join16(dir, "install.sh");
13312
13916
  try {
13313
13917
  writeFileSync7(file, script, { mode: 448 });
13314
13918
  return await new Promise((resolvePromise) => {
@@ -13395,7 +13999,7 @@ function errStderr(e) {
13395
13999
  return "";
13396
14000
  }
13397
14001
  async function detectPackageManager2() {
13398
- const pkgRoot = resolve13(dirname9(fileURLToPath2(import.meta.url)), "..", "..");
14002
+ const pkgRoot = resolve14(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
13399
14003
  const candidates = [];
13400
14004
  const npmRoot = await pmRoot("npm");
13401
14005
  if (npmRoot !== null)
@@ -13403,7 +14007,7 @@ async function detectPackageManager2() {
13403
14007
  const pnpmRoot = await pmRoot("pnpm");
13404
14008
  if (pnpmRoot !== null)
13405
14009
  candidates.push(["pnpm", pnpmRoot]);
13406
- candidates.push(["bun", resolve13(homedir3(), ".bun/install/global/node_modules")]);
14010
+ candidates.push(["bun", resolve14(homedir3(), ".bun/install/global/node_modules")]);
13407
14011
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
13408
14012
  return matches.length === 1 ? matches[0][0] : null;
13409
14013
  }
@@ -13417,8 +14021,8 @@ async function pmRoot(pm) {
13417
14021
  }
13418
14022
  }
13419
14023
  function pathContains(root, child) {
13420
- const rel = relative4(root, child);
13421
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
14024
+ const rel = relative5(root, child);
14025
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
13422
14026
  }
13423
14027
  function runUpgrade(pm) {
13424
14028
  return new Promise((resolvePromise) => {
@@ -13541,7 +14145,7 @@ function printHelp5(io) {
13541
14145
 
13542
14146
  // src/commands/validate.ts
13543
14147
  import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
13544
- import { dirname as dirname10, resolve as resolve14 } from "node:path";
14148
+ import { dirname as dirname11, resolve as resolve15 } from "node:path";
13545
14149
  var validateCommand = {
13546
14150
  name: "validate",
13547
14151
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -13555,13 +14159,13 @@ async function runValidate(args, io) {
13555
14159
  return 64;
13556
14160
  }
13557
14161
  const { file, json, strictGroups } = parseResult;
13558
- const path13 = resolve14(process.cwd(), file ?? "launchpad.yaml");
13559
- const result = loadManifest(path13);
14162
+ const path14 = resolve15(process.cwd(), file ?? "launchpad.yaml");
14163
+ const result = loadManifest(path14);
13560
14164
  if (result.kind !== "ok") {
13561
14165
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
13562
14166
  }
13563
- const boundary = checkBoundary(path13, result.manifest.app !== undefined);
13564
- const serveErrors = checkStaticServeDir(path13, result.manifest);
14167
+ const boundary = checkBoundary(path14, result.manifest.app !== undefined);
14168
+ const serveErrors = checkStaticServeDir(path14, result.manifest);
13565
14169
  if (serveErrors.length > 0) {
13566
14170
  boundary.errors = [...boundary.errors, ...serveErrors];
13567
14171
  }
@@ -13569,7 +14173,7 @@ async function runValidate(args, io) {
13569
14173
  return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
13570
14174
  }
13571
14175
  function checkBoundary(manifestPath, declared) {
13572
- const dir = dirname10(manifestPath);
14176
+ const dir = dirname11(manifestPath);
13573
14177
  let files;
13574
14178
  try {
13575
14179
  files = walkCwd(dir).files;
@@ -13601,8 +14205,8 @@ function checkStaticServeDir(manifestPath, manifest) {
13601
14205
  `static app destination_dir '${serveDir}' is unsafe — use a confined relative subdirectory (e.g. '${STATIC_SERVE_DIR}'). ` + `Serving '.' or an absolute/parent path would publish files outside your app's content.`
13602
14206
  ];
13603
14207
  }
13604
- const dir = dirname10(manifestPath);
13605
- const abs = resolve14(dir, serveDir);
14208
+ const dir = dirname11(manifestPath);
14209
+ const abs = resolve15(dir, serveDir);
13606
14210
  if (!existsSync15(abs)) {
13607
14211
  return [
13608
14212
  `static app serves '${serveDir}/' but that directory does not exist — nothing would be served. ` + `Create ${serveDir}/index.html (or run \`launchpad init\` to scaffold it).`
@@ -13628,7 +14232,7 @@ function checkStaticServeDir(manifestPath, manifest) {
13628
14232
  `static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
13629
14233
  ];
13630
14234
  }
13631
- if (!existsSync15(resolve14(abs, "index.html"))) {
14235
+ if (!existsSync15(resolve15(abs, "index.html"))) {
13632
14236
  return [
13633
14237
  `static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
13634
14238
  ];
@@ -14497,7 +15101,7 @@ var listOwnersCommand = makeListOwnersCommand();
14497
15101
  // src/update-notifier.ts
14498
15102
  import { spawn as spawn6 } from "node:child_process";
14499
15103
  import { homedir as homedir4 } from "node:os";
14500
- import { join as join16 } from "node:path";
15104
+ import { join as join17 } from "node:path";
14501
15105
  import {
14502
15106
  existsSync as existsSync16,
14503
15107
  mkdirSync as mkdirSync4,
@@ -14507,7 +15111,7 @@ import {
14507
15111
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
14508
15112
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
14509
15113
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
14510
- var CACHE_FILE = join16(homedir4(), ".launchpad", "update-check.json");
15114
+ var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
14511
15115
  function readCache2() {
14512
15116
  try {
14513
15117
  const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
@@ -14523,12 +15127,12 @@ function readCache2() {
14523
15127
  }
14524
15128
  function writeCache2(state) {
14525
15129
  try {
14526
- mkdirSync4(join16(homedir4(), ".launchpad"), { recursive: true });
15130
+ mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
14527
15131
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
14528
15132
  `, { mode: 384 });
14529
15133
  } catch {}
14530
15134
  }
14531
- var SKILLS_HINT_MARKER = join16(homedir4(), ".launchpad", "skills-hint-shown");
15135
+ var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
14532
15136
  function absentHintShown() {
14533
15137
  try {
14534
15138
  return existsSync16(SKILLS_HINT_MARKER);
@@ -14538,7 +15142,7 @@ function absentHintShown() {
14538
15142
  }
14539
15143
  function markAbsentHintShown() {
14540
15144
  try {
14541
- mkdirSync4(join16(homedir4(), ".launchpad"), { recursive: true });
15145
+ mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
14542
15146
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
14543
15147
  `, { mode: 384 });
14544
15148
  } catch {}
@@ -14641,7 +15245,7 @@ var refreshUpdateCacheCommand = {
14641
15245
  // src/telemetry.ts
14642
15246
  import { spawn as spawn7 } from "node:child_process";
14643
15247
  import { homedir as homedir5 } from "node:os";
14644
- import { join as join17 } from "node:path";
15248
+ import { join as join18 } from "node:path";
14645
15249
  import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
14646
15250
  import { randomUUID } from "node:crypto";
14647
15251
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
@@ -14650,8 +15254,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
14650
15254
  var CAPTURE_PATH = "/i/v0/e/";
14651
15255
  var SEND_TIMEOUT_MS = 3000;
14652
15256
  var APP = "launchpad-cli";
14653
- var DEVICE_ID_FILE = join17(homedir5(), ".launchpad", "telemetry-id.json");
14654
- var FIRST_RUN_MARKER = join17(homedir5(), ".launchpad", "telemetry-notice-shown");
15257
+ var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
15258
+ var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
14655
15259
  function buildEvent(ctx, identity, nowMs) {
14656
15260
  const properties = {
14657
15261
  app: APP,
@@ -14725,7 +15329,7 @@ function getOrCreateDeviceId() {
14725
15329
  } catch {}
14726
15330
  const id = randomUUID();
14727
15331
  try {
14728
- mkdirSync5(join17(homedir5(), ".launchpad"), { recursive: true });
15332
+ mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
14729
15333
  writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
14730
15334
  `, {
14731
15335
  mode: 384
@@ -14803,7 +15407,7 @@ function firstRunNoticeShown() {
14803
15407
  }
14804
15408
  function markFirstRunNotice() {
14805
15409
  try {
14806
- mkdirSync5(join17(homedir5(), ".launchpad"), { recursive: true });
15410
+ mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
14807
15411
  writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
14808
15412
  `, { mode: 384 });
14809
15413
  } catch {}
@@ -14871,14 +15475,14 @@ import { platform } from "node:os";
14871
15475
  // src/report/breadcrumb.ts
14872
15476
  import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
14873
15477
  import { homedir as homedir6 } from "node:os";
14874
- import { join as join18, dirname as dirname11 } from "node:path";
15478
+ import { join as join19, dirname as dirname12 } from "node:path";
14875
15479
  function breadcrumbPath() {
14876
- return join18(homedir6(), ".launchpad", "last-run.json");
15480
+ return join19(homedir6(), ".launchpad", "last-run.json");
14877
15481
  }
14878
15482
  function writeBreadcrumb(verb, exit) {
14879
15483
  try {
14880
15484
  const p = breadcrumbPath();
14881
- mkdirSync6(dirname11(p), { recursive: true });
15485
+ mkdirSync6(dirname12(p), { recursive: true });
14882
15486
  writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
14883
15487
  } catch {}
14884
15488
  }