@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/CHANGELOG.md +22 -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 +773 -169
- 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/pull.d.ts +2 -0
- package/dist/commands/pull.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
|
};
|
|
@@ -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 : {
|
|
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
|
|
7855
|
-
io.err(" launchpad
|
|
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;
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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>] [--
|
|
11760
|
+
"usage: launchpad pull [<slug>] [--slug <slug>] [--bootstrap]",
|
|
11761
|
+
" launchpad pull --manifest [<slug>] [--out <path>] [--status]",
|
|
11159
11762
|
"",
|
|
11160
|
-
"
|
|
11161
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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 (${
|
|
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(
|
|
11717
|
-
if (!existsSync10(
|
|
12320
|
+
function readCurrentManifest(path14) {
|
|
12321
|
+
if (!existsSync10(path14))
|
|
11718
12322
|
return null;
|
|
11719
12323
|
let raw;
|
|
11720
12324
|
try {
|
|
11721
|
-
raw = readFileSync15(
|
|
12325
|
+
raw = readFileSync15(path14, "utf8");
|
|
11722
12326
|
} catch {
|
|
11723
12327
|
return null;
|
|
11724
12328
|
}
|
|
11725
|
-
const parsed = parseManifest2(raw,
|
|
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
|
|
12276
|
-
if (!existsSync12(
|
|
12277
|
-
io.err(`✗ ${
|
|
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(
|
|
12887
|
+
obj = parseYaml8(readFileSync17(path14, "utf8"));
|
|
12284
12888
|
} catch (e) {
|
|
12285
|
-
io.err(`✗ ${
|
|
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(`✗ ${
|
|
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 ${
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
12840
|
-
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";
|
|
12841
13445
|
|
|
12842
13446
|
// src/skills-bundle.ts
|
|
12843
13447
|
import { homedir as homedir2 } from "node:os";
|
|
12844
|
-
import { join as
|
|
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 ??
|
|
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(
|
|
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(
|
|
13468
|
+
function readSkillVersion(path14) {
|
|
12865
13469
|
try {
|
|
12866
|
-
const text = readFileSync18(
|
|
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 =
|
|
13547
|
+
const here = dirname9(fileURLToPath(import.meta.url));
|
|
12944
13548
|
const candidates = [
|
|
12945
|
-
|
|
12946
|
-
|
|
13549
|
+
resolve13(here, "..", "skills"),
|
|
13550
|
+
resolve13(here, "..", "..", "skills")
|
|
12947
13551
|
];
|
|
12948
13552
|
for (const c of candidates) {
|
|
12949
|
-
if (existsSync14(
|
|
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
|
|
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 =
|
|
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 =
|
|
12976
|
-
await
|
|
12977
|
-
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 });
|
|
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
|
|
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 =
|
|
13001
|
-
await
|
|
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
|
|
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 =
|
|
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(
|
|
13633
|
+
async function readVersion(path14) {
|
|
13030
13634
|
let text;
|
|
13031
13635
|
try {
|
|
13032
|
-
text = await
|
|
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(
|
|
13646
|
+
async function isDir(path14) {
|
|
13043
13647
|
try {
|
|
13044
|
-
const stat = await
|
|
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
|
|
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
|
|
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(
|
|
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((
|
|
13104
|
-
server.once("error", () =>
|
|
13105
|
-
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));
|
|
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(
|
|
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 =
|
|
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(
|
|
13311
|
-
const file =
|
|
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 =
|
|
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",
|
|
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 =
|
|
13421
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
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
|
|
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
|
|
13559
|
-
const result = loadManifest(
|
|
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(
|
|
13564
|
-
const serveErrors = checkStaticServeDir(
|
|
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 =
|
|
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 =
|
|
13605
|
-
const abs =
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
14654
|
-
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");
|
|
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(
|
|
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(
|
|
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
|
|
15478
|
+
import { join as join19, dirname as dirname12 } from "node:path";
|
|
14875
15479
|
function breadcrumbPath() {
|
|
14876
|
-
return
|
|
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(
|
|
15485
|
+
mkdirSync6(dirname12(p), { recursive: true });
|
|
14882
15486
|
writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
|
|
14883
15487
|
} catch {}
|
|
14884
15488
|
}
|