@m-kopa/launchpad-cli 0.51.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/CHANGELOG.md +32 -0
- package/dist/bundle/cwd-walker.d.ts.map +1 -1
- package/dist/bundle/orchestrate.d.ts +2 -0
- package/dist/bundle/orchestrate.d.ts.map +1 -1
- package/dist/bundle/upload.d.ts +4 -1
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +830 -179
- package/dist/clone/base-sha-marker.d.ts +12 -3
- package/dist/clone/base-sha-marker.d.ts.map +1 -1
- package/dist/clone/tar-extract.d.ts +2 -0
- package/dist/clone/tar-extract.d.ts.map +1 -1
- package/dist/commands/deploy-flags.d.ts +2 -0
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/merge.d.ts +1 -0
- package/dist/commands/merge.d.ts.map +1 -1
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/deploy/apply.d.ts.map +1 -1
- package/dist/pull/source-sync.d.ts +11 -0
- package/dist/pull/source-sync.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +3 -3
- package/skills/launchpad-deploy/SKILL.md +8 -1
- package/skills/launchpad-deploy-status/SKILL.md +3 -3
- package/skills/launchpad-destroy/SKILL.md +2 -2
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +6 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +20 -13
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.
|
|
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
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
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 (
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
io.err(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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((
|
|
2471
|
-
const hasSingular =
|
|
2472
|
-
const hasPlural =
|
|
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(
|
|
2489
|
-
if (
|
|
2490
|
-
return
|
|
2541
|
+
function allowedEntraGroups(access) {
|
|
2542
|
+
if (access.allowed_entra_groups !== undefined) {
|
|
2543
|
+
return access.allowed_entra_groups;
|
|
2491
2544
|
}
|
|
2492
|
-
return [
|
|
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: {
|
|
6695
|
+
mode: {
|
|
6696
|
+
kind: "content",
|
|
6697
|
+
slug,
|
|
6698
|
+
allowStale,
|
|
6699
|
+
rebaseOntoHead,
|
|
6700
|
+
overrideStale,
|
|
6701
|
+
allowUnguarded
|
|
6702
|
+
},
|
|
6630
6703
|
message,
|
|
6631
6704
|
timeoutSeconds
|
|
6632
6705
|
};
|
|
@@ -6777,6 +6850,31 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6777
6850
|
io.err(`launchpad deploy --apply: --at expects a 40-char lowercase hex git SHA, got '${opts.atSha}'`);
|
|
6778
6851
|
return 64;
|
|
6779
6852
|
}
|
|
6853
|
+
if (opts.preopenedPr !== undefined) {
|
|
6854
|
+
if (opts.atSha === undefined) {
|
|
6855
|
+
io.err("launchpad deploy --apply: server-preopened apply attachment requires its exact manifest SHA");
|
|
6856
|
+
return 2;
|
|
6857
|
+
}
|
|
6858
|
+
io.out(`Attaching to server-preopened apply PR #${opts.preopenedPr.number} ` + `at head ${opts.preopenedPr.headSha}.`);
|
|
6859
|
+
const outcome2 = await pollUntilApplied({
|
|
6860
|
+
slug,
|
|
6861
|
+
manifestSha: opts.atSha,
|
|
6862
|
+
prNumber: opts.preopenedPr.number,
|
|
6863
|
+
expectedHeadSha: opts.preopenedPr.headSha,
|
|
6864
|
+
cfg,
|
|
6865
|
+
io,
|
|
6866
|
+
fetcher,
|
|
6867
|
+
pollIntervalSec,
|
|
6868
|
+
timeoutMs
|
|
6869
|
+
});
|
|
6870
|
+
if (outcome2.kind !== "applied") {
|
|
6871
|
+
return outcomeToExit(outcome2, opts.preopenedPr.url, io);
|
|
6872
|
+
}
|
|
6873
|
+
deletePinIfPresent(cfg, slug, io);
|
|
6874
|
+
io.out(`✓ Applied ${slug} from PR #${opts.preopenedPr.number}.`);
|
|
6875
|
+
deps.onApplied?.();
|
|
6876
|
+
return 0;
|
|
6877
|
+
}
|
|
6780
6878
|
io.out(opts.atSha !== undefined ? `Planning apply for ${slug} @ ${opts.atSha.slice(0, 7)} (managed repo) …` : `Planning apply for ${slug} (bot resolves managed main HEAD) …`);
|
|
6781
6879
|
let plan;
|
|
6782
6880
|
try {
|
|
@@ -6817,9 +6915,6 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6817
6915
|
return 0;
|
|
6818
6916
|
}
|
|
6819
6917
|
}
|
|
6820
|
-
if (opts.preopenedPr !== undefined) {
|
|
6821
|
-
io.out(`Server already opened exact-SHA apply PR #${opts.preopenedPr.number}; ` + "confirming it idempotently before polling.");
|
|
6822
|
-
}
|
|
6823
6918
|
let apply;
|
|
6824
6919
|
try {
|
|
6825
6920
|
apply = await apiJson(cfg, {
|
|
@@ -6834,10 +6929,6 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6834
6929
|
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/apply: ` + `requested ${manifestSha}, received ${String(apply.manifestSha)}`);
|
|
6835
6930
|
return 2;
|
|
6836
6931
|
}
|
|
6837
|
-
if (opts.preopenedPr !== undefined && apply.prNumber !== opts.preopenedPr.number) {
|
|
6838
|
-
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/apply: ` + `server-preopened PR #${opts.preopenedPr.number}, received #${apply.prNumber}`);
|
|
6839
|
-
return 2;
|
|
6840
|
-
}
|
|
6841
6932
|
io.out("");
|
|
6842
6933
|
io.out(`PR opened: ${apply.prUrl}`);
|
|
6843
6934
|
io.out(`Branch: ${apply.branch}`);
|
|
@@ -6896,6 +6987,13 @@ async function pollUntilApplied(args) {
|
|
|
6896
6987
|
await sleep(args.pollIntervalSec * 1000);
|
|
6897
6988
|
continue;
|
|
6898
6989
|
}
|
|
6990
|
+
if (args.expectedHeadSha !== undefined) {
|
|
6991
|
+
breakDots();
|
|
6992
|
+
return {
|
|
6993
|
+
kind: "transport",
|
|
6994
|
+
reason: "the bot cannot verify the exact server-preopened apply attempt"
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6899
6997
|
breakDots();
|
|
6900
6998
|
args.io.err("! bot has no apply-status endpoint yet — falling back to the coarse merge-poll (upgrade pending bot deploy)");
|
|
6901
6999
|
mode = "legacy";
|
|
@@ -6916,6 +7014,13 @@ async function pollUntilApplied(args) {
|
|
|
6916
7014
|
reason: `protocol failure from /apps/${args.slug}/manifest/apply-status: ` + `requested ${args.manifestSha}, received ${status.manifestSha}`
|
|
6917
7015
|
};
|
|
6918
7016
|
}
|
|
7017
|
+
if (args.expectedHeadSha !== undefined && (status.prNumber !== args.prNumber || status.headSha !== args.expectedHeadSha)) {
|
|
7018
|
+
breakDots();
|
|
7019
|
+
return {
|
|
7020
|
+
kind: "transport",
|
|
7021
|
+
reason: `server-preopened PR #${args.prNumber} head ${args.expectedHeadSha}, ` + `received PR #${status.prNumber} head ${String(status.headSha)}`
|
|
7022
|
+
};
|
|
7023
|
+
}
|
|
6919
7024
|
if (status.state === "applied") {
|
|
6920
7025
|
breakDots();
|
|
6921
7026
|
return { kind: "applied" };
|
|
@@ -7567,7 +7672,12 @@ async function runDeploy(args, io) {
|
|
|
7567
7672
|
const cwd = process.cwd();
|
|
7568
7673
|
const manifestPath = path8.join(cwd, "launchpad.yaml");
|
|
7569
7674
|
if (existsSync6(manifestPath)) {
|
|
7570
|
-
const contentMode = flags.mode.kind === "content" ? flags.mode : {
|
|
7675
|
+
const contentMode = flags.mode.kind === "content" ? flags.mode : {
|
|
7676
|
+
allowStale: false,
|
|
7677
|
+
rebaseOntoHead: false,
|
|
7678
|
+
overrideStale: null,
|
|
7679
|
+
allowUnguarded: false
|
|
7680
|
+
};
|
|
7571
7681
|
return runModelADeploy({
|
|
7572
7682
|
cwd,
|
|
7573
7683
|
manifestPath,
|
|
@@ -7575,9 +7685,14 @@ async function runDeploy(args, io) {
|
|
|
7575
7685
|
io,
|
|
7576
7686
|
allowStale: contentMode.allowStale,
|
|
7577
7687
|
rebaseOntoHead: contentMode.rebaseOntoHead,
|
|
7578
|
-
overrideStale: contentMode.overrideStale
|
|
7688
|
+
overrideStale: contentMode.overrideStale,
|
|
7689
|
+
allowUnguarded: contentMode.allowUnguarded
|
|
7579
7690
|
});
|
|
7580
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
|
+
}
|
|
7581
7696
|
const parsed = parseArgs3(args);
|
|
7582
7697
|
if (parsed === null) {
|
|
7583
7698
|
io.err("launchpad deploy: malformed argv (run with --help)");
|
|
@@ -7691,7 +7806,7 @@ async function runDeploy(args, io) {
|
|
|
7691
7806
|
}
|
|
7692
7807
|
}
|
|
7693
7808
|
function deployUsage() {
|
|
7694
|
-
return `usage: launchpad deploy [--message <text>] [--slug <slug>]
|
|
7809
|
+
return `usage: launchpad deploy [--message <text>] [--slug <slug>] [--allow-unguarded]
|
|
7695
7810
|
` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
|
|
7696
7811
|
` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
7697
7812
|
` + `provisioning modes:
|
|
@@ -7819,8 +7934,9 @@ function surfaceStaleBlock(body, io) {
|
|
|
7819
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.`);
|
|
7820
7935
|
}
|
|
7821
7936
|
io.err("");
|
|
7822
|
-
io.err(" To recover
|
|
7823
|
-
io.err(" launchpad
|
|
7937
|
+
io.err(" To recover safely:");
|
|
7938
|
+
io.err(" launchpad pull");
|
|
7939
|
+
io.err(" launchpad deploy");
|
|
7824
7940
|
if (head !== null) {
|
|
7825
7941
|
io.err(" Or, to deliberately overwrite them in place:");
|
|
7826
7942
|
io.err(` launchpad deploy --override-stale ${head.slice(0, 8)}`);
|
|
@@ -7938,12 +8054,26 @@ async function runModelADeploy(args) {
|
|
|
7938
8054
|
const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
|
|
7939
8055
|
let result;
|
|
7940
8056
|
try {
|
|
8057
|
+
if (args.allowUnguarded) {
|
|
8058
|
+
io.err("warning: --allow-unguarded disables clone staleness protection for this deploy.");
|
|
8059
|
+
}
|
|
7941
8060
|
result = await bundleAndDeploy({
|
|
7942
8061
|
cwd,
|
|
7943
8062
|
cfg,
|
|
7944
8063
|
slug,
|
|
7945
|
-
overrideStale: args.overrideStale
|
|
8064
|
+
overrideStale: args.overrideStale,
|
|
8065
|
+
allowUnguarded: args.allowUnguarded
|
|
7946
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
|
+
}
|
|
7947
8077
|
if (args.rebaseOntoHead && result.kind === "upload-error" && result.status === 409 && isStaleBlock(result.body)) {
|
|
7948
8078
|
const head = result.body.head_sha;
|
|
7949
8079
|
if (typeof head === "string" && isValidBaseSha(head)) {
|
|
@@ -7951,7 +8081,7 @@ async function runModelADeploy(args) {
|
|
|
7951
8081
|
io.out(`Re-stamped base revision to current head ${head.slice(0, 12)}; retrying deploy …`);
|
|
7952
8082
|
result = await bundleAndDeploy({ cwd, cfg, slug });
|
|
7953
8083
|
} else {
|
|
7954
|
-
io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block;
|
|
8084
|
+
io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block; run `launchpad pull` and retry.");
|
|
7955
8085
|
return 1;
|
|
7956
8086
|
}
|
|
7957
8087
|
}
|
|
@@ -8002,6 +8132,12 @@ async function runModelADeploy(args) {
|
|
|
8002
8132
|
surfaceStaleBlock(body, io);
|
|
8003
8133
|
return 1;
|
|
8004
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
|
+
}
|
|
8005
8141
|
if (result.status === 409 && errorCode === "access_drift") {
|
|
8006
8142
|
return handleAccessDrift({
|
|
8007
8143
|
slug,
|
|
@@ -9918,16 +10054,16 @@ function buildManifest(inputs, detected) {
|
|
|
9918
10054
|
metadata.description = inputs.description;
|
|
9919
10055
|
}
|
|
9920
10056
|
const deployment = { type: inputs.type };
|
|
9921
|
-
const
|
|
10057
|
+
const access = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
|
|
9922
10058
|
if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
|
|
9923
|
-
|
|
10059
|
+
access.session_duration = inputs.sessionDuration;
|
|
9924
10060
|
}
|
|
9925
10061
|
const manifest = {
|
|
9926
10062
|
apiVersion: "launchpad.m-kopa.us/v1alpha1",
|
|
9927
10063
|
kind: "App",
|
|
9928
10064
|
metadata,
|
|
9929
10065
|
deployment,
|
|
9930
|
-
access
|
|
10066
|
+
access
|
|
9931
10067
|
};
|
|
9932
10068
|
const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
|
|
9933
10069
|
if (resolvedAuth === "gateway") {
|
|
@@ -10337,7 +10473,7 @@ var HANDLED_STATUSES = [409, 422, 502, 503];
|
|
|
10337
10473
|
async function runMerge(args, io) {
|
|
10338
10474
|
const parsed = parseArgs8(args);
|
|
10339
10475
|
if (parsed === null) {
|
|
10340
|
-
io.err(`usage: launchpad merge <prNumber> [--slug <slug>]
|
|
10476
|
+
io.err(`usage: launchpad merge <prNumber> [--slug <slug>] [--repo app|platform]
|
|
10341
10477
|
` + " (prNumber is required; slug defaults to the cwd's `launchpad-app-<slug>` suffix)");
|
|
10342
10478
|
return 64;
|
|
10343
10479
|
}
|
|
@@ -10362,7 +10498,7 @@ async function runMerge(args, io) {
|
|
|
10362
10498
|
io.out(`Merging PR #${parsed.prNumber} on ${slug} …`);
|
|
10363
10499
|
const res = await apiRaw(cfg, {
|
|
10364
10500
|
method: "POST",
|
|
10365
|
-
path: `/apps/${slug}/merge/${parsed.prNumber}
|
|
10501
|
+
path: `/apps/${slug}/merge/${parsed.prNumber}` + (parsed.repository === null ? "" : `?repository=${parsed.repository}`),
|
|
10366
10502
|
nonThrowingStatuses: [...HANDLED_STATUSES]
|
|
10367
10503
|
});
|
|
10368
10504
|
if (res.ok) {
|
|
@@ -10416,6 +10552,7 @@ async function runMerge(args, io) {
|
|
|
10416
10552
|
}
|
|
10417
10553
|
function parseArgs8(args) {
|
|
10418
10554
|
let slug = null;
|
|
10555
|
+
let repository = null;
|
|
10419
10556
|
let prNumber = null;
|
|
10420
10557
|
let i = 0;
|
|
10421
10558
|
while (i < args.length) {
|
|
@@ -10428,6 +10565,14 @@ function parseArgs8(args) {
|
|
|
10428
10565
|
i += 2;
|
|
10429
10566
|
continue;
|
|
10430
10567
|
}
|
|
10568
|
+
if (a === "--repo") {
|
|
10569
|
+
const value = args[i + 1];
|
|
10570
|
+
if (value !== "app" && value !== "platform")
|
|
10571
|
+
return null;
|
|
10572
|
+
repository = value;
|
|
10573
|
+
i += 2;
|
|
10574
|
+
continue;
|
|
10575
|
+
}
|
|
10431
10576
|
if (/^\d+$/.test(a)) {
|
|
10432
10577
|
if (prNumber !== null)
|
|
10433
10578
|
return null;
|
|
@@ -10442,7 +10587,7 @@ function parseArgs8(args) {
|
|
|
10442
10587
|
}
|
|
10443
10588
|
if (prNumber === null)
|
|
10444
10589
|
return null;
|
|
10445
|
-
return { slug, prNumber };
|
|
10590
|
+
return { slug, prNumber, repository };
|
|
10446
10591
|
}
|
|
10447
10592
|
function renderBotError(status, env, io, prNumber = null) {
|
|
10448
10593
|
const code = env?.error ?? "unknown";
|
|
@@ -10479,6 +10624,12 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
10479
10624
|
case "pipeline_unavailable":
|
|
10480
10625
|
io.err("launchpad merge: no review state recorded for this PR — push a commit to trigger the workflow, or wait for the first event.");
|
|
10481
10626
|
return;
|
|
10627
|
+
case "platform_apply_auto_managed":
|
|
10628
|
+
io.err("launchpad merge: this platform apply PR is auto-managed; inspect `launchpad status` or apply status.");
|
|
10629
|
+
return;
|
|
10630
|
+
case "merge_pr_ambiguous":
|
|
10631
|
+
io.err("launchpad merge: that PR number exists in both repositories; pass `--repo app` or `--repo platform`.");
|
|
10632
|
+
return;
|
|
10482
10633
|
case "credential_unavailable":
|
|
10483
10634
|
io.err("launchpad merge: bot credential is temporarily unavailable — retry in a moment.");
|
|
10484
10635
|
return;
|
|
@@ -10960,10 +11111,474 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
|
10960
11111
|
return apiJson(cfg, { path: path12 }, fetcher);
|
|
10961
11112
|
}
|
|
10962
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
|
+
|
|
10963
11578
|
// src/commands/pull.ts
|
|
10964
11579
|
var pullCommand = {
|
|
10965
11580
|
name: "pull",
|
|
10966
|
-
summary: "
|
|
11581
|
+
summary: "sync upstream source into a clone without overwriting local work",
|
|
10967
11582
|
run: runPull
|
|
10968
11583
|
};
|
|
10969
11584
|
var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
@@ -10976,6 +11591,15 @@ async function runPull(args, io) {
|
|
|
10976
11591
|
}
|
|
10977
11592
|
try {
|
|
10978
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
|
+
}
|
|
10979
11603
|
if (parsed.status) {
|
|
10980
11604
|
const noStatusMsg = `launchpad pull: no status block deployed for "${parsed.slug}" yet — ` + `the bot writes it after the first apply.`;
|
|
10981
11605
|
let res;
|
|
@@ -11058,6 +11682,8 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11058
11682
|
let slug = null;
|
|
11059
11683
|
let out = null;
|
|
11060
11684
|
let status = false;
|
|
11685
|
+
let manifest = false;
|
|
11686
|
+
let bootstrap = false;
|
|
11061
11687
|
let i = 0;
|
|
11062
11688
|
while (i < args.length) {
|
|
11063
11689
|
const a = args[i] ?? "";
|
|
@@ -11066,6 +11692,16 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11066
11692
|
i += 1;
|
|
11067
11693
|
continue;
|
|
11068
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
|
+
}
|
|
11069
11705
|
if (a === "--slug") {
|
|
11070
11706
|
const v = args[i + 1];
|
|
11071
11707
|
if (v === undefined)
|
|
@@ -11104,23 +11740,38 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11104
11740
|
if (!SLUG_RE11.test(slug)) {
|
|
11105
11741
|
return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
|
|
11106
11742
|
}
|
|
11107
|
-
|
|
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;
|
|
11108
11757
|
}
|
|
11109
11758
|
function printUsage5(io) {
|
|
11110
11759
|
io.err([
|
|
11111
|
-
"usage: launchpad pull [<slug>] [--slug <slug>] [--
|
|
11760
|
+
"usage: launchpad pull [<slug>] [--slug <slug>] [--bootstrap]",
|
|
11761
|
+
" launchpad pull --manifest [<slug>] [--out <path>] [--status]",
|
|
11112
11762
|
"",
|
|
11113
|
-
"
|
|
11114
|
-
"
|
|
11763
|
+
" By default, safely syncs managed source into the current clone.",
|
|
11764
|
+
" --manifest keeps the previous deployed-manifest read behavior.",
|
|
11115
11765
|
"",
|
|
11116
11766
|
" With no slug, it is inferred from the local launchpad.yaml's",
|
|
11117
11767
|
" declared slug first, then from a launchpad-app-<slug>/",
|
|
11118
11768
|
" directory name. Explicit --slug or positional override.",
|
|
11119
11769
|
"",
|
|
11120
|
-
" --status read the role-redacted status block
|
|
11770
|
+
" --status with --manifest, read the role-redacted status block",
|
|
11771
|
+
" (what your",
|
|
11121
11772
|
" role may see) instead of the spec manifest.",
|
|
11122
11773
|
"",
|
|
11123
|
-
"
|
|
11774
|
+
" In manifest mode, omitting --out writes YAML to stdout."
|
|
11124
11775
|
].join(`
|
|
11125
11776
|
`));
|
|
11126
11777
|
}
|
|
@@ -11389,7 +12040,7 @@ var watchCommand = {
|
|
|
11389
12040
|
};
|
|
11390
12041
|
|
|
11391
12042
|
// src/commands/review.ts
|
|
11392
|
-
import * as
|
|
12043
|
+
import * as path13 from "node:path";
|
|
11393
12044
|
var reviewCommand = {
|
|
11394
12045
|
name: "review",
|
|
11395
12046
|
summary: "show the review state for a PR (slug-scoped)",
|
|
@@ -11409,7 +12060,7 @@ async function runReview(args, io) {
|
|
|
11409
12060
|
} else {
|
|
11410
12061
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
11411
12062
|
if (inferred === null) {
|
|
11412
|
-
io.err(`launchpad review: could not infer slug from cwd (${
|
|
12063
|
+
io.err(`launchpad review: could not infer slug from cwd (${path13.basename(process.cwd())});
|
|
11413
12064
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
11414
12065
|
return 64;
|
|
11415
12066
|
}
|
|
@@ -11666,16 +12317,16 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
11666
12317
|
yes: true
|
|
11667
12318
|
}, io, applyDeps);
|
|
11668
12319
|
}
|
|
11669
|
-
function readCurrentManifest(
|
|
11670
|
-
if (!existsSync10(
|
|
12320
|
+
function readCurrentManifest(path14) {
|
|
12321
|
+
if (!existsSync10(path14))
|
|
11671
12322
|
return null;
|
|
11672
12323
|
let raw;
|
|
11673
12324
|
try {
|
|
11674
|
-
raw = readFileSync15(
|
|
12325
|
+
raw = readFileSync15(path14, "utf8");
|
|
11675
12326
|
} catch {
|
|
11676
12327
|
return null;
|
|
11677
12328
|
}
|
|
11678
|
-
const parsed = parseManifest2(raw,
|
|
12329
|
+
const parsed = parseManifest2(raw, path14);
|
|
11679
12330
|
if (parsed.kind !== "ok")
|
|
11680
12331
|
return null;
|
|
11681
12332
|
return summarise(parsed.manifest);
|
|
@@ -12225,23 +12876,23 @@ var CELL_LABEL2 = {
|
|
|
12225
12876
|
not_deployed: "NOT_DEPLOYED"
|
|
12226
12877
|
};
|
|
12227
12878
|
function loadSet(fleetFile, setName, io) {
|
|
12228
|
-
const
|
|
12229
|
-
if (!existsSync12(
|
|
12230
|
-
io.err(`✗ ${
|
|
12879
|
+
const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
12880
|
+
if (!existsSync12(path14)) {
|
|
12881
|
+
io.err(`✗ ${path14}`);
|
|
12231
12882
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
12232
12883
|
return 2;
|
|
12233
12884
|
}
|
|
12234
12885
|
let obj;
|
|
12235
12886
|
try {
|
|
12236
|
-
obj = parseYaml8(readFileSync17(
|
|
12887
|
+
obj = parseYaml8(readFileSync17(path14, "utf8"));
|
|
12237
12888
|
} catch (e) {
|
|
12238
|
-
io.err(`✗ ${
|
|
12889
|
+
io.err(`✗ ${path14}`);
|
|
12239
12890
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12240
12891
|
return 1;
|
|
12241
12892
|
}
|
|
12242
12893
|
const parsed = parseFleetSecretSets(obj);
|
|
12243
12894
|
if (!parsed.ok) {
|
|
12244
|
-
io.err(`✗ ${
|
|
12895
|
+
io.err(`✗ ${path14}`);
|
|
12245
12896
|
io.err(` ${parsed.issues.length} schema issue(s):`);
|
|
12246
12897
|
for (const i of parsed.issues)
|
|
12247
12898
|
io.err(` ${i.path}: ${i.message}`);
|
|
@@ -12250,7 +12901,7 @@ function loadSet(fleetFile, setName, io) {
|
|
|
12250
12901
|
const set = parsed.manifest.secretSets.find((s) => s.name === setName);
|
|
12251
12902
|
if (set === undefined) {
|
|
12252
12903
|
const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
|
|
12253
|
-
io.err(`✗ secret-set "${setName}" not found in ${
|
|
12904
|
+
io.err(`✗ secret-set "${setName}" not found in ${path14}`);
|
|
12254
12905
|
io.err(` declared sets: ${names}`);
|
|
12255
12906
|
return 1;
|
|
12256
12907
|
}
|
|
@@ -12470,7 +13121,7 @@ function setPushExit(e) {
|
|
|
12470
13121
|
|
|
12471
13122
|
// src/commands/secrets-template.ts
|
|
12472
13123
|
import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "node:fs";
|
|
12473
|
-
import { resolve as
|
|
13124
|
+
import { resolve as resolve12 } from "node:path";
|
|
12474
13125
|
async function runSecretsTemplate(args, io) {
|
|
12475
13126
|
const flags = parseFlags4(args);
|
|
12476
13127
|
if (flags.kind === "usage-error") {
|
|
@@ -12478,7 +13129,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
12478
13129
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
12479
13130
|
return 64;
|
|
12480
13131
|
}
|
|
12481
|
-
const manifestPath =
|
|
13132
|
+
const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
12482
13133
|
const result = loadManifest(manifestPath);
|
|
12483
13134
|
const renderResult = renderManifest(result, io);
|
|
12484
13135
|
if (renderResult.kind !== "ok") {
|
|
@@ -12492,7 +13143,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
12492
13143
|
}
|
|
12493
13144
|
return 0;
|
|
12494
13145
|
}
|
|
12495
|
-
const outPath =
|
|
13146
|
+
const outPath = resolve12(process.cwd(), flags.out);
|
|
12496
13147
|
if (existsSync13(outPath) && !flags.force) {
|
|
12497
13148
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
12498
13149
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
@@ -12789,16 +13440,16 @@ function printHelp3(io) {
|
|
|
12789
13440
|
|
|
12790
13441
|
// src/commands/skills.ts
|
|
12791
13442
|
import { fileURLToPath } from "node:url";
|
|
12792
|
-
import { dirname as
|
|
12793
|
-
import { promises as
|
|
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";
|
|
12794
13445
|
|
|
12795
13446
|
// src/skills-bundle.ts
|
|
12796
13447
|
import { homedir as homedir2 } from "node:os";
|
|
12797
|
-
import { join as
|
|
13448
|
+
import { join as join14 } from "node:path";
|
|
12798
13449
|
import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
|
|
12799
13450
|
var SKILL_PREFIX = "launchpad-";
|
|
12800
13451
|
function skillsTargetDir() {
|
|
12801
|
-
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
13452
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
|
|
12802
13453
|
}
|
|
12803
13454
|
function readInstalledSkills() {
|
|
12804
13455
|
try {
|
|
@@ -12808,15 +13459,15 @@ function readInstalledSkills() {
|
|
|
12808
13459
|
return { present: false, version: null };
|
|
12809
13460
|
return {
|
|
12810
13461
|
present: true,
|
|
12811
|
-
version: readSkillVersion(
|
|
13462
|
+
version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
|
|
12812
13463
|
};
|
|
12813
13464
|
} catch {
|
|
12814
13465
|
return { present: false, version: null };
|
|
12815
13466
|
}
|
|
12816
13467
|
}
|
|
12817
|
-
function readSkillVersion(
|
|
13468
|
+
function readSkillVersion(path14) {
|
|
12818
13469
|
try {
|
|
12819
|
-
const text = readFileSync18(
|
|
13470
|
+
const text = readFileSync18(path14, "utf8");
|
|
12820
13471
|
const fenceEnd = text.indexOf(`
|
|
12821
13472
|
---`, 4);
|
|
12822
13473
|
const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
|
|
@@ -12893,13 +13544,13 @@ function resolveInstallEnv() {
|
|
|
12893
13544
|
return { bundleDir, userSkillsDir };
|
|
12894
13545
|
}
|
|
12895
13546
|
function defaultBundleDir() {
|
|
12896
|
-
const here =
|
|
13547
|
+
const here = dirname9(fileURLToPath(import.meta.url));
|
|
12897
13548
|
const candidates = [
|
|
12898
|
-
|
|
12899
|
-
|
|
13549
|
+
resolve13(here, "..", "skills"),
|
|
13550
|
+
resolve13(here, "..", "..", "skills")
|
|
12900
13551
|
];
|
|
12901
13552
|
for (const c of candidates) {
|
|
12902
|
-
if (existsSync14(
|
|
13553
|
+
if (existsSync14(join15(c, "launchpad-onboard", "SKILL.md"))) {
|
|
12903
13554
|
return c;
|
|
12904
13555
|
}
|
|
12905
13556
|
}
|
|
@@ -12915,19 +13566,19 @@ async function doInstall(io) {
|
|
|
12915
13566
|
return 1;
|
|
12916
13567
|
}
|
|
12917
13568
|
if (!await isDir(env.userSkillsDir)) {
|
|
12918
|
-
await
|
|
13569
|
+
await fs7.mkdir(env.userSkillsDir, { recursive: true });
|
|
12919
13570
|
io.out(`Created ${env.userSkillsDir}.`);
|
|
12920
13571
|
}
|
|
12921
13572
|
let installed = 0;
|
|
12922
13573
|
for (const skill of BUNDLED_SKILLS) {
|
|
12923
|
-
const src =
|
|
13574
|
+
const src = join15(env.bundleDir, skill);
|
|
12924
13575
|
if (!await isDir(src)) {
|
|
12925
13576
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
12926
13577
|
return 1;
|
|
12927
13578
|
}
|
|
12928
|
-
const dest =
|
|
12929
|
-
await
|
|
12930
|
-
await
|
|
13579
|
+
const dest = join15(env.userSkillsDir, skill);
|
|
13580
|
+
await fs7.rm(dest, { recursive: true, force: true });
|
|
13581
|
+
await fs7.cp(src, dest, { recursive: true });
|
|
12931
13582
|
installed++;
|
|
12932
13583
|
io.out(`✓ ${skill} → ${dest}`);
|
|
12933
13584
|
}
|
|
@@ -12943,15 +13594,15 @@ async function doUninstall(io) {
|
|
|
12943
13594
|
io.out(`launchpad skills uninstall: ${env.userSkillsDir} does not exist — nothing to do.`);
|
|
12944
13595
|
return 0;
|
|
12945
13596
|
}
|
|
12946
|
-
const entries = await
|
|
13597
|
+
const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
12947
13598
|
let removed = 0;
|
|
12948
13599
|
for (const entry of entries) {
|
|
12949
13600
|
if (!entry.isDirectory())
|
|
12950
13601
|
continue;
|
|
12951
13602
|
if (!isBundleManaged(entry.name))
|
|
12952
13603
|
continue;
|
|
12953
|
-
const target =
|
|
12954
|
-
await
|
|
13604
|
+
const target = join15(env.userSkillsDir, entry.name);
|
|
13605
|
+
await fs7.rm(target, { recursive: true, force: true });
|
|
12955
13606
|
removed++;
|
|
12956
13607
|
io.out(`✗ removed ${target}`);
|
|
12957
13608
|
}
|
|
@@ -12965,7 +13616,7 @@ async function doList(io) {
|
|
|
12965
13616
|
io.out("(none — ~/.claude/skills/ does not exist)");
|
|
12966
13617
|
return 0;
|
|
12967
13618
|
}
|
|
12968
|
-
const entries = await
|
|
13619
|
+
const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
12969
13620
|
const managedDirs = entries.filter((e) => e.isDirectory() && isBundleManaged(e.name)).map((e) => e.name).sort();
|
|
12970
13621
|
if (managedDirs.length === 0) {
|
|
12971
13622
|
io.out("(none — run `launchpad skills install`)");
|
|
@@ -12973,16 +13624,16 @@ async function doList(io) {
|
|
|
12973
13624
|
}
|
|
12974
13625
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
12975
13626
|
for (const name of managedDirs) {
|
|
12976
|
-
const skillFile =
|
|
13627
|
+
const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
|
|
12977
13628
|
const version = await readVersion(skillFile);
|
|
12978
13629
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
12979
13630
|
}
|
|
12980
13631
|
return 0;
|
|
12981
13632
|
}
|
|
12982
|
-
async function readVersion(
|
|
13633
|
+
async function readVersion(path14) {
|
|
12983
13634
|
let text;
|
|
12984
13635
|
try {
|
|
12985
|
-
text = await
|
|
13636
|
+
text = await fs7.readFile(path14, "utf8");
|
|
12986
13637
|
} catch {
|
|
12987
13638
|
return null;
|
|
12988
13639
|
}
|
|
@@ -12992,9 +13643,9 @@ async function readVersion(path13) {
|
|
|
12992
13643
|
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
12993
13644
|
return m === null ? null : m[1] ?? null;
|
|
12994
13645
|
}
|
|
12995
|
-
async function isDir(
|
|
13646
|
+
async function isDir(path14) {
|
|
12996
13647
|
try {
|
|
12997
|
-
const stat = await
|
|
13648
|
+
const stat = await fs7.stat(path14);
|
|
12998
13649
|
return stat.isDirectory();
|
|
12999
13650
|
} catch {
|
|
13000
13651
|
return false;
|
|
@@ -13005,16 +13656,16 @@ function describe29(e) {
|
|
|
13005
13656
|
}
|
|
13006
13657
|
|
|
13007
13658
|
// src/commands/update.ts
|
|
13008
|
-
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
13659
|
+
import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
|
|
13009
13660
|
import { promisify } from "node:util";
|
|
13010
13661
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13011
|
-
import { dirname as
|
|
13662
|
+
import { dirname as dirname10, resolve as resolve14, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
|
|
13012
13663
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
13013
13664
|
import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
13014
13665
|
|
|
13015
13666
|
// src/commands/channel-auth.ts
|
|
13016
13667
|
import { createServer as createServer2 } from "node:http";
|
|
13017
|
-
import { createHash as
|
|
13668
|
+
import { createHash as createHash4, randomBytes as randomBytes5 } from "node:crypto";
|
|
13018
13669
|
var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
|
|
13019
13670
|
var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
|
|
13020
13671
|
var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
|
|
@@ -13025,7 +13676,7 @@ function base64url(b) {
|
|
|
13025
13676
|
}
|
|
13026
13677
|
function pkcePair() {
|
|
13027
13678
|
const verifier = base64url(randomBytes5(32));
|
|
13028
|
-
const challenge = base64url(
|
|
13679
|
+
const challenge = base64url(createHash4("sha256").update(verifier).digest());
|
|
13029
13680
|
return { verifier, challenge };
|
|
13030
13681
|
}
|
|
13031
13682
|
async function startLoopback(state, timeoutMs) {
|
|
@@ -13053,9 +13704,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
13053
13704
|
resolveCode(code);
|
|
13054
13705
|
}
|
|
13055
13706
|
});
|
|
13056
|
-
const bound = await new Promise((
|
|
13057
|
-
server.once("error", () =>
|
|
13058
|
-
server.listen(0, "127.0.0.1", () =>
|
|
13707
|
+
const bound = await new Promise((resolve14) => {
|
|
13708
|
+
server.once("error", () => resolve14(false));
|
|
13709
|
+
server.listen(0, "127.0.0.1", () => resolve14(true));
|
|
13059
13710
|
});
|
|
13060
13711
|
if (!bound)
|
|
13061
13712
|
return null;
|
|
@@ -13126,12 +13777,12 @@ async function runChannelLoopbackUpdate(deps) {
|
|
|
13126
13777
|
}
|
|
13127
13778
|
|
|
13128
13779
|
// src/commands/update.ts
|
|
13129
|
-
var execFileAsync = promisify(
|
|
13780
|
+
var execFileAsync = promisify(execFile2);
|
|
13130
13781
|
var PKG = "@m-kopa/launchpad-cli";
|
|
13131
13782
|
var REGISTRY = "https://registry.npmjs.org";
|
|
13132
13783
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
13133
13784
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
13134
|
-
var CHANNEL_MARKER =
|
|
13785
|
+
var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
|
|
13135
13786
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
13136
13787
|
var UPGRADE_ARGS = {
|
|
13137
13788
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -13260,8 +13911,8 @@ async function openSystemBrowser(url) {
|
|
|
13260
13911
|
await execFileAsync(opener, [url]);
|
|
13261
13912
|
}
|
|
13262
13913
|
async function runInstallerScript(script) {
|
|
13263
|
-
const dir = mkdtempSync(
|
|
13264
|
-
const file =
|
|
13914
|
+
const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
|
|
13915
|
+
const file = join16(dir, "install.sh");
|
|
13265
13916
|
try {
|
|
13266
13917
|
writeFileSync7(file, script, { mode: 448 });
|
|
13267
13918
|
return await new Promise((resolvePromise) => {
|
|
@@ -13348,7 +13999,7 @@ function errStderr(e) {
|
|
|
13348
13999
|
return "";
|
|
13349
14000
|
}
|
|
13350
14001
|
async function detectPackageManager2() {
|
|
13351
|
-
const pkgRoot =
|
|
14002
|
+
const pkgRoot = resolve14(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
|
|
13352
14003
|
const candidates = [];
|
|
13353
14004
|
const npmRoot = await pmRoot("npm");
|
|
13354
14005
|
if (npmRoot !== null)
|
|
@@ -13356,7 +14007,7 @@ async function detectPackageManager2() {
|
|
|
13356
14007
|
const pnpmRoot = await pmRoot("pnpm");
|
|
13357
14008
|
if (pnpmRoot !== null)
|
|
13358
14009
|
candidates.push(["pnpm", pnpmRoot]);
|
|
13359
|
-
candidates.push(["bun",
|
|
14010
|
+
candidates.push(["bun", resolve14(homedir3(), ".bun/install/global/node_modules")]);
|
|
13360
14011
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
13361
14012
|
return matches.length === 1 ? matches[0][0] : null;
|
|
13362
14013
|
}
|
|
@@ -13370,8 +14021,8 @@ async function pmRoot(pm) {
|
|
|
13370
14021
|
}
|
|
13371
14022
|
}
|
|
13372
14023
|
function pathContains(root, child) {
|
|
13373
|
-
const rel =
|
|
13374
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
14024
|
+
const rel = relative5(root, child);
|
|
14025
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
13375
14026
|
}
|
|
13376
14027
|
function runUpgrade(pm) {
|
|
13377
14028
|
return new Promise((resolvePromise) => {
|
|
@@ -13494,7 +14145,7 @@ function printHelp5(io) {
|
|
|
13494
14145
|
|
|
13495
14146
|
// src/commands/validate.ts
|
|
13496
14147
|
import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
13497
|
-
import { dirname as
|
|
14148
|
+
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
13498
14149
|
var validateCommand = {
|
|
13499
14150
|
name: "validate",
|
|
13500
14151
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -13508,13 +14159,13 @@ async function runValidate(args, io) {
|
|
|
13508
14159
|
return 64;
|
|
13509
14160
|
}
|
|
13510
14161
|
const { file, json, strictGroups } = parseResult;
|
|
13511
|
-
const
|
|
13512
|
-
const result = loadManifest(
|
|
14162
|
+
const path14 = resolve15(process.cwd(), file ?? "launchpad.yaml");
|
|
14163
|
+
const result = loadManifest(path14);
|
|
13513
14164
|
if (result.kind !== "ok") {
|
|
13514
14165
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
13515
14166
|
}
|
|
13516
|
-
const boundary = checkBoundary(
|
|
13517
|
-
const serveErrors = checkStaticServeDir(
|
|
14167
|
+
const boundary = checkBoundary(path14, result.manifest.app !== undefined);
|
|
14168
|
+
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
13518
14169
|
if (serveErrors.length > 0) {
|
|
13519
14170
|
boundary.errors = [...boundary.errors, ...serveErrors];
|
|
13520
14171
|
}
|
|
@@ -13522,7 +14173,7 @@ async function runValidate(args, io) {
|
|
|
13522
14173
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
13523
14174
|
}
|
|
13524
14175
|
function checkBoundary(manifestPath, declared) {
|
|
13525
|
-
const dir =
|
|
14176
|
+
const dir = dirname11(manifestPath);
|
|
13526
14177
|
let files;
|
|
13527
14178
|
try {
|
|
13528
14179
|
files = walkCwd(dir).files;
|
|
@@ -13554,8 +14205,8 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
13554
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.`
|
|
13555
14206
|
];
|
|
13556
14207
|
}
|
|
13557
|
-
const dir =
|
|
13558
|
-
const abs =
|
|
14208
|
+
const dir = dirname11(manifestPath);
|
|
14209
|
+
const abs = resolve15(dir, serveDir);
|
|
13559
14210
|
if (!existsSync15(abs)) {
|
|
13560
14211
|
return [
|
|
13561
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).`
|
|
@@ -13581,7 +14232,7 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
13581
14232
|
`static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
|
|
13582
14233
|
];
|
|
13583
14234
|
}
|
|
13584
|
-
if (!existsSync15(
|
|
14235
|
+
if (!existsSync15(resolve15(abs, "index.html"))) {
|
|
13585
14236
|
return [
|
|
13586
14237
|
`static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
|
|
13587
14238
|
];
|
|
@@ -14450,7 +15101,7 @@ var listOwnersCommand = makeListOwnersCommand();
|
|
|
14450
15101
|
// src/update-notifier.ts
|
|
14451
15102
|
import { spawn as spawn6 } from "node:child_process";
|
|
14452
15103
|
import { homedir as homedir4 } from "node:os";
|
|
14453
|
-
import { join as
|
|
15104
|
+
import { join as join17 } from "node:path";
|
|
14454
15105
|
import {
|
|
14455
15106
|
existsSync as existsSync16,
|
|
14456
15107
|
mkdirSync as mkdirSync4,
|
|
@@ -14460,7 +15111,7 @@ import {
|
|
|
14460
15111
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
14461
15112
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14462
15113
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
14463
|
-
var CACHE_FILE =
|
|
15114
|
+
var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
|
|
14464
15115
|
function readCache2() {
|
|
14465
15116
|
try {
|
|
14466
15117
|
const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
|
|
@@ -14476,12 +15127,12 @@ function readCache2() {
|
|
|
14476
15127
|
}
|
|
14477
15128
|
function writeCache2(state) {
|
|
14478
15129
|
try {
|
|
14479
|
-
mkdirSync4(
|
|
15130
|
+
mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
|
|
14480
15131
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
14481
15132
|
`, { mode: 384 });
|
|
14482
15133
|
} catch {}
|
|
14483
15134
|
}
|
|
14484
|
-
var SKILLS_HINT_MARKER =
|
|
15135
|
+
var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
|
|
14485
15136
|
function absentHintShown() {
|
|
14486
15137
|
try {
|
|
14487
15138
|
return existsSync16(SKILLS_HINT_MARKER);
|
|
@@ -14491,7 +15142,7 @@ function absentHintShown() {
|
|
|
14491
15142
|
}
|
|
14492
15143
|
function markAbsentHintShown() {
|
|
14493
15144
|
try {
|
|
14494
|
-
mkdirSync4(
|
|
15145
|
+
mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
|
|
14495
15146
|
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
14496
15147
|
`, { mode: 384 });
|
|
14497
15148
|
} catch {}
|
|
@@ -14594,7 +15245,7 @@ var refreshUpdateCacheCommand = {
|
|
|
14594
15245
|
// src/telemetry.ts
|
|
14595
15246
|
import { spawn as spawn7 } from "node:child_process";
|
|
14596
15247
|
import { homedir as homedir5 } from "node:os";
|
|
14597
|
-
import { join as
|
|
15248
|
+
import { join as join18 } from "node:path";
|
|
14598
15249
|
import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
|
|
14599
15250
|
import { randomUUID } from "node:crypto";
|
|
14600
15251
|
var INTERNAL_EMIT_VERB = "__emit-telemetry";
|
|
@@ -14603,8 +15254,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
|
|
|
14603
15254
|
var CAPTURE_PATH = "/i/v0/e/";
|
|
14604
15255
|
var SEND_TIMEOUT_MS = 3000;
|
|
14605
15256
|
var APP = "launchpad-cli";
|
|
14606
|
-
var DEVICE_ID_FILE =
|
|
14607
|
-
var FIRST_RUN_MARKER =
|
|
15257
|
+
var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
|
|
15258
|
+
var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
|
|
14608
15259
|
function buildEvent(ctx, identity, nowMs) {
|
|
14609
15260
|
const properties = {
|
|
14610
15261
|
app: APP,
|
|
@@ -14678,7 +15329,7 @@ function getOrCreateDeviceId() {
|
|
|
14678
15329
|
} catch {}
|
|
14679
15330
|
const id = randomUUID();
|
|
14680
15331
|
try {
|
|
14681
|
-
mkdirSync5(
|
|
15332
|
+
mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
|
|
14682
15333
|
writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
|
|
14683
15334
|
`, {
|
|
14684
15335
|
mode: 384
|
|
@@ -14756,7 +15407,7 @@ function firstRunNoticeShown() {
|
|
|
14756
15407
|
}
|
|
14757
15408
|
function markFirstRunNotice() {
|
|
14758
15409
|
try {
|
|
14759
|
-
mkdirSync5(
|
|
15410
|
+
mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
|
|
14760
15411
|
writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
|
|
14761
15412
|
`, { mode: 384 });
|
|
14762
15413
|
} catch {}
|
|
@@ -14824,14 +15475,14 @@ import { platform } from "node:os";
|
|
|
14824
15475
|
// src/report/breadcrumb.ts
|
|
14825
15476
|
import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
|
|
14826
15477
|
import { homedir as homedir6 } from "node:os";
|
|
14827
|
-
import { join as
|
|
15478
|
+
import { join as join19, dirname as dirname12 } from "node:path";
|
|
14828
15479
|
function breadcrumbPath() {
|
|
14829
|
-
return
|
|
15480
|
+
return join19(homedir6(), ".launchpad", "last-run.json");
|
|
14830
15481
|
}
|
|
14831
15482
|
function writeBreadcrumb(verb, exit) {
|
|
14832
15483
|
try {
|
|
14833
15484
|
const p = breadcrumbPath();
|
|
14834
|
-
mkdirSync6(
|
|
15485
|
+
mkdirSync6(dirname12(p), { recursive: true });
|
|
14835
15486
|
writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
|
|
14836
15487
|
} catch {}
|
|
14837
15488
|
}
|