@m-kopa/launchpad-cli 0.52.0 → 0.54.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 +33 -0
- package/dist/auth/discovery.d.ts.map +1 -1
- package/dist/auth/gateway-flow.d.ts.map +1 -1
- package/dist/auth/registration.d.ts.map +1 -1
- package/dist/auth/token.d.ts.map +1 -1
- 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 +6 -1
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +906 -186
- 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/dispatcher.d.ts +5 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/http/api-client.d.ts.map +1 -1
- package/dist/http/errors.d.ts +11 -0
- package/dist/http/errors.d.ts.map +1 -1
- package/dist/http/preflight.d.ts +7 -0
- package/dist/http/preflight.d.ts.map +1 -0
- package/dist/http/version-headers.d.ts +10 -0
- package/dist/http/version-headers.d.ts.map +1 -0
- 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 +4 -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.54.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -213,6 +213,22 @@ class ForbiddenError extends Error {
|
|
|
213
213
|
class NotFoundError extends Error {
|
|
214
214
|
code = "not_found";
|
|
215
215
|
}
|
|
216
|
+
var EXIT_CLI_UPGRADE_REQUIRED = 42;
|
|
217
|
+
|
|
218
|
+
class UpgradeRequiredError extends Error {
|
|
219
|
+
code = "cli_upgrade_required";
|
|
220
|
+
minimum;
|
|
221
|
+
yours;
|
|
222
|
+
constructor(minimum, yours) {
|
|
223
|
+
super(upgradeRequiredMessage(minimum, yours));
|
|
224
|
+
this.name = "UpgradeRequiredError";
|
|
225
|
+
this.minimum = minimum;
|
|
226
|
+
this.yours = yours;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function upgradeRequiredMessage(minimum, yours) {
|
|
230
|
+
return `you're on ${yours}; minimum supported is ${minimum} - run launchpad update`;
|
|
231
|
+
}
|
|
216
232
|
|
|
217
233
|
class ApiError extends Error {
|
|
218
234
|
faultCode = "api_error";
|
|
@@ -363,6 +379,21 @@ function escapeHtml(s) {
|
|
|
363
379
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
364
380
|
}
|
|
365
381
|
|
|
382
|
+
// src/http/version-headers.ts
|
|
383
|
+
var CLI_VERSION_HEADER = "x-launchpad-cli-version";
|
|
384
|
+
function cliVersionHeaders(headers = {}) {
|
|
385
|
+
const result = { ...headers };
|
|
386
|
+
for (const key of Object.keys(result)) {
|
|
387
|
+
const lower = key.toLowerCase();
|
|
388
|
+
if (lower === CLI_VERSION_HEADER || lower === "user-agent") {
|
|
389
|
+
delete result[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
result[CLI_VERSION_HEADER] = CLI_VERSION;
|
|
393
|
+
result["user-agent"] = `launchpad-cli/${CLI_VERSION}`;
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
366
397
|
// src/auth/discovery.ts
|
|
367
398
|
class DiscoveryError extends Error {
|
|
368
399
|
code = "discovery_error";
|
|
@@ -401,7 +432,10 @@ async function discoverOauthEndpoints(botUrl, fetcher = fetch) {
|
|
|
401
432
|
async function fetchJson(fetcher, url) {
|
|
402
433
|
let res;
|
|
403
434
|
try {
|
|
404
|
-
res = await fetcher(url, {
|
|
435
|
+
res = await fetcher(url, {
|
|
436
|
+
method: "GET",
|
|
437
|
+
headers: cliVersionHeaders()
|
|
438
|
+
});
|
|
405
439
|
} catch (e) {
|
|
406
440
|
throw new DiscoveryError(`discovery: network error fetching ${url}: ${describe(e)}`);
|
|
407
441
|
}
|
|
@@ -471,10 +505,10 @@ async function registerClient(registrationEndpoint, redirectUri, fetcher = fetch
|
|
|
471
505
|
try {
|
|
472
506
|
res = await fetcher(registrationEndpoint, {
|
|
473
507
|
method: "POST",
|
|
474
|
-
headers: {
|
|
508
|
+
headers: cliVersionHeaders({
|
|
475
509
|
"content-type": "application/json",
|
|
476
510
|
accept: "application/json"
|
|
477
|
-
},
|
|
511
|
+
}),
|
|
478
512
|
body: JSON.stringify(body)
|
|
479
513
|
});
|
|
480
514
|
} catch (e) {
|
|
@@ -535,10 +569,10 @@ async function postTokenForm(tokenEndpoint, form, fetcher) {
|
|
|
535
569
|
try {
|
|
536
570
|
res = await fetcher(tokenEndpoint, {
|
|
537
571
|
method: "POST",
|
|
538
|
-
headers: {
|
|
572
|
+
headers: cliVersionHeaders({
|
|
539
573
|
"content-type": "application/x-www-form-urlencoded",
|
|
540
574
|
accept: "application/json"
|
|
541
|
-
},
|
|
575
|
+
}),
|
|
542
576
|
body: form.toString()
|
|
543
577
|
});
|
|
544
578
|
} catch (e) {
|
|
@@ -805,7 +839,9 @@ async function revokeGatewaySession(params, fetcher = fetch) {
|
|
|
805
839
|
try {
|
|
806
840
|
res = await fetcher(url, {
|
|
807
841
|
method: "POST",
|
|
808
|
-
headers: {
|
|
842
|
+
headers: cliVersionHeaders({
|
|
843
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
844
|
+
}),
|
|
809
845
|
body: new URLSearchParams({ refresh_token: params.refreshToken }).toString()
|
|
810
846
|
});
|
|
811
847
|
} catch (e) {
|
|
@@ -823,7 +859,10 @@ async function probeGateway(gatewayUrl, fetcher) {
|
|
|
823
859
|
const url = `${gatewayUrl}${CLI_SESSION_AUTH_PATH}`;
|
|
824
860
|
let res;
|
|
825
861
|
try {
|
|
826
|
-
res = await fetcher(url, {
|
|
862
|
+
res = await fetcher(url, {
|
|
863
|
+
method: "GET",
|
|
864
|
+
headers: cliVersionHeaders()
|
|
865
|
+
});
|
|
827
866
|
} catch (e) {
|
|
828
867
|
throw new GatewayUnavailableError(`auth gateway unreachable at ${gatewayUrl}: ${describe5(e)}`);
|
|
829
868
|
}
|
|
@@ -840,10 +879,10 @@ async function postSessionTokenForm(gatewayUrl, form, fetcher) {
|
|
|
840
879
|
try {
|
|
841
880
|
res = await fetcher(url, {
|
|
842
881
|
method: "POST",
|
|
843
|
-
headers: {
|
|
882
|
+
headers: cliVersionHeaders({
|
|
844
883
|
"content-type": "application/x-www-form-urlencoded",
|
|
845
884
|
accept: "application/json"
|
|
846
|
-
},
|
|
885
|
+
}),
|
|
847
886
|
body: form.toString()
|
|
848
887
|
});
|
|
849
888
|
} catch (e) {
|
|
@@ -1054,12 +1093,11 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
1054
1093
|
}
|
|
1055
1094
|
throw e;
|
|
1056
1095
|
}
|
|
1057
|
-
const headers = {
|
|
1096
|
+
const headers = cliVersionHeaders({
|
|
1058
1097
|
accept: "application/json",
|
|
1059
1098
|
...opts.headers,
|
|
1060
|
-
authorization: `Bearer ${token}
|
|
1061
|
-
|
|
1062
|
-
};
|
|
1099
|
+
authorization: `Bearer ${token}`
|
|
1100
|
+
});
|
|
1063
1101
|
for (const k of Object.keys(headers)) {
|
|
1064
1102
|
if (k !== "authorization" && k.toLowerCase() === "authorization") {
|
|
1065
1103
|
delete headers[k];
|
|
@@ -1083,6 +1121,9 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
1083
1121
|
} catch (e) {
|
|
1084
1122
|
throw recordedFault(new TransportError(`network error calling ${url}: ${describe7(e)}`));
|
|
1085
1123
|
}
|
|
1124
|
+
if (res.status === 426) {
|
|
1125
|
+
throw recordedFault(await decodeUpgradeRequired(res, opts.path));
|
|
1126
|
+
}
|
|
1086
1127
|
if (res.status === 401) {
|
|
1087
1128
|
const detail = await errorDetail(res, opts.path);
|
|
1088
1129
|
throw recordedFault(new UnauthenticatedError(`bot returned 401 for ${opts.path}: ${detail} — run \`launchpad login\``));
|
|
@@ -1104,6 +1145,18 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
1104
1145
|
clearFault();
|
|
1105
1146
|
return res;
|
|
1106
1147
|
}
|
|
1148
|
+
async function decodeUpgradeRequired(response, endpoint) {
|
|
1149
|
+
const body = await readErrorText(response);
|
|
1150
|
+
if (body.kind === "text") {
|
|
1151
|
+
try {
|
|
1152
|
+
const parsed = JSON.parse(body.text);
|
|
1153
|
+
if (typeof parsed.minimum === "string" && typeof parsed.yours === "string") {
|
|
1154
|
+
return new UpgradeRequiredError(parsed.minimum, parsed.yours);
|
|
1155
|
+
}
|
|
1156
|
+
} catch {}
|
|
1157
|
+
}
|
|
1158
|
+
return new ApiError(decodeErrorEnvelope({ code: "cli_upgrade_required", message: "CLI upgrade required" }, { status: 426, endpoint }));
|
|
1159
|
+
}
|
|
1107
1160
|
async function readErrorText(res) {
|
|
1108
1161
|
const reader = res.body?.getReader();
|
|
1109
1162
|
if (reader === undefined)
|
|
@@ -1342,7 +1395,7 @@ async function extractToDir(body, destDir, opts = {}) {
|
|
|
1342
1395
|
const absDest = path3.resolve(destDir);
|
|
1343
1396
|
try {
|
|
1344
1397
|
const existing = await fs2.readdir(absDest);
|
|
1345
|
-
if (existing.length > 0) {
|
|
1398
|
+
if (existing.length > 0 && opts.allowNonEmpty !== true) {
|
|
1346
1399
|
throw new DestinationNotEmptyError(`destination ${absDest} is not empty (${existing.length} entries); refusing to extract`);
|
|
1347
1400
|
}
|
|
1348
1401
|
} catch (e) {
|
|
@@ -1470,8 +1523,18 @@ async function writeBaseShaMarker(cwd, sha) {
|
|
|
1470
1523
|
await ensureExcluded(cwd);
|
|
1471
1524
|
const dir = path4.resolve(cwd, MARKER_DIR);
|
|
1472
1525
|
await fs3.mkdir(dir, { recursive: true });
|
|
1473
|
-
|
|
1474
|
-
|
|
1526
|
+
const marker = path4.resolve(cwd, MARKER_RELPATH);
|
|
1527
|
+
const temporary = `${marker}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
1528
|
+
try {
|
|
1529
|
+
await fs3.writeFile(temporary, `${sha}
|
|
1530
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1531
|
+
await fs3.rename(temporary, marker);
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
await fs3.rm(temporary, { force: true }).catch(() => {
|
|
1534
|
+
return;
|
|
1535
|
+
});
|
|
1536
|
+
throw error;
|
|
1537
|
+
}
|
|
1475
1538
|
}
|
|
1476
1539
|
async function readBaseShaMarker(cwd) {
|
|
1477
1540
|
let raw;
|
|
@@ -1483,6 +1546,18 @@ async function readBaseShaMarker(cwd) {
|
|
|
1483
1546
|
const sha = raw.trim();
|
|
1484
1547
|
return isValidBaseSha(sha) ? sha : null;
|
|
1485
1548
|
}
|
|
1549
|
+
async function readBaseShaMarkerState(cwd) {
|
|
1550
|
+
let raw;
|
|
1551
|
+
try {
|
|
1552
|
+
raw = await fs3.readFile(path4.resolve(cwd, MARKER_RELPATH), "utf8");
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (isErrno3(error) && error.code === "ENOENT")
|
|
1555
|
+
return { kind: "missing" };
|
|
1556
|
+
return { kind: "malformed" };
|
|
1557
|
+
}
|
|
1558
|
+
const sha = raw.trim();
|
|
1559
|
+
return isValidBaseSha(sha) ? { kind: "valid", sha } : { kind: "malformed" };
|
|
1560
|
+
}
|
|
1486
1561
|
async function ensureExcluded(cwd) {
|
|
1487
1562
|
const excludePath = path4.resolve(cwd, EXCLUDE_RELPATH);
|
|
1488
1563
|
let current = "";
|
|
@@ -1501,6 +1576,9 @@ async function ensureExcluded(cwd) {
|
|
|
1501
1576
|
await fs3.writeFile(excludePath, `${current}${sep}${EXCLUDE_LINE}
|
|
1502
1577
|
`, "utf8");
|
|
1503
1578
|
}
|
|
1579
|
+
function isErrno3(error) {
|
|
1580
|
+
return error instanceof Error && "code" in error;
|
|
1581
|
+
}
|
|
1504
1582
|
|
|
1505
1583
|
// src/commands/clone.ts
|
|
1506
1584
|
var cloneCommand = {
|
|
@@ -1525,12 +1603,7 @@ async function runClone(args, io) {
|
|
|
1525
1603
|
try {
|
|
1526
1604
|
cfg = loadConfig();
|
|
1527
1605
|
destDir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
|
|
1528
|
-
|
|
1529
|
-
await fs4.access(destDir);
|
|
1530
|
-
destPreExisted = true;
|
|
1531
|
-
} catch {
|
|
1532
|
-
destPreExisted = false;
|
|
1533
|
-
}
|
|
1606
|
+
destPreExisted = await pathExists(destDir);
|
|
1534
1607
|
io.out(`Cloning ${slug} into ${destDir} …`);
|
|
1535
1608
|
const res = await apiRaw(cfg, { path: `/apps/${slug}/source-bundle` });
|
|
1536
1609
|
if (res.body === null) {
|
|
@@ -1538,67 +1611,93 @@ async function runClone(args, io) {
|
|
|
1538
1611
|
return 1;
|
|
1539
1612
|
}
|
|
1540
1613
|
const headerSha = res.headers.get(BASE_SHA_HEADER);
|
|
1541
|
-
|
|
1614
|
+
if (headerSha === null || !isValidBaseSha(headerSha)) {
|
|
1615
|
+
io.err("launchpad clone: bot did not return trustworthy source provenance; no files were extracted.");
|
|
1616
|
+
io.err(" Retry later or ask the Launchpad team to verify the bot version.");
|
|
1617
|
+
return 1;
|
|
1618
|
+
}
|
|
1619
|
+
const baseSha = headerSha;
|
|
1542
1620
|
const stats = await extractToDir(res.body, destDir, { stripComponents: 1 });
|
|
1543
1621
|
await initGitRepo({
|
|
1544
1622
|
cwd: destDir,
|
|
1545
1623
|
initialCommitMessage: `Initial baseline from launchpad clone ${slug}`
|
|
1546
1624
|
});
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1625
|
+
const markerFailure = await persistCloneMarker({
|
|
1626
|
+
slug,
|
|
1627
|
+
destDir,
|
|
1628
|
+
destPreExisted,
|
|
1629
|
+
baseSha,
|
|
1630
|
+
io
|
|
1631
|
+
});
|
|
1632
|
+
if (markerFailure !== null)
|
|
1633
|
+
return markerFailure;
|
|
1557
1634
|
io.out("");
|
|
1558
1635
|
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
|
-
}
|
|
1636
|
+
io.out(`Base revision ${baseSha.slice(0, 12)} recorded - \`launchpad pull\` can refresh this clone safely.`);
|
|
1562
1637
|
io.out(`No remote configured — use \`launchpad deploy\` to ship changes back.`);
|
|
1563
1638
|
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;
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
return handleCloneError(error, slug, destPreExisted, io);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async function persistCloneMarker(args) {
|
|
1644
|
+
try {
|
|
1645
|
+
await writeBaseShaMarker(args.destDir, args.baseSha);
|
|
1646
|
+
return null;
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
args.io.err(`launchpad clone: could not record source provenance: ${describe9(error)}`);
|
|
1649
|
+
if (args.destPreExisted) {
|
|
1650
|
+
args.io.err(` Remove the generated files from ${args.destDir}, then retry.`);
|
|
1651
|
+
} else {
|
|
1652
|
+
await cleanupBestEffort(args.slug);
|
|
1653
|
+
args.io.err(" The incomplete clone was removed. Retry the command.");
|
|
1597
1654
|
}
|
|
1598
|
-
io.err(`launchpad clone failed: ${describe9(e)}`);
|
|
1599
1655
|
return 1;
|
|
1600
1656
|
}
|
|
1601
1657
|
}
|
|
1658
|
+
async function handleCloneError(error, slug, destPreExisted, io) {
|
|
1659
|
+
if (isCloneAuthError(error)) {
|
|
1660
|
+
io.err(error.message);
|
|
1661
|
+
} else if (error instanceof NotFoundError) {
|
|
1662
|
+
io.err(`launchpad clone: app "${slug}" not found (or you have no access)`);
|
|
1663
|
+
} else if (error instanceof DestinationNotEmptyError) {
|
|
1664
|
+
io.err(`launchpad clone: ${error.message}`);
|
|
1665
|
+
io.err("(move or delete the existing directory and re-run)");
|
|
1666
|
+
} else if (error instanceof TarballParseError) {
|
|
1667
|
+
io.err(`launchpad clone: malformed tarball from bot: ${error.message}`);
|
|
1668
|
+
if (!destPreExisted)
|
|
1669
|
+
await cleanupBestEffort(slug);
|
|
1670
|
+
} else if (error instanceof PathTraversalError) {
|
|
1671
|
+
io.err(`launchpad clone: refusing to extract - ${error.message}`);
|
|
1672
|
+
if (!destPreExisted)
|
|
1673
|
+
await cleanupBestEffort(slug);
|
|
1674
|
+
} else if (error instanceof GitInitError) {
|
|
1675
|
+
await handleGitInitError(error, slug, destPreExisted, io);
|
|
1676
|
+
} else {
|
|
1677
|
+
io.err(`launchpad clone failed: ${describe9(error)}`);
|
|
1678
|
+
}
|
|
1679
|
+
return 1;
|
|
1680
|
+
}
|
|
1681
|
+
async function handleGitInitError(error, slug, destPreExisted, io) {
|
|
1682
|
+
io.err(`launchpad clone: ${error.message}`);
|
|
1683
|
+
if (destPreExisted)
|
|
1684
|
+
return;
|
|
1685
|
+
await cleanupBestEffort(slug);
|
|
1686
|
+
io.err(" The incomplete clone was removed. Retry the command.");
|
|
1687
|
+
}
|
|
1688
|
+
function isCloneAuthError(error) {
|
|
1689
|
+
return error instanceof UnauthenticatedError || error instanceof ForbiddenError;
|
|
1690
|
+
}
|
|
1691
|
+
async function pathExists(target) {
|
|
1692
|
+
try {
|
|
1693
|
+
await fs4.lstat(target);
|
|
1694
|
+
return true;
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
if (isErrno4(error) && error.code === "ENOENT")
|
|
1697
|
+
return false;
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1602
1701
|
async function cleanupBestEffort(slug) {
|
|
1603
1702
|
const dir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
|
|
1604
1703
|
await fs4.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
@@ -1615,6 +1714,9 @@ function formatBytes(n) {
|
|
|
1615
1714
|
function describe9(e) {
|
|
1616
1715
|
return e instanceof Error ? e.message : String(e);
|
|
1617
1716
|
}
|
|
1717
|
+
function isErrno4(error) {
|
|
1718
|
+
return error instanceof Error && "code" in error;
|
|
1719
|
+
}
|
|
1618
1720
|
|
|
1619
1721
|
// src/commands/create.ts
|
|
1620
1722
|
var createCommand = {
|
|
@@ -1794,7 +1896,7 @@ async function packTarGz(cwd, files) {
|
|
|
1794
1896
|
try {
|
|
1795
1897
|
stat = await fs5.lstat(abs);
|
|
1796
1898
|
} catch (e) {
|
|
1797
|
-
if (
|
|
1899
|
+
if (isErrno5(e) && e.code === "ENOENT")
|
|
1798
1900
|
continue;
|
|
1799
1901
|
throw e;
|
|
1800
1902
|
}
|
|
@@ -1919,13 +2021,14 @@ function findSplit(p, enc) {
|
|
|
1919
2021
|
}
|
|
1920
2022
|
return null;
|
|
1921
2023
|
}
|
|
1922
|
-
function
|
|
2024
|
+
function isErrno5(e) {
|
|
1923
2025
|
return e instanceof Error && typeof e.code === "string";
|
|
1924
2026
|
}
|
|
1925
2027
|
|
|
1926
2028
|
// src/bundle/upload.ts
|
|
1927
2029
|
var ACCESS_RECONCILE_EXACT_SHA_V1 = "access-reconcile-exact-sha-v1";
|
|
1928
|
-
|
|
2030
|
+
var SOURCE_PROVENANCE_V1 = "source-provenance-v1";
|
|
2031
|
+
async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact, baseSha, overrideStale, allowUnguarded = false) {
|
|
1929
2032
|
const formData = new FormData;
|
|
1930
2033
|
formData.append("manifest", manifestYaml);
|
|
1931
2034
|
const buf = new ArrayBuffer(bundleTarGz.byteLength);
|
|
@@ -1945,15 +2048,17 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1945
2048
|
}
|
|
1946
2049
|
const url = `${cfg.botUrl}/apps/${slug}/deploy/bundle`;
|
|
1947
2050
|
const { accessToken } = await getValidAccessToken(cfg.sessionPath);
|
|
1948
|
-
const headers = {
|
|
2051
|
+
const headers = cliVersionHeaders({
|
|
1949
2052
|
authorization: `Bearer ${accessToken}`,
|
|
1950
2053
|
accept: "application/json",
|
|
1951
|
-
"x-launchpad-capabilities": ACCESS_RECONCILE_EXACT_SHA_V1
|
|
1952
|
-
};
|
|
2054
|
+
"x-launchpad-capabilities": `${ACCESS_RECONCILE_EXACT_SHA_V1},${SOURCE_PROVENANCE_V1}`
|
|
2055
|
+
});
|
|
1953
2056
|
if (baseSha)
|
|
1954
2057
|
headers["x-launchpad-base-sha"] = baseSha;
|
|
1955
2058
|
if (overrideStale)
|
|
1956
2059
|
headers["x-launchpad-stale-override"] = overrideStale;
|
|
2060
|
+
if (allowUnguarded)
|
|
2061
|
+
headers["x-launchpad-allow-unguarded"] = "true";
|
|
1957
2062
|
let res;
|
|
1958
2063
|
try {
|
|
1959
2064
|
res = await fetch(url, { method: "POST", headers, body: formData });
|
|
@@ -2076,7 +2181,9 @@ function deployBundleError(envelope, body) {
|
|
|
2076
2181
|
...manifestOnly === undefined ? {} : { manifest_only: manifestOnly },
|
|
2077
2182
|
...typeof body.head_sha === "string" ? { head_sha: body.head_sha } : {},
|
|
2078
2183
|
...paths === undefined ? {} : { paths },
|
|
2079
|
-
...typeof body.reason === "string" ? { reason: body.reason } : {}
|
|
2184
|
+
...typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
2185
|
+
...typeof body.minimum === "string" ? { minimum: body.minimum } : {},
|
|
2186
|
+
...typeof body.yours === "string" ? { yours: body.yours } : {}
|
|
2080
2187
|
};
|
|
2081
2188
|
}
|
|
2082
2189
|
function readStringArray(value) {
|
|
@@ -2104,6 +2211,7 @@ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2104
2211
|
import { join as join4, relative as relative2, sep } from "node:path";
|
|
2105
2212
|
var DEFAULT_IGNORE = [
|
|
2106
2213
|
".git",
|
|
2214
|
+
".launchpad",
|
|
2107
2215
|
"node_modules",
|
|
2108
2216
|
"dist",
|
|
2109
2217
|
"build",
|
|
@@ -2467,9 +2575,9 @@ var AccessSchema = z.object({
|
|
|
2467
2575
|
allowed_entra_group: z.string().min(1).optional(),
|
|
2468
2576
|
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
2577
|
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 =
|
|
2578
|
+
}).strict().superRefine((access, ctx) => {
|
|
2579
|
+
const hasSingular = access.allowed_entra_group !== undefined;
|
|
2580
|
+
const hasPlural = access.allowed_entra_groups !== undefined;
|
|
2473
2581
|
if (!hasSingular && !hasPlural) {
|
|
2474
2582
|
ctx.addIssue({
|
|
2475
2583
|
code: z.ZodIssueCode.custom,
|
|
@@ -2485,11 +2593,11 @@ var AccessSchema = z.object({
|
|
|
2485
2593
|
});
|
|
2486
2594
|
}
|
|
2487
2595
|
});
|
|
2488
|
-
function allowedEntraGroups(
|
|
2489
|
-
if (
|
|
2490
|
-
return
|
|
2596
|
+
function allowedEntraGroups(access) {
|
|
2597
|
+
if (access.allowed_entra_groups !== undefined) {
|
|
2598
|
+
return access.allowed_entra_groups;
|
|
2491
2599
|
}
|
|
2492
|
-
return [
|
|
2600
|
+
return [access.allowed_entra_group];
|
|
2493
2601
|
}
|
|
2494
2602
|
var ContainerSchema = z.object({
|
|
2495
2603
|
name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
|
|
@@ -2957,6 +3065,13 @@ function checkDenyList(path7) {
|
|
|
2957
3065
|
if (segments.includes(".claude")) {
|
|
2958
3066
|
return { path: path7, rule: "claude-dir", message: `'${path7}' is under a .claude/ directory (AI-workspace state) — never shippable` };
|
|
2959
3067
|
}
|
|
3068
|
+
if (segments.includes(".launchpad")) {
|
|
3069
|
+
return {
|
|
3070
|
+
path: path7,
|
|
3071
|
+
rule: "launchpad-dir",
|
|
3072
|
+
message: `'${path7}' is under .launchpad/ (clone-local state) - never shippable`
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
2960
3075
|
if (segments.includes(".git")) {
|
|
2961
3076
|
return { path: path7, rule: "git-dir", message: `'${path7}' is under .git/ — never shippable` };
|
|
2962
3077
|
}
|
|
@@ -4006,7 +4121,7 @@ ${lines}`
|
|
|
4006
4121
|
|
|
4007
4122
|
// src/bundle/orchestrate.ts
|
|
4008
4123
|
async function bundleAndDeploy(args) {
|
|
4009
|
-
const { cfg, cwd, slug, overrideStale } = args;
|
|
4124
|
+
const { cfg, cwd, slug, overrideStale, allowUnguarded = false } = args;
|
|
4010
4125
|
const manifestPath = join7(cwd, "launchpad.yaml");
|
|
4011
4126
|
let manifestYaml;
|
|
4012
4127
|
try {
|
|
@@ -4052,7 +4167,7 @@ async function bundleAndDeploy(args) {
|
|
|
4052
4167
|
}
|
|
4053
4168
|
const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
|
|
4054
4169
|
const baseSha = await readBaseShaMarker(cwd);
|
|
4055
|
-
const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact, baseSha, overrideStale);
|
|
4170
|
+
const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact, baseSha, overrideStale, allowUnguarded);
|
|
4056
4171
|
if (uploadResult.kind !== "ok") {
|
|
4057
4172
|
return {
|
|
4058
4173
|
kind: "upload-error",
|
|
@@ -6319,6 +6434,7 @@ function parseDeployFlags(args) {
|
|
|
6319
6434
|
let allowStale = false;
|
|
6320
6435
|
let rebaseOntoHead = false;
|
|
6321
6436
|
let overrideStale = null;
|
|
6437
|
+
let allowUnguarded = false;
|
|
6322
6438
|
let i = 0;
|
|
6323
6439
|
while (i < args.length) {
|
|
6324
6440
|
const a = args[i] ?? "";
|
|
@@ -6451,6 +6567,11 @@ function parseDeployFlags(args) {
|
|
|
6451
6567
|
i += 1;
|
|
6452
6568
|
continue;
|
|
6453
6569
|
}
|
|
6570
|
+
if (a === "--allow-unguarded") {
|
|
6571
|
+
allowUnguarded = true;
|
|
6572
|
+
i += 1;
|
|
6573
|
+
continue;
|
|
6574
|
+
}
|
|
6454
6575
|
if (a === "--rebase-onto-head") {
|
|
6455
6576
|
rebaseOntoHead = true;
|
|
6456
6577
|
i += 1;
|
|
@@ -6532,8 +6653,8 @@ function parseDeployFlags(args) {
|
|
|
6532
6653
|
if (modeFlagsSeen > 1) {
|
|
6533
6654
|
return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
|
|
6534
6655
|
}
|
|
6535
|
-
if (mode !== "content" && (allowStale || rebaseOntoHead || overrideStale !== null)) {
|
|
6536
|
-
return "--allow-stale / --rebase-onto-head / --override-stale are only valid for a content deploy";
|
|
6656
|
+
if (mode !== "content" && (allowStale || rebaseOntoHead || overrideStale !== null || allowUnguarded)) {
|
|
6657
|
+
return "--allow-stale / --rebase-onto-head / --override-stale / --allow-unguarded are only valid for a content deploy";
|
|
6537
6658
|
}
|
|
6538
6659
|
if (rebaseOntoHead && overrideStale !== null) {
|
|
6539
6660
|
return "--rebase-onto-head and --override-stale are mutually exclusive — re-stamp to head, or override in place, not both";
|
|
@@ -6626,7 +6747,14 @@ function parseDeployFlags(args) {
|
|
|
6626
6747
|
return `invalid slug "${slug}" — expected ${SLUG_RE4.source}`;
|
|
6627
6748
|
}
|
|
6628
6749
|
return {
|
|
6629
|
-
mode: {
|
|
6750
|
+
mode: {
|
|
6751
|
+
kind: "content",
|
|
6752
|
+
slug,
|
|
6753
|
+
allowStale,
|
|
6754
|
+
rebaseOntoHead,
|
|
6755
|
+
overrideStale,
|
|
6756
|
+
allowUnguarded
|
|
6757
|
+
},
|
|
6630
6758
|
message,
|
|
6631
6759
|
timeoutSeconds
|
|
6632
6760
|
};
|
|
@@ -7599,7 +7727,12 @@ async function runDeploy(args, io) {
|
|
|
7599
7727
|
const cwd = process.cwd();
|
|
7600
7728
|
const manifestPath = path8.join(cwd, "launchpad.yaml");
|
|
7601
7729
|
if (existsSync6(manifestPath)) {
|
|
7602
|
-
const contentMode = flags.mode.kind === "content" ? flags.mode : {
|
|
7730
|
+
const contentMode = flags.mode.kind === "content" ? flags.mode : {
|
|
7731
|
+
allowStale: false,
|
|
7732
|
+
rebaseOntoHead: false,
|
|
7733
|
+
overrideStale: null,
|
|
7734
|
+
allowUnguarded: false
|
|
7735
|
+
};
|
|
7603
7736
|
return runModelADeploy({
|
|
7604
7737
|
cwd,
|
|
7605
7738
|
manifestPath,
|
|
@@ -7607,9 +7740,14 @@ async function runDeploy(args, io) {
|
|
|
7607
7740
|
io,
|
|
7608
7741
|
allowStale: contentMode.allowStale,
|
|
7609
7742
|
rebaseOntoHead: contentMode.rebaseOntoHead,
|
|
7610
|
-
overrideStale: contentMode.overrideStale
|
|
7743
|
+
overrideStale: contentMode.overrideStale,
|
|
7744
|
+
allowUnguarded: contentMode.allowUnguarded
|
|
7611
7745
|
});
|
|
7612
7746
|
}
|
|
7747
|
+
if (flags.mode.kind === "content" && flags.mode.allowUnguarded) {
|
|
7748
|
+
io.err("launchpad deploy: --allow-unguarded requires launchpad.yaml (Model A); legacy content deploys do not support this override.");
|
|
7749
|
+
return 64;
|
|
7750
|
+
}
|
|
7613
7751
|
const parsed = parseArgs3(args);
|
|
7614
7752
|
if (parsed === null) {
|
|
7615
7753
|
io.err("launchpad deploy: malformed argv (run with --help)");
|
|
@@ -7690,6 +7828,10 @@ async function runDeploy(args, io) {
|
|
|
7690
7828
|
}
|
|
7691
7829
|
return 0;
|
|
7692
7830
|
} catch (e) {
|
|
7831
|
+
if (e instanceof UpgradeRequiredError) {
|
|
7832
|
+
io.err(e.message);
|
|
7833
|
+
return EXIT_CLI_UPGRADE_REQUIRED;
|
|
7834
|
+
}
|
|
7693
7835
|
if (e instanceof UnauthenticatedError) {
|
|
7694
7836
|
io.err(e.message);
|
|
7695
7837
|
return 1;
|
|
@@ -7723,7 +7865,7 @@ async function runDeploy(args, io) {
|
|
|
7723
7865
|
}
|
|
7724
7866
|
}
|
|
7725
7867
|
function deployUsage() {
|
|
7726
|
-
return `usage: launchpad deploy [--message <text>] [--slug <slug>]
|
|
7868
|
+
return `usage: launchpad deploy [--message <text>] [--slug <slug>] [--allow-unguarded]
|
|
7727
7869
|
` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
|
|
7728
7870
|
` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
7729
7871
|
` + `provisioning modes:
|
|
@@ -7851,8 +7993,9 @@ function surfaceStaleBlock(body, io) {
|
|
|
7851
7993
|
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
7994
|
}
|
|
7853
7995
|
io.err("");
|
|
7854
|
-
io.err(" To recover
|
|
7855
|
-
io.err(" launchpad
|
|
7996
|
+
io.err(" To recover safely:");
|
|
7997
|
+
io.err(" launchpad pull");
|
|
7998
|
+
io.err(" launchpad deploy");
|
|
7856
7999
|
if (head !== null) {
|
|
7857
8000
|
io.err(" Or, to deliberately overwrite them in place:");
|
|
7858
8001
|
io.err(` launchpad deploy --override-stale ${head.slice(0, 8)}`);
|
|
@@ -7970,12 +8113,26 @@ async function runModelADeploy(args) {
|
|
|
7970
8113
|
const deployBaseline = noVerify ? null : await readDeploymentBaseline(realCommitWaitDeps(cfg), slug);
|
|
7971
8114
|
let result;
|
|
7972
8115
|
try {
|
|
8116
|
+
if (args.allowUnguarded) {
|
|
8117
|
+
io.err("warning: --allow-unguarded disables clone staleness protection for this deploy.");
|
|
8118
|
+
}
|
|
7973
8119
|
result = await bundleAndDeploy({
|
|
7974
8120
|
cwd,
|
|
7975
8121
|
cfg,
|
|
7976
8122
|
slug,
|
|
7977
|
-
overrideStale: args.overrideStale
|
|
8123
|
+
overrideStale: args.overrideStale,
|
|
8124
|
+
allowUnguarded: args.allowUnguarded
|
|
7978
8125
|
});
|
|
8126
|
+
if (result.kind === "upload-error" && result.status === 409 && result.body.error === "main_advanced") {
|
|
8127
|
+
io.out("Managed main advanced during validation; recomputing and retrying once ...");
|
|
8128
|
+
result = await bundleAndDeploy({
|
|
8129
|
+
cwd,
|
|
8130
|
+
cfg,
|
|
8131
|
+
slug,
|
|
8132
|
+
overrideStale: args.overrideStale,
|
|
8133
|
+
allowUnguarded: args.allowUnguarded
|
|
8134
|
+
});
|
|
8135
|
+
}
|
|
7979
8136
|
if (args.rebaseOntoHead && result.kind === "upload-error" && result.status === 409 && isStaleBlock(result.body)) {
|
|
7980
8137
|
const head = result.body.head_sha;
|
|
7981
8138
|
if (typeof head === "string" && isValidBaseSha(head)) {
|
|
@@ -7983,11 +8140,15 @@ async function runModelADeploy(args) {
|
|
|
7983
8140
|
io.out(`Re-stamped base revision to current head ${head.slice(0, 12)}; retrying deploy …`);
|
|
7984
8141
|
result = await bundleAndDeploy({ cwd, cfg, slug });
|
|
7985
8142
|
} else {
|
|
7986
|
-
io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block;
|
|
8143
|
+
io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block; run `launchpad pull` and retry.");
|
|
7987
8144
|
return 1;
|
|
7988
8145
|
}
|
|
7989
8146
|
}
|
|
7990
8147
|
} catch (e) {
|
|
8148
|
+
if (e instanceof UpgradeRequiredError) {
|
|
8149
|
+
io.err(e.message);
|
|
8150
|
+
return EXIT_CLI_UPGRADE_REQUIRED;
|
|
8151
|
+
}
|
|
7991
8152
|
if (e instanceof UnauthenticatedError) {
|
|
7992
8153
|
io.err(`launchpad deploy: ${e.message}`);
|
|
7993
8154
|
io.err(" run `launchpad login` to refresh your session.");
|
|
@@ -8015,6 +8176,10 @@ async function runModelADeploy(args) {
|
|
|
8015
8176
|
case "upload-error": {
|
|
8016
8177
|
const body = result.body;
|
|
8017
8178
|
const errorCode = body.error;
|
|
8179
|
+
if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
|
|
8180
|
+
io.err(upgradeRequiredMessage(body.minimum, body.yours));
|
|
8181
|
+
return EXIT_CLI_UPGRADE_REQUIRED;
|
|
8182
|
+
}
|
|
8018
8183
|
if (result.status === 409 && errorCode === "slug_in_flight") {
|
|
8019
8184
|
io.err(`launchpad deploy: slug "${slug}" is already provisioning.`);
|
|
8020
8185
|
if (typeof body.message === "string") {
|
|
@@ -8034,6 +8199,12 @@ async function runModelADeploy(args) {
|
|
|
8034
8199
|
surfaceStaleBlock(body, io);
|
|
8035
8200
|
return 1;
|
|
8036
8201
|
}
|
|
8202
|
+
if (result.status === 409 && errorCode === "untrusted_base") {
|
|
8203
|
+
io.err("launchpad deploy: this working tree has no trusted source baseline.");
|
|
8204
|
+
io.err(" Run `launchpad pull --bootstrap` to establish provenance for an unchanged legacy clone,");
|
|
8205
|
+
io.err(" use a fresh Launchpad clone, or pass --allow-unguarded only for an intentional non-clone deploy.");
|
|
8206
|
+
return 1;
|
|
8207
|
+
}
|
|
8037
8208
|
if (result.status === 409 && errorCode === "access_drift") {
|
|
8038
8209
|
return handleAccessDrift({
|
|
8039
8210
|
slug,
|
|
@@ -9950,16 +10121,16 @@ function buildManifest(inputs, detected) {
|
|
|
9950
10121
|
metadata.description = inputs.description;
|
|
9951
10122
|
}
|
|
9952
10123
|
const deployment = { type: inputs.type };
|
|
9953
|
-
const
|
|
10124
|
+
const access = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
|
|
9954
10125
|
if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
|
|
9955
|
-
|
|
10126
|
+
access.session_duration = inputs.sessionDuration;
|
|
9956
10127
|
}
|
|
9957
10128
|
const manifest = {
|
|
9958
10129
|
apiVersion: "launchpad.m-kopa.us/v1alpha1",
|
|
9959
10130
|
kind: "App",
|
|
9960
10131
|
metadata,
|
|
9961
10132
|
deployment,
|
|
9962
|
-
access
|
|
10133
|
+
access
|
|
9963
10134
|
};
|
|
9964
10135
|
const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
|
|
9965
10136
|
if (resolvedAuth === "gateway") {
|
|
@@ -11007,10 +11178,474 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
|
11007
11178
|
return apiJson(cfg, { path: path12 }, fetcher);
|
|
11008
11179
|
}
|
|
11009
11180
|
|
|
11181
|
+
// src/pull/source-sync.ts
|
|
11182
|
+
import * as fs6 from "node:fs/promises";
|
|
11183
|
+
import * as path12 from "node:path";
|
|
11184
|
+
import { constants as fsConstants } from "node:fs";
|
|
11185
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
11186
|
+
import { execFile } from "node:child_process";
|
|
11187
|
+
var CONFLICT_REPORT = `${MARKER_DIR}/pull-conflicts.json`;
|
|
11188
|
+
var CONFLICT_DIR = `${MARKER_DIR}/pull-conflicts`;
|
|
11189
|
+
var DELETE_NOTE = new TextEncoder().encode(`Upstream deleted this file.
|
|
11190
|
+
`);
|
|
11191
|
+
var UNSUPPORTED_NOTE = new TextEncoder().encode(`Upstream or baseline uses a Git entry type that launchpad pull cannot reconcile.
|
|
11192
|
+
`);
|
|
11193
|
+
async function runSourceSync(args) {
|
|
11194
|
+
await assertSafeCloneRoot(args.cwd);
|
|
11195
|
+
const marker = await readBaseShaMarkerState(args.cwd);
|
|
11196
|
+
if (marker.kind !== "valid") {
|
|
11197
|
+
if (args.bootstrap === true)
|
|
11198
|
+
return bootstrapProvenance(args);
|
|
11199
|
+
args.io.err(`launchpad pull: this directory has ${marker.kind} source provenance and cannot be reconciled safely.`);
|
|
11200
|
+
args.io.err(" Preserve this directory, then clone the app into a separate empty parent directory to recover a trusted baseline.");
|
|
11201
|
+
return 1;
|
|
11202
|
+
}
|
|
11203
|
+
const compare = await apiJson(args.cfg, {
|
|
11204
|
+
path: `/apps/${args.slug}/source/compare?base=${marker.sha}`
|
|
11205
|
+
});
|
|
11206
|
+
if (compare.baseSha !== marker.sha) {
|
|
11207
|
+
throw new Error("source compare returned a different base SHA");
|
|
11208
|
+
}
|
|
11209
|
+
const paths = [
|
|
11210
|
+
...new Set([
|
|
11211
|
+
...compare.changed,
|
|
11212
|
+
...compare.deleted,
|
|
11213
|
+
...compare.unsupported ?? []
|
|
11214
|
+
])
|
|
11215
|
+
].sort();
|
|
11216
|
+
if (paths.length === 0) {
|
|
11217
|
+
await cleanupConflictState(args.cwd);
|
|
11218
|
+
await writeBaseShaMarker(args.cwd, compare.headSha);
|
|
11219
|
+
args.io.out(`Already current at ${compare.headSha.slice(0, 12)}.`);
|
|
11220
|
+
return 0;
|
|
11221
|
+
}
|
|
11222
|
+
const stageRoot = path12.resolve(args.cwd, MARKER_DIR, `pull-stage-${process.pid}-${crypto.randomUUID()}`);
|
|
11223
|
+
const baseDir = path12.join(stageRoot, "base");
|
|
11224
|
+
const headDir = path12.join(stageRoot, "head");
|
|
11225
|
+
await fs6.mkdir(baseDir, { recursive: true });
|
|
11226
|
+
await fs6.mkdir(headDir, { recursive: true });
|
|
11227
|
+
try {
|
|
11228
|
+
await Promise.all([
|
|
11229
|
+
fetchSnapshot(args.cfg, args.slug, compare.baseSha, baseDir),
|
|
11230
|
+
fetchSnapshot(args.cfg, args.slug, compare.headSha, headDir)
|
|
11231
|
+
]);
|
|
11232
|
+
return await reconcile({ ...args, compare, paths, baseDir, headDir });
|
|
11233
|
+
} finally {
|
|
11234
|
+
await fs6.rm(stageRoot, { recursive: true, force: true }).catch(() => {
|
|
11235
|
+
return;
|
|
11236
|
+
});
|
|
11237
|
+
}
|
|
11238
|
+
}
|
|
11239
|
+
async function fetchSnapshot(cfg, slug, sha, destination) {
|
|
11240
|
+
await fetchSnapshotRef(cfg, slug, sha, destination, sha);
|
|
11241
|
+
}
|
|
11242
|
+
async function fetchSnapshotRef(cfg, slug, ref, destination, expectedSha) {
|
|
11243
|
+
const response = await apiRaw(cfg, {
|
|
11244
|
+
path: `/apps/${slug}/source-bundle?at=${ref}`
|
|
11245
|
+
});
|
|
11246
|
+
const pinnedSha = response.headers.get(BASE_SHA_HEADER);
|
|
11247
|
+
if (pinnedSha === null || !/^[0-9a-f]{40}$/.test(pinnedSha) || expectedSha !== undefined && pinnedSha !== expectedSha || response.body === null) {
|
|
11248
|
+
throw new Error(`source snapshot provenance mismatch at ${ref}`);
|
|
11249
|
+
}
|
|
11250
|
+
await extractToDir(response.body, destination, {
|
|
11251
|
+
stripComponents: 1,
|
|
11252
|
+
allowNonEmpty: true
|
|
11253
|
+
});
|
|
11254
|
+
return pinnedSha;
|
|
11255
|
+
}
|
|
11256
|
+
async function bootstrapProvenance(args) {
|
|
11257
|
+
let status;
|
|
11258
|
+
let tracked;
|
|
11259
|
+
try {
|
|
11260
|
+
status = await gitOutput(args.cwd, [
|
|
11261
|
+
"status",
|
|
11262
|
+
"--porcelain=v1",
|
|
11263
|
+
"-z",
|
|
11264
|
+
"--untracked-files=all"
|
|
11265
|
+
]);
|
|
11266
|
+
tracked = splitNull(await gitOutput(args.cwd, ["ls-files", "-z"]));
|
|
11267
|
+
} catch {
|
|
11268
|
+
args.io.err("launchpad pull: --bootstrap requires a legacy Launchpad clone with its synthetic Git baseline.");
|
|
11269
|
+
return 1;
|
|
11270
|
+
}
|
|
11271
|
+
if (status.length > 0) {
|
|
11272
|
+
args.io.err("launchpad pull: --bootstrap refused because the legacy clone has local changes or untracked files.");
|
|
11273
|
+
args.io.err(" Preserve this directory and obtain a separate current clone for comparison.");
|
|
11274
|
+
return 1;
|
|
11275
|
+
}
|
|
11276
|
+
const stageRoot = path12.resolve(args.cwd, MARKER_DIR, `pull-bootstrap-${process.pid}-${crypto.randomUUID()}`);
|
|
11277
|
+
await fs6.mkdir(stageRoot, { recursive: true });
|
|
11278
|
+
try {
|
|
11279
|
+
const headSha = await fetchSnapshotRef(args.cfg, args.slug, "main", stageRoot);
|
|
11280
|
+
const headPaths = await listFiles(stageRoot);
|
|
11281
|
+
if (!await treesEqual(args.cwd, tracked, stageRoot, headPaths)) {
|
|
11282
|
+
args.io.err("launchpad pull: --bootstrap could not prove this legacy baseline matches current managed main.");
|
|
11283
|
+
args.io.err(" The directory was not changed. Preserve it and obtain a separate current clone.");
|
|
11284
|
+
return 1;
|
|
11285
|
+
}
|
|
11286
|
+
await writeBaseShaMarker(args.cwd, headSha);
|
|
11287
|
+
args.io.out(`Trusted source provenance established at ${headSha.slice(0, 12)}.`);
|
|
11288
|
+
return 0;
|
|
11289
|
+
} finally {
|
|
11290
|
+
await fs6.rm(stageRoot, { recursive: true, force: true }).catch(() => {
|
|
11291
|
+
return;
|
|
11292
|
+
});
|
|
11293
|
+
}
|
|
11294
|
+
}
|
|
11295
|
+
async function treesEqual(cwd, tracked, headDir, headPaths) {
|
|
11296
|
+
if (tracked.length !== headPaths.length)
|
|
11297
|
+
return false;
|
|
11298
|
+
const expected = new Set(headPaths);
|
|
11299
|
+
for (const relative5 of tracked) {
|
|
11300
|
+
if (!expected.has(relative5))
|
|
11301
|
+
return false;
|
|
11302
|
+
if (!equal(await readEntry(path12.resolve(cwd, relative5)), await readEntry(path12.resolve(headDir, relative5)))) {
|
|
11303
|
+
return false;
|
|
11304
|
+
}
|
|
11305
|
+
}
|
|
11306
|
+
return true;
|
|
11307
|
+
}
|
|
11308
|
+
async function listFiles(root, relative5 = "") {
|
|
11309
|
+
const files = [];
|
|
11310
|
+
const entries = await fs6.readdir(path12.resolve(root, relative5), {
|
|
11311
|
+
withFileTypes: true
|
|
11312
|
+
});
|
|
11313
|
+
for (const entry of entries) {
|
|
11314
|
+
const child = relative5.length === 0 ? entry.name : `${relative5}/${entry.name}`;
|
|
11315
|
+
if (entry.isDirectory())
|
|
11316
|
+
files.push(...await listFiles(root, child));
|
|
11317
|
+
else if (entry.isFile())
|
|
11318
|
+
files.push(child);
|
|
11319
|
+
}
|
|
11320
|
+
return files.sort();
|
|
11321
|
+
}
|
|
11322
|
+
function gitOutput(cwd, args) {
|
|
11323
|
+
return new Promise((resolve12, reject) => {
|
|
11324
|
+
execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
|
|
11325
|
+
if (error !== null)
|
|
11326
|
+
reject(error);
|
|
11327
|
+
else
|
|
11328
|
+
resolve12(stdout);
|
|
11329
|
+
});
|
|
11330
|
+
});
|
|
11331
|
+
}
|
|
11332
|
+
function splitNull(value) {
|
|
11333
|
+
return value.split("\x00").filter((entry) => entry.length > 0).sort();
|
|
11334
|
+
}
|
|
11335
|
+
async function reconcile(args) {
|
|
11336
|
+
const removals = await cleanAncestorRemovals(args);
|
|
11337
|
+
const planned = await Promise.all(args.paths.map((relative5) => planPath(args, relative5, removals)));
|
|
11338
|
+
const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
|
|
11339
|
+
if (conflicts.length > 0) {
|
|
11340
|
+
const report = {
|
|
11341
|
+
version: 1,
|
|
11342
|
+
slug: args.slug,
|
|
11343
|
+
baseSha: args.compare.baseSha,
|
|
11344
|
+
headSha: args.compare.headSha,
|
|
11345
|
+
conflicts
|
|
11346
|
+
};
|
|
11347
|
+
await writeJsonAtomic(args.cwd, CONFLICT_REPORT, report);
|
|
11348
|
+
await resetConflictArtifacts(args.cwd);
|
|
11349
|
+
for (const entry of planned) {
|
|
11350
|
+
if (entry.conflict !== undefined) {
|
|
11351
|
+
await writeConflictArtifact(args.cwd, entry);
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
}
|
|
11355
|
+
let applied = 0;
|
|
11356
|
+
for (const entry of planned) {
|
|
11357
|
+
if (!entry.apply)
|
|
11358
|
+
continue;
|
|
11359
|
+
await applyUpstream(args.cwd, entry);
|
|
11360
|
+
applied += 1;
|
|
11361
|
+
}
|
|
11362
|
+
if (conflicts.length > 0) {
|
|
11363
|
+
args.io.err(`launchpad pull: ${conflicts.length} conflict(s); ${applied} clean upstream change(s) applied.`);
|
|
11364
|
+
for (const conflict of conflicts.slice(0, 20)) {
|
|
11365
|
+
args.io.err(` - ${conflict.path}: ${conflict.reason}`);
|
|
11366
|
+
args.io.err(` upstream copy: ${conflict.artifact}`);
|
|
11367
|
+
}
|
|
11368
|
+
if (conflicts.length > 20) {
|
|
11369
|
+
args.io.err(` ... and ${conflicts.length - 20} more in ${CONFLICT_REPORT}`);
|
|
11370
|
+
}
|
|
11371
|
+
args.io.err("Resolve each original path using its upstream artifact, then run `launchpad pull` again.");
|
|
11372
|
+
return 1;
|
|
11373
|
+
}
|
|
11374
|
+
await cleanupConflictState(args.cwd);
|
|
11375
|
+
await writeBaseShaMarker(args.cwd, args.compare.headSha);
|
|
11376
|
+
args.io.out(`Updated ${applied} path(s) to managed main ${args.compare.headSha.slice(0, 12)}; local-only work was preserved.`);
|
|
11377
|
+
return 0;
|
|
11378
|
+
}
|
|
11379
|
+
async function cleanAncestorRemovals(args) {
|
|
11380
|
+
const removals = new Set;
|
|
11381
|
+
for (const relative5 of args.compare.deleted) {
|
|
11382
|
+
assertSafePath(relative5);
|
|
11383
|
+
const checked = await checkParents(args.cwd, relative5, removals);
|
|
11384
|
+
if (checked.kind === "blocked")
|
|
11385
|
+
continue;
|
|
11386
|
+
const base = await readEntry(path12.resolve(args.baseDir, relative5));
|
|
11387
|
+
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11388
|
+
if (local.kind !== "missing" && cleanLocal(base, local))
|
|
11389
|
+
removals.add(relative5);
|
|
11390
|
+
}
|
|
11391
|
+
return removals;
|
|
11392
|
+
}
|
|
11393
|
+
async function planPath(args, relative5, removals) {
|
|
11394
|
+
assertSafePath(relative5);
|
|
11395
|
+
const checked = await checkParents(args.cwd, relative5, removals);
|
|
11396
|
+
const base = await readEntry(path12.resolve(args.baseDir, relative5));
|
|
11397
|
+
const upstreamDeleted = args.compare.deleted.includes(relative5);
|
|
11398
|
+
const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative5));
|
|
11399
|
+
const unsupported = (args.compare.unsupported ?? []).includes(relative5);
|
|
11400
|
+
if (checked.kind === "blocked") {
|
|
11401
|
+
return conflictPlan(relative5, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
|
|
11402
|
+
}
|
|
11403
|
+
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11404
|
+
if (unsupported) {
|
|
11405
|
+
return conflictPlan(relative5, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
|
|
11406
|
+
}
|
|
11407
|
+
if (alreadyResolved(local, head, upstreamDeleted)) {
|
|
11408
|
+
return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: false };
|
|
11409
|
+
}
|
|
11410
|
+
if (unsupportedHead(head, upstreamDeleted)) {
|
|
11411
|
+
return conflictPlan(relative5, checked.target, local, head, true, "unsupported upstream file type");
|
|
11412
|
+
}
|
|
11413
|
+
if (cleanLocal(base, local)) {
|
|
11414
|
+
return { relative: relative5, target: checked.target, local, head, upstreamDeleted, apply: true };
|
|
11415
|
+
}
|
|
11416
|
+
return conflictPlan(relative5, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
|
|
11417
|
+
}
|
|
11418
|
+
function conflictPlan(relative5, target, local, head, upstreamDeleted, reason) {
|
|
11419
|
+
const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
|
|
11420
|
+
const digest = createHash3("sha256").update(relative5).digest("hex");
|
|
11421
|
+
return {
|
|
11422
|
+
relative: relative5,
|
|
11423
|
+
target,
|
|
11424
|
+
local,
|
|
11425
|
+
head,
|
|
11426
|
+
upstreamDeleted,
|
|
11427
|
+
apply: false,
|
|
11428
|
+
conflict: {
|
|
11429
|
+
path: relative5,
|
|
11430
|
+
reason,
|
|
11431
|
+
artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
|
|
11432
|
+
upstreamDeleted
|
|
11433
|
+
}
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
function alreadyResolved(local, head, upstreamDeleted) {
|
|
11437
|
+
return equal(local, head) || upstreamDeleted && local.kind === "missing";
|
|
11438
|
+
}
|
|
11439
|
+
function unsupportedHead(head, upstreamDeleted) {
|
|
11440
|
+
return head.kind === "other" || !upstreamDeleted && head.kind === "missing";
|
|
11441
|
+
}
|
|
11442
|
+
function cleanLocal(base, local) {
|
|
11443
|
+
return equal(local, base) || base.kind === "missing" && local.kind === "missing";
|
|
11444
|
+
}
|
|
11445
|
+
async function applyUpstream(cwd, entry) {
|
|
11446
|
+
const checked = await checkParents(cwd, entry.relative, new Set);
|
|
11447
|
+
if (checked.kind === "blocked")
|
|
11448
|
+
throw new Error(checked.reason);
|
|
11449
|
+
if (entry.upstreamDeleted) {
|
|
11450
|
+
if (entry.local.kind !== "missing")
|
|
11451
|
+
await fs6.unlink(checked.target);
|
|
11452
|
+
return;
|
|
11453
|
+
}
|
|
11454
|
+
if (entry.head.kind === "file") {
|
|
11455
|
+
await writeAtomic(cwd, entry.relative, entry.head.bytes, entry.head.mode);
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
function conflictReason(base, local, upstreamDeleted) {
|
|
11459
|
+
if (local.kind === "other")
|
|
11460
|
+
return "local path is not a regular file";
|
|
11461
|
+
if (base.kind === "missing")
|
|
11462
|
+
return "local untracked file collides with an upstream addition";
|
|
11463
|
+
if (local.kind === "missing")
|
|
11464
|
+
return "local deletion conflicts with an upstream change";
|
|
11465
|
+
if (upstreamDeleted)
|
|
11466
|
+
return "locally modified file was deleted upstream";
|
|
11467
|
+
return "local and upstream changes differ";
|
|
11468
|
+
}
|
|
11469
|
+
async function writeConflictArtifact(cwd, entry) {
|
|
11470
|
+
if (entry.conflict === undefined)
|
|
11471
|
+
return;
|
|
11472
|
+
const bytes = entry.head.kind === "file" ? entry.head.bytes : entry.upstreamDeleted ? DELETE_NOTE : UNSUPPORTED_NOTE;
|
|
11473
|
+
await writeAtomic(cwd, entry.conflict.artifact, bytes, entry.head.kind === "file" ? entry.head.mode : 420, true);
|
|
11474
|
+
}
|
|
11475
|
+
async function readEntry(file) {
|
|
11476
|
+
const initial = await lstatIfPresent(file);
|
|
11477
|
+
if (initial === null)
|
|
11478
|
+
return { kind: "missing" };
|
|
11479
|
+
if (!initial.isFile() || initial.isSymbolicLink())
|
|
11480
|
+
return { kind: "other" };
|
|
11481
|
+
const opened = await openWithoutFollowing(file);
|
|
11482
|
+
if (opened.kind !== "open")
|
|
11483
|
+
return opened;
|
|
11484
|
+
try {
|
|
11485
|
+
const stat = await opened.handle.stat();
|
|
11486
|
+
if (!stat.isFile())
|
|
11487
|
+
return { kind: "other" };
|
|
11488
|
+
return {
|
|
11489
|
+
kind: "file",
|
|
11490
|
+
bytes: new Uint8Array(await opened.handle.readFile()),
|
|
11491
|
+
mode: stat.mode & 511
|
|
11492
|
+
};
|
|
11493
|
+
} finally {
|
|
11494
|
+
await opened.handle.close();
|
|
11495
|
+
}
|
|
11496
|
+
}
|
|
11497
|
+
async function lstatIfPresent(file) {
|
|
11498
|
+
try {
|
|
11499
|
+
return await fs6.lstat(file);
|
|
11500
|
+
} catch (error) {
|
|
11501
|
+
if (isErrno6(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
11502
|
+
return null;
|
|
11503
|
+
}
|
|
11504
|
+
throw error;
|
|
11505
|
+
}
|
|
11506
|
+
}
|
|
11507
|
+
async function openWithoutFollowing(file) {
|
|
11508
|
+
try {
|
|
11509
|
+
return {
|
|
11510
|
+
kind: "open",
|
|
11511
|
+
handle: await fs6.open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW)
|
|
11512
|
+
};
|
|
11513
|
+
} catch (error) {
|
|
11514
|
+
if (isErrno6(error) && error.code === "ENOENT")
|
|
11515
|
+
return { kind: "missing" };
|
|
11516
|
+
if (isErrno6(error) && error.code === "ELOOP")
|
|
11517
|
+
return { kind: "other" };
|
|
11518
|
+
throw error;
|
|
11519
|
+
}
|
|
11520
|
+
}
|
|
11521
|
+
function equal(left, right) {
|
|
11522
|
+
if (left.kind !== right.kind)
|
|
11523
|
+
return false;
|
|
11524
|
+
if (left.kind !== "file" || right.kind !== "file")
|
|
11525
|
+
return true;
|
|
11526
|
+
if (left.mode !== right.mode || left.bytes.length !== right.bytes.length)
|
|
11527
|
+
return false;
|
|
11528
|
+
return left.bytes.every((byte, index) => byte === right.bytes[index]);
|
|
11529
|
+
}
|
|
11530
|
+
async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
|
|
11531
|
+
const checked = await checkParents(cwd, relative5, new Set, allowMetadata);
|
|
11532
|
+
if (checked.kind === "blocked")
|
|
11533
|
+
throw new Error(checked.reason);
|
|
11534
|
+
const file = checked.target;
|
|
11535
|
+
await fs6.mkdir(path12.dirname(file), { recursive: true });
|
|
11536
|
+
const rechecked = await checkParents(cwd, relative5, new Set, allowMetadata);
|
|
11537
|
+
if (rechecked.kind === "blocked")
|
|
11538
|
+
throw new Error(rechecked.reason);
|
|
11539
|
+
const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
11540
|
+
try {
|
|
11541
|
+
await fs6.writeFile(temporary, bytes, { mode });
|
|
11542
|
+
await fs6.chmod(temporary, mode);
|
|
11543
|
+
await fs6.rename(temporary, file);
|
|
11544
|
+
} catch (error) {
|
|
11545
|
+
await fs6.rm(temporary, { force: true }).catch(() => {
|
|
11546
|
+
return;
|
|
11547
|
+
});
|
|
11548
|
+
throw error;
|
|
11549
|
+
}
|
|
11550
|
+
}
|
|
11551
|
+
async function writeJsonAtomic(cwd, relative5, value) {
|
|
11552
|
+
await writeAtomic(cwd, relative5, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
11553
|
+
`), 384, true);
|
|
11554
|
+
}
|
|
11555
|
+
async function resetConflictArtifacts(cwd) {
|
|
11556
|
+
const checked = await checkParents(cwd, CONFLICT_DIR, new Set, true);
|
|
11557
|
+
if (checked.kind === "blocked")
|
|
11558
|
+
throw new Error(checked.reason);
|
|
11559
|
+
await fs6.rm(checked.target, { recursive: true, force: true });
|
|
11560
|
+
}
|
|
11561
|
+
async function cleanupConflictState(cwd) {
|
|
11562
|
+
await resetConflictArtifacts(cwd);
|
|
11563
|
+
const report = await checkParents(cwd, CONFLICT_REPORT, new Set, true);
|
|
11564
|
+
if (report.kind === "blocked")
|
|
11565
|
+
throw new Error(report.reason);
|
|
11566
|
+
await fs6.rm(report.target, { force: true });
|
|
11567
|
+
}
|
|
11568
|
+
function assertSafePath(relative5) {
|
|
11569
|
+
if (!isSafeSourcePath(relative5)) {
|
|
11570
|
+
throw new Error(`unsafe source path returned by bot: ${relative5}`);
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
function isSafeSourcePath(relative5) {
|
|
11574
|
+
return isSafeRelativePath(relative5) && !relative5.startsWith(`${MARKER_DIR}/`);
|
|
11575
|
+
}
|
|
11576
|
+
function isSafeRelativePath(relative5) {
|
|
11577
|
+
const normalized = path12.posix.normalize(relative5);
|
|
11578
|
+
return relative5.length > 0 && !relative5.includes("\\") && !path12.isAbsolute(relative5) && normalized === relative5 && normalized !== ".." && !normalized.startsWith("../");
|
|
11579
|
+
}
|
|
11580
|
+
function resolveInsideClone(cwd, relative5) {
|
|
11581
|
+
const root = path12.resolve(cwd);
|
|
11582
|
+
const target = path12.resolve(root, relative5);
|
|
11583
|
+
const fromRoot = path12.relative(root, target);
|
|
11584
|
+
if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
|
|
11585
|
+
throw new Error(`unsafe source path returned by bot: ${relative5}`);
|
|
11586
|
+
}
|
|
11587
|
+
return target;
|
|
11588
|
+
}
|
|
11589
|
+
async function checkParents(cwd, relative5, projectedRemovals, allowMetadata = false) {
|
|
11590
|
+
if (allowMetadata) {
|
|
11591
|
+
if (!isSafeRelativePath(relative5)) {
|
|
11592
|
+
throw new Error(`unsafe metadata path: ${relative5}`);
|
|
11593
|
+
}
|
|
11594
|
+
} else {
|
|
11595
|
+
assertSafePath(relative5);
|
|
11596
|
+
}
|
|
11597
|
+
const target = resolveInsideClone(cwd, relative5);
|
|
11598
|
+
const components = relative5.split("/");
|
|
11599
|
+
let current = path12.resolve(cwd);
|
|
11600
|
+
for (let index = 0;index < components.length - 1; index += 1) {
|
|
11601
|
+
current = path12.join(current, components[index]);
|
|
11602
|
+
const ancestor = components.slice(0, index + 1).join("/");
|
|
11603
|
+
const stat = await lstatIfPresent(current);
|
|
11604
|
+
if (stat === null)
|
|
11605
|
+
return { kind: "safe", target, projectedMissing: true };
|
|
11606
|
+
if (projectedRemovals.has(ancestor)) {
|
|
11607
|
+
return { kind: "safe", target, projectedMissing: true };
|
|
11608
|
+
}
|
|
11609
|
+
if (stat.isSymbolicLink()) {
|
|
11610
|
+
return {
|
|
11611
|
+
kind: "blocked",
|
|
11612
|
+
target,
|
|
11613
|
+
reason: `local parent is a symbolic link: ${ancestor}`
|
|
11614
|
+
};
|
|
11615
|
+
}
|
|
11616
|
+
if (!stat.isDirectory()) {
|
|
11617
|
+
return {
|
|
11618
|
+
kind: "blocked",
|
|
11619
|
+
target,
|
|
11620
|
+
reason: `local parent is not a directory: ${ancestor}`
|
|
11621
|
+
};
|
|
11622
|
+
}
|
|
11623
|
+
}
|
|
11624
|
+
return { kind: "safe", target, projectedMissing: false };
|
|
11625
|
+
}
|
|
11626
|
+
async function assertSafeCloneRoot(cwd) {
|
|
11627
|
+
const root = await fs6.lstat(path12.resolve(cwd));
|
|
11628
|
+
if (!root.isDirectory() || root.isSymbolicLink()) {
|
|
11629
|
+
throw new Error("launchpad pull requires a real clone directory");
|
|
11630
|
+
}
|
|
11631
|
+
try {
|
|
11632
|
+
const metadata = await fs6.lstat(path12.resolve(cwd, MARKER_DIR));
|
|
11633
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
11634
|
+
throw new Error("launchpad pull refuses a non-directory or symlinked .launchpad path");
|
|
11635
|
+
}
|
|
11636
|
+
} catch (error) {
|
|
11637
|
+
if (!isErrno6(error) || error.code !== "ENOENT")
|
|
11638
|
+
throw error;
|
|
11639
|
+
}
|
|
11640
|
+
}
|
|
11641
|
+
function isErrno6(error) {
|
|
11642
|
+
return error instanceof Error && "code" in error;
|
|
11643
|
+
}
|
|
11644
|
+
|
|
11010
11645
|
// src/commands/pull.ts
|
|
11011
11646
|
var pullCommand = {
|
|
11012
11647
|
name: "pull",
|
|
11013
|
-
summary: "
|
|
11648
|
+
summary: "sync upstream source into a clone without overwriting local work",
|
|
11014
11649
|
run: runPull
|
|
11015
11650
|
};
|
|
11016
11651
|
var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
@@ -11023,6 +11658,15 @@ async function runPull(args, io) {
|
|
|
11023
11658
|
}
|
|
11024
11659
|
try {
|
|
11025
11660
|
const cfg = loadConfig();
|
|
11661
|
+
if (!parsed.manifest) {
|
|
11662
|
+
return await runSourceSync({
|
|
11663
|
+
cfg,
|
|
11664
|
+
cwd: process.cwd(),
|
|
11665
|
+
slug: parsed.slug,
|
|
11666
|
+
io,
|
|
11667
|
+
bootstrap: parsed.bootstrap
|
|
11668
|
+
});
|
|
11669
|
+
}
|
|
11026
11670
|
if (parsed.status) {
|
|
11027
11671
|
const noStatusMsg = `launchpad pull: no status block deployed for "${parsed.slug}" yet — ` + `the bot writes it after the first apply.`;
|
|
11028
11672
|
let res;
|
|
@@ -11105,6 +11749,8 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11105
11749
|
let slug = null;
|
|
11106
11750
|
let out = null;
|
|
11107
11751
|
let status = false;
|
|
11752
|
+
let manifest = false;
|
|
11753
|
+
let bootstrap = false;
|
|
11108
11754
|
let i = 0;
|
|
11109
11755
|
while (i < args.length) {
|
|
11110
11756
|
const a = args[i] ?? "";
|
|
@@ -11113,6 +11759,16 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11113
11759
|
i += 1;
|
|
11114
11760
|
continue;
|
|
11115
11761
|
}
|
|
11762
|
+
if (a === "--manifest") {
|
|
11763
|
+
manifest = true;
|
|
11764
|
+
i += 1;
|
|
11765
|
+
continue;
|
|
11766
|
+
}
|
|
11767
|
+
if (a === "--bootstrap") {
|
|
11768
|
+
bootstrap = true;
|
|
11769
|
+
i += 1;
|
|
11770
|
+
continue;
|
|
11771
|
+
}
|
|
11116
11772
|
if (a === "--slug") {
|
|
11117
11773
|
const v = args[i + 1];
|
|
11118
11774
|
if (v === undefined)
|
|
@@ -11151,23 +11807,38 @@ function parseArgs10(args, cwd = process.cwd(), warn) {
|
|
|
11151
11807
|
if (!SLUG_RE11.test(slug)) {
|
|
11152
11808
|
return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
|
|
11153
11809
|
}
|
|
11154
|
-
|
|
11810
|
+
const modeError = validateModeFlags({ manifest, status, out, bootstrap });
|
|
11811
|
+
if (modeError !== null)
|
|
11812
|
+
return modeError;
|
|
11813
|
+
return { slug, out, status, manifest, bootstrap };
|
|
11814
|
+
}
|
|
11815
|
+
function validateModeFlags(args) {
|
|
11816
|
+
if (!args.manifest && args.status)
|
|
11817
|
+
return "--status requires --manifest";
|
|
11818
|
+
if (!args.manifest && args.out !== null)
|
|
11819
|
+
return "--out requires --manifest";
|
|
11820
|
+
if (args.manifest && args.bootstrap) {
|
|
11821
|
+
return "--bootstrap cannot be combined with --manifest";
|
|
11822
|
+
}
|
|
11823
|
+
return null;
|
|
11155
11824
|
}
|
|
11156
11825
|
function printUsage5(io) {
|
|
11157
11826
|
io.err([
|
|
11158
|
-
"usage: launchpad pull [<slug>] [--slug <slug>] [--
|
|
11827
|
+
"usage: launchpad pull [<slug>] [--slug <slug>] [--bootstrap]",
|
|
11828
|
+
" launchpad pull --manifest [<slug>] [--out <path>] [--status]",
|
|
11159
11829
|
"",
|
|
11160
|
-
"
|
|
11161
|
-
"
|
|
11830
|
+
" By default, safely syncs managed source into the current clone.",
|
|
11831
|
+
" --manifest keeps the previous deployed-manifest read behavior.",
|
|
11162
11832
|
"",
|
|
11163
11833
|
" With no slug, it is inferred from the local launchpad.yaml's",
|
|
11164
11834
|
" declared slug first, then from a launchpad-app-<slug>/",
|
|
11165
11835
|
" directory name. Explicit --slug or positional override.",
|
|
11166
11836
|
"",
|
|
11167
|
-
" --status read the role-redacted status block
|
|
11837
|
+
" --status with --manifest, read the role-redacted status block",
|
|
11838
|
+
" (what your",
|
|
11168
11839
|
" role may see) instead of the spec manifest.",
|
|
11169
11840
|
"",
|
|
11170
|
-
"
|
|
11841
|
+
" In manifest mode, omitting --out writes YAML to stdout."
|
|
11171
11842
|
].join(`
|
|
11172
11843
|
`));
|
|
11173
11844
|
}
|
|
@@ -11436,7 +12107,7 @@ var watchCommand = {
|
|
|
11436
12107
|
};
|
|
11437
12108
|
|
|
11438
12109
|
// src/commands/review.ts
|
|
11439
|
-
import * as
|
|
12110
|
+
import * as path13 from "node:path";
|
|
11440
12111
|
var reviewCommand = {
|
|
11441
12112
|
name: "review",
|
|
11442
12113
|
summary: "show the review state for a PR (slug-scoped)",
|
|
@@ -11456,7 +12127,7 @@ async function runReview(args, io) {
|
|
|
11456
12127
|
} else {
|
|
11457
12128
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
11458
12129
|
if (inferred === null) {
|
|
11459
|
-
io.err(`launchpad review: could not infer slug from cwd (${
|
|
12130
|
+
io.err(`launchpad review: could not infer slug from cwd (${path13.basename(process.cwd())});
|
|
11460
12131
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
11461
12132
|
return 64;
|
|
11462
12133
|
}
|
|
@@ -11713,16 +12384,16 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
11713
12384
|
yes: true
|
|
11714
12385
|
}, io, applyDeps);
|
|
11715
12386
|
}
|
|
11716
|
-
function readCurrentManifest(
|
|
11717
|
-
if (!existsSync10(
|
|
12387
|
+
function readCurrentManifest(path14) {
|
|
12388
|
+
if (!existsSync10(path14))
|
|
11718
12389
|
return null;
|
|
11719
12390
|
let raw;
|
|
11720
12391
|
try {
|
|
11721
|
-
raw = readFileSync15(
|
|
12392
|
+
raw = readFileSync15(path14, "utf8");
|
|
11722
12393
|
} catch {
|
|
11723
12394
|
return null;
|
|
11724
12395
|
}
|
|
11725
|
-
const parsed = parseManifest2(raw,
|
|
12396
|
+
const parsed = parseManifest2(raw, path14);
|
|
11726
12397
|
if (parsed.kind !== "ok")
|
|
11727
12398
|
return null;
|
|
11728
12399
|
return summarise(parsed.manifest);
|
|
@@ -12272,23 +12943,23 @@ var CELL_LABEL2 = {
|
|
|
12272
12943
|
not_deployed: "NOT_DEPLOYED"
|
|
12273
12944
|
};
|
|
12274
12945
|
function loadSet(fleetFile, setName, io) {
|
|
12275
|
-
const
|
|
12276
|
-
if (!existsSync12(
|
|
12277
|
-
io.err(`✗ ${
|
|
12946
|
+
const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
12947
|
+
if (!existsSync12(path14)) {
|
|
12948
|
+
io.err(`✗ ${path14}`);
|
|
12278
12949
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
12279
12950
|
return 2;
|
|
12280
12951
|
}
|
|
12281
12952
|
let obj;
|
|
12282
12953
|
try {
|
|
12283
|
-
obj = parseYaml8(readFileSync17(
|
|
12954
|
+
obj = parseYaml8(readFileSync17(path14, "utf8"));
|
|
12284
12955
|
} catch (e) {
|
|
12285
|
-
io.err(`✗ ${
|
|
12956
|
+
io.err(`✗ ${path14}`);
|
|
12286
12957
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12287
12958
|
return 1;
|
|
12288
12959
|
}
|
|
12289
12960
|
const parsed = parseFleetSecretSets(obj);
|
|
12290
12961
|
if (!parsed.ok) {
|
|
12291
|
-
io.err(`✗ ${
|
|
12962
|
+
io.err(`✗ ${path14}`);
|
|
12292
12963
|
io.err(` ${parsed.issues.length} schema issue(s):`);
|
|
12293
12964
|
for (const i of parsed.issues)
|
|
12294
12965
|
io.err(` ${i.path}: ${i.message}`);
|
|
@@ -12297,7 +12968,7 @@ function loadSet(fleetFile, setName, io) {
|
|
|
12297
12968
|
const set = parsed.manifest.secretSets.find((s) => s.name === setName);
|
|
12298
12969
|
if (set === undefined) {
|
|
12299
12970
|
const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
|
|
12300
|
-
io.err(`✗ secret-set "${setName}" not found in ${
|
|
12971
|
+
io.err(`✗ secret-set "${setName}" not found in ${path14}`);
|
|
12301
12972
|
io.err(` declared sets: ${names}`);
|
|
12302
12973
|
return 1;
|
|
12303
12974
|
}
|
|
@@ -12517,7 +13188,7 @@ function setPushExit(e) {
|
|
|
12517
13188
|
|
|
12518
13189
|
// src/commands/secrets-template.ts
|
|
12519
13190
|
import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "node:fs";
|
|
12520
|
-
import { resolve as
|
|
13191
|
+
import { resolve as resolve12 } from "node:path";
|
|
12521
13192
|
async function runSecretsTemplate(args, io) {
|
|
12522
13193
|
const flags = parseFlags4(args);
|
|
12523
13194
|
if (flags.kind === "usage-error") {
|
|
@@ -12525,7 +13196,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
12525
13196
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
12526
13197
|
return 64;
|
|
12527
13198
|
}
|
|
12528
|
-
const manifestPath =
|
|
13199
|
+
const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
12529
13200
|
const result = loadManifest(manifestPath);
|
|
12530
13201
|
const renderResult = renderManifest(result, io);
|
|
12531
13202
|
if (renderResult.kind !== "ok") {
|
|
@@ -12539,7 +13210,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
12539
13210
|
}
|
|
12540
13211
|
return 0;
|
|
12541
13212
|
}
|
|
12542
|
-
const outPath =
|
|
13213
|
+
const outPath = resolve12(process.cwd(), flags.out);
|
|
12543
13214
|
if (existsSync13(outPath) && !flags.force) {
|
|
12544
13215
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
12545
13216
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
@@ -12836,16 +13507,16 @@ function printHelp3(io) {
|
|
|
12836
13507
|
|
|
12837
13508
|
// src/commands/skills.ts
|
|
12838
13509
|
import { fileURLToPath } from "node:url";
|
|
12839
|
-
import { dirname as
|
|
12840
|
-
import { promises as
|
|
13510
|
+
import { dirname as dirname9, join as join15, resolve as resolve13 } from "node:path";
|
|
13511
|
+
import { promises as fs7, existsSync as existsSync14 } from "node:fs";
|
|
12841
13512
|
|
|
12842
13513
|
// src/skills-bundle.ts
|
|
12843
13514
|
import { homedir as homedir2 } from "node:os";
|
|
12844
|
-
import { join as
|
|
13515
|
+
import { join as join14 } from "node:path";
|
|
12845
13516
|
import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
|
|
12846
13517
|
var SKILL_PREFIX = "launchpad-";
|
|
12847
13518
|
function skillsTargetDir() {
|
|
12848
|
-
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
13519
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir2(), ".claude", "skills");
|
|
12849
13520
|
}
|
|
12850
13521
|
function readInstalledSkills() {
|
|
12851
13522
|
try {
|
|
@@ -12855,15 +13526,15 @@ function readInstalledSkills() {
|
|
|
12855
13526
|
return { present: false, version: null };
|
|
12856
13527
|
return {
|
|
12857
13528
|
present: true,
|
|
12858
|
-
version: readSkillVersion(
|
|
13529
|
+
version: readSkillVersion(join14(dir, bundle.name, "SKILL.md"))
|
|
12859
13530
|
};
|
|
12860
13531
|
} catch {
|
|
12861
13532
|
return { present: false, version: null };
|
|
12862
13533
|
}
|
|
12863
13534
|
}
|
|
12864
|
-
function readSkillVersion(
|
|
13535
|
+
function readSkillVersion(path14) {
|
|
12865
13536
|
try {
|
|
12866
|
-
const text = readFileSync18(
|
|
13537
|
+
const text = readFileSync18(path14, "utf8");
|
|
12867
13538
|
const fenceEnd = text.indexOf(`
|
|
12868
13539
|
---`, 4);
|
|
12869
13540
|
const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
|
|
@@ -12940,13 +13611,13 @@ function resolveInstallEnv() {
|
|
|
12940
13611
|
return { bundleDir, userSkillsDir };
|
|
12941
13612
|
}
|
|
12942
13613
|
function defaultBundleDir() {
|
|
12943
|
-
const here =
|
|
13614
|
+
const here = dirname9(fileURLToPath(import.meta.url));
|
|
12944
13615
|
const candidates = [
|
|
12945
|
-
|
|
12946
|
-
|
|
13616
|
+
resolve13(here, "..", "skills"),
|
|
13617
|
+
resolve13(here, "..", "..", "skills")
|
|
12947
13618
|
];
|
|
12948
13619
|
for (const c of candidates) {
|
|
12949
|
-
if (existsSync14(
|
|
13620
|
+
if (existsSync14(join15(c, "launchpad-onboard", "SKILL.md"))) {
|
|
12950
13621
|
return c;
|
|
12951
13622
|
}
|
|
12952
13623
|
}
|
|
@@ -12962,19 +13633,19 @@ async function doInstall(io) {
|
|
|
12962
13633
|
return 1;
|
|
12963
13634
|
}
|
|
12964
13635
|
if (!await isDir(env.userSkillsDir)) {
|
|
12965
|
-
await
|
|
13636
|
+
await fs7.mkdir(env.userSkillsDir, { recursive: true });
|
|
12966
13637
|
io.out(`Created ${env.userSkillsDir}.`);
|
|
12967
13638
|
}
|
|
12968
13639
|
let installed = 0;
|
|
12969
13640
|
for (const skill of BUNDLED_SKILLS) {
|
|
12970
|
-
const src =
|
|
13641
|
+
const src = join15(env.bundleDir, skill);
|
|
12971
13642
|
if (!await isDir(src)) {
|
|
12972
13643
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
12973
13644
|
return 1;
|
|
12974
13645
|
}
|
|
12975
|
-
const dest =
|
|
12976
|
-
await
|
|
12977
|
-
await
|
|
13646
|
+
const dest = join15(env.userSkillsDir, skill);
|
|
13647
|
+
await fs7.rm(dest, { recursive: true, force: true });
|
|
13648
|
+
await fs7.cp(src, dest, { recursive: true });
|
|
12978
13649
|
installed++;
|
|
12979
13650
|
io.out(`✓ ${skill} → ${dest}`);
|
|
12980
13651
|
}
|
|
@@ -12990,15 +13661,15 @@ async function doUninstall(io) {
|
|
|
12990
13661
|
io.out(`launchpad skills uninstall: ${env.userSkillsDir} does not exist — nothing to do.`);
|
|
12991
13662
|
return 0;
|
|
12992
13663
|
}
|
|
12993
|
-
const entries = await
|
|
13664
|
+
const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
12994
13665
|
let removed = 0;
|
|
12995
13666
|
for (const entry of entries) {
|
|
12996
13667
|
if (!entry.isDirectory())
|
|
12997
13668
|
continue;
|
|
12998
13669
|
if (!isBundleManaged(entry.name))
|
|
12999
13670
|
continue;
|
|
13000
|
-
const target =
|
|
13001
|
-
await
|
|
13671
|
+
const target = join15(env.userSkillsDir, entry.name);
|
|
13672
|
+
await fs7.rm(target, { recursive: true, force: true });
|
|
13002
13673
|
removed++;
|
|
13003
13674
|
io.out(`✗ removed ${target}`);
|
|
13004
13675
|
}
|
|
@@ -13012,7 +13683,7 @@ async function doList(io) {
|
|
|
13012
13683
|
io.out("(none — ~/.claude/skills/ does not exist)");
|
|
13013
13684
|
return 0;
|
|
13014
13685
|
}
|
|
13015
|
-
const entries = await
|
|
13686
|
+
const entries = await fs7.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
13016
13687
|
const managedDirs = entries.filter((e) => e.isDirectory() && isBundleManaged(e.name)).map((e) => e.name).sort();
|
|
13017
13688
|
if (managedDirs.length === 0) {
|
|
13018
13689
|
io.out("(none — run `launchpad skills install`)");
|
|
@@ -13020,16 +13691,16 @@ async function doList(io) {
|
|
|
13020
13691
|
}
|
|
13021
13692
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
13022
13693
|
for (const name of managedDirs) {
|
|
13023
|
-
const skillFile =
|
|
13694
|
+
const skillFile = join15(env.userSkillsDir, name, "SKILL.md");
|
|
13024
13695
|
const version = await readVersion(skillFile);
|
|
13025
13696
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
13026
13697
|
}
|
|
13027
13698
|
return 0;
|
|
13028
13699
|
}
|
|
13029
|
-
async function readVersion(
|
|
13700
|
+
async function readVersion(path14) {
|
|
13030
13701
|
let text;
|
|
13031
13702
|
try {
|
|
13032
|
-
text = await
|
|
13703
|
+
text = await fs7.readFile(path14, "utf8");
|
|
13033
13704
|
} catch {
|
|
13034
13705
|
return null;
|
|
13035
13706
|
}
|
|
@@ -13039,9 +13710,9 @@ async function readVersion(path13) {
|
|
|
13039
13710
|
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
13040
13711
|
return m === null ? null : m[1] ?? null;
|
|
13041
13712
|
}
|
|
13042
|
-
async function isDir(
|
|
13713
|
+
async function isDir(path14) {
|
|
13043
13714
|
try {
|
|
13044
|
-
const stat = await
|
|
13715
|
+
const stat = await fs7.stat(path14);
|
|
13045
13716
|
return stat.isDirectory();
|
|
13046
13717
|
} catch {
|
|
13047
13718
|
return false;
|
|
@@ -13052,16 +13723,16 @@ function describe29(e) {
|
|
|
13052
13723
|
}
|
|
13053
13724
|
|
|
13054
13725
|
// src/commands/update.ts
|
|
13055
|
-
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
13726
|
+
import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
|
|
13056
13727
|
import { promisify } from "node:util";
|
|
13057
13728
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13058
|
-
import { dirname as
|
|
13729
|
+
import { dirname as dirname10, resolve as resolve14, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
|
|
13059
13730
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
13060
13731
|
import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
13061
13732
|
|
|
13062
13733
|
// src/commands/channel-auth.ts
|
|
13063
13734
|
import { createServer as createServer2 } from "node:http";
|
|
13064
|
-
import { createHash as
|
|
13735
|
+
import { createHash as createHash4, randomBytes as randomBytes5 } from "node:crypto";
|
|
13065
13736
|
var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
|
|
13066
13737
|
var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
|
|
13067
13738
|
var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
|
|
@@ -13072,7 +13743,7 @@ function base64url(b) {
|
|
|
13072
13743
|
}
|
|
13073
13744
|
function pkcePair() {
|
|
13074
13745
|
const verifier = base64url(randomBytes5(32));
|
|
13075
|
-
const challenge = base64url(
|
|
13746
|
+
const challenge = base64url(createHash4("sha256").update(verifier).digest());
|
|
13076
13747
|
return { verifier, challenge };
|
|
13077
13748
|
}
|
|
13078
13749
|
async function startLoopback(state, timeoutMs) {
|
|
@@ -13100,9 +13771,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
13100
13771
|
resolveCode(code);
|
|
13101
13772
|
}
|
|
13102
13773
|
});
|
|
13103
|
-
const bound = await new Promise((
|
|
13104
|
-
server.once("error", () =>
|
|
13105
|
-
server.listen(0, "127.0.0.1", () =>
|
|
13774
|
+
const bound = await new Promise((resolve14) => {
|
|
13775
|
+
server.once("error", () => resolve14(false));
|
|
13776
|
+
server.listen(0, "127.0.0.1", () => resolve14(true));
|
|
13106
13777
|
});
|
|
13107
13778
|
if (!bound)
|
|
13108
13779
|
return null;
|
|
@@ -13173,12 +13844,12 @@ async function runChannelLoopbackUpdate(deps) {
|
|
|
13173
13844
|
}
|
|
13174
13845
|
|
|
13175
13846
|
// src/commands/update.ts
|
|
13176
|
-
var execFileAsync = promisify(
|
|
13847
|
+
var execFileAsync = promisify(execFile2);
|
|
13177
13848
|
var PKG = "@m-kopa/launchpad-cli";
|
|
13178
13849
|
var REGISTRY = "https://registry.npmjs.org";
|
|
13179
13850
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
13180
13851
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
13181
|
-
var CHANNEL_MARKER =
|
|
13852
|
+
var CHANNEL_MARKER = join16(homedir3(), ".launchpad", "channel");
|
|
13182
13853
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
13183
13854
|
var UPGRADE_ARGS = {
|
|
13184
13855
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -13307,8 +13978,8 @@ async function openSystemBrowser(url) {
|
|
|
13307
13978
|
await execFileAsync(opener, [url]);
|
|
13308
13979
|
}
|
|
13309
13980
|
async function runInstallerScript(script) {
|
|
13310
|
-
const dir = mkdtempSync(
|
|
13311
|
-
const file =
|
|
13981
|
+
const dir = mkdtempSync(join16(tmpdir(), "launchpad-update-"));
|
|
13982
|
+
const file = join16(dir, "install.sh");
|
|
13312
13983
|
try {
|
|
13313
13984
|
writeFileSync7(file, script, { mode: 448 });
|
|
13314
13985
|
return await new Promise((resolvePromise) => {
|
|
@@ -13395,7 +14066,7 @@ function errStderr(e) {
|
|
|
13395
14066
|
return "";
|
|
13396
14067
|
}
|
|
13397
14068
|
async function detectPackageManager2() {
|
|
13398
|
-
const pkgRoot =
|
|
14069
|
+
const pkgRoot = resolve14(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
|
|
13399
14070
|
const candidates = [];
|
|
13400
14071
|
const npmRoot = await pmRoot("npm");
|
|
13401
14072
|
if (npmRoot !== null)
|
|
@@ -13403,7 +14074,7 @@ async function detectPackageManager2() {
|
|
|
13403
14074
|
const pnpmRoot = await pmRoot("pnpm");
|
|
13404
14075
|
if (pnpmRoot !== null)
|
|
13405
14076
|
candidates.push(["pnpm", pnpmRoot]);
|
|
13406
|
-
candidates.push(["bun",
|
|
14077
|
+
candidates.push(["bun", resolve14(homedir3(), ".bun/install/global/node_modules")]);
|
|
13407
14078
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
13408
14079
|
return matches.length === 1 ? matches[0][0] : null;
|
|
13409
14080
|
}
|
|
@@ -13417,8 +14088,8 @@ async function pmRoot(pm) {
|
|
|
13417
14088
|
}
|
|
13418
14089
|
}
|
|
13419
14090
|
function pathContains(root, child) {
|
|
13420
|
-
const rel =
|
|
13421
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
14091
|
+
const rel = relative5(root, child);
|
|
14092
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
13422
14093
|
}
|
|
13423
14094
|
function runUpgrade(pm) {
|
|
13424
14095
|
return new Promise((resolvePromise) => {
|
|
@@ -13541,7 +14212,7 @@ function printHelp5(io) {
|
|
|
13541
14212
|
|
|
13542
14213
|
// src/commands/validate.ts
|
|
13543
14214
|
import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
13544
|
-
import { dirname as
|
|
14215
|
+
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
13545
14216
|
var validateCommand = {
|
|
13546
14217
|
name: "validate",
|
|
13547
14218
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -13555,13 +14226,13 @@ async function runValidate(args, io) {
|
|
|
13555
14226
|
return 64;
|
|
13556
14227
|
}
|
|
13557
14228
|
const { file, json, strictGroups } = parseResult;
|
|
13558
|
-
const
|
|
13559
|
-
const result = loadManifest(
|
|
14229
|
+
const path14 = resolve15(process.cwd(), file ?? "launchpad.yaml");
|
|
14230
|
+
const result = loadManifest(path14);
|
|
13560
14231
|
if (result.kind !== "ok") {
|
|
13561
14232
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
13562
14233
|
}
|
|
13563
|
-
const boundary = checkBoundary(
|
|
13564
|
-
const serveErrors = checkStaticServeDir(
|
|
14234
|
+
const boundary = checkBoundary(path14, result.manifest.app !== undefined);
|
|
14235
|
+
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
13565
14236
|
if (serveErrors.length > 0) {
|
|
13566
14237
|
boundary.errors = [...boundary.errors, ...serveErrors];
|
|
13567
14238
|
}
|
|
@@ -13569,7 +14240,7 @@ async function runValidate(args, io) {
|
|
|
13569
14240
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
13570
14241
|
}
|
|
13571
14242
|
function checkBoundary(manifestPath, declared) {
|
|
13572
|
-
const dir =
|
|
14243
|
+
const dir = dirname11(manifestPath);
|
|
13573
14244
|
let files;
|
|
13574
14245
|
try {
|
|
13575
14246
|
files = walkCwd(dir).files;
|
|
@@ -13601,8 +14272,8 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
13601
14272
|
`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
14273
|
];
|
|
13603
14274
|
}
|
|
13604
|
-
const dir =
|
|
13605
|
-
const abs =
|
|
14275
|
+
const dir = dirname11(manifestPath);
|
|
14276
|
+
const abs = resolve15(dir, serveDir);
|
|
13606
14277
|
if (!existsSync15(abs)) {
|
|
13607
14278
|
return [
|
|
13608
14279
|
`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 +14299,7 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
13628
14299
|
`static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
|
|
13629
14300
|
];
|
|
13630
14301
|
}
|
|
13631
|
-
if (!existsSync15(
|
|
14302
|
+
if (!existsSync15(resolve15(abs, "index.html"))) {
|
|
13632
14303
|
return [
|
|
13633
14304
|
`static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
|
|
13634
14305
|
];
|
|
@@ -14497,7 +15168,7 @@ var listOwnersCommand = makeListOwnersCommand();
|
|
|
14497
15168
|
// src/update-notifier.ts
|
|
14498
15169
|
import { spawn as spawn6 } from "node:child_process";
|
|
14499
15170
|
import { homedir as homedir4 } from "node:os";
|
|
14500
|
-
import { join as
|
|
15171
|
+
import { join as join17 } from "node:path";
|
|
14501
15172
|
import {
|
|
14502
15173
|
existsSync as existsSync16,
|
|
14503
15174
|
mkdirSync as mkdirSync4,
|
|
@@ -14507,7 +15178,7 @@ import {
|
|
|
14507
15178
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
14508
15179
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14509
15180
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
14510
|
-
var CACHE_FILE =
|
|
15181
|
+
var CACHE_FILE = join17(homedir4(), ".launchpad", "update-check.json");
|
|
14511
15182
|
function readCache2() {
|
|
14512
15183
|
try {
|
|
14513
15184
|
const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
|
|
@@ -14523,12 +15194,12 @@ function readCache2() {
|
|
|
14523
15194
|
}
|
|
14524
15195
|
function writeCache2(state) {
|
|
14525
15196
|
try {
|
|
14526
|
-
mkdirSync4(
|
|
15197
|
+
mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
|
|
14527
15198
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
14528
15199
|
`, { mode: 384 });
|
|
14529
15200
|
} catch {}
|
|
14530
15201
|
}
|
|
14531
|
-
var SKILLS_HINT_MARKER =
|
|
15202
|
+
var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
|
|
14532
15203
|
function absentHintShown() {
|
|
14533
15204
|
try {
|
|
14534
15205
|
return existsSync16(SKILLS_HINT_MARKER);
|
|
@@ -14538,7 +15209,7 @@ function absentHintShown() {
|
|
|
14538
15209
|
}
|
|
14539
15210
|
function markAbsentHintShown() {
|
|
14540
15211
|
try {
|
|
14541
|
-
mkdirSync4(
|
|
15212
|
+
mkdirSync4(join17(homedir4(), ".launchpad"), { recursive: true });
|
|
14542
15213
|
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
14543
15214
|
`, { mode: 384 });
|
|
14544
15215
|
} catch {}
|
|
@@ -14641,7 +15312,7 @@ var refreshUpdateCacheCommand = {
|
|
|
14641
15312
|
// src/telemetry.ts
|
|
14642
15313
|
import { spawn as spawn7 } from "node:child_process";
|
|
14643
15314
|
import { homedir as homedir5 } from "node:os";
|
|
14644
|
-
import { join as
|
|
15315
|
+
import { join as join18 } from "node:path";
|
|
14645
15316
|
import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
|
|
14646
15317
|
import { randomUUID } from "node:crypto";
|
|
14647
15318
|
var INTERNAL_EMIT_VERB = "__emit-telemetry";
|
|
@@ -14650,8 +15321,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
|
|
|
14650
15321
|
var CAPTURE_PATH = "/i/v0/e/";
|
|
14651
15322
|
var SEND_TIMEOUT_MS = 3000;
|
|
14652
15323
|
var APP = "launchpad-cli";
|
|
14653
|
-
var DEVICE_ID_FILE =
|
|
14654
|
-
var FIRST_RUN_MARKER =
|
|
15324
|
+
var DEVICE_ID_FILE = join18(homedir5(), ".launchpad", "telemetry-id.json");
|
|
15325
|
+
var FIRST_RUN_MARKER = join18(homedir5(), ".launchpad", "telemetry-notice-shown");
|
|
14655
15326
|
function buildEvent(ctx, identity, nowMs) {
|
|
14656
15327
|
const properties = {
|
|
14657
15328
|
app: APP,
|
|
@@ -14725,7 +15396,7 @@ function getOrCreateDeviceId() {
|
|
|
14725
15396
|
} catch {}
|
|
14726
15397
|
const id = randomUUID();
|
|
14727
15398
|
try {
|
|
14728
|
-
mkdirSync5(
|
|
15399
|
+
mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
|
|
14729
15400
|
writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
|
|
14730
15401
|
`, {
|
|
14731
15402
|
mode: 384
|
|
@@ -14803,7 +15474,7 @@ function firstRunNoticeShown() {
|
|
|
14803
15474
|
}
|
|
14804
15475
|
function markFirstRunNotice() {
|
|
14805
15476
|
try {
|
|
14806
|
-
mkdirSync5(
|
|
15477
|
+
mkdirSync5(join18(homedir5(), ".launchpad"), { recursive: true });
|
|
14807
15478
|
writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
|
|
14808
15479
|
`, { mode: 384 });
|
|
14809
15480
|
} catch {}
|
|
@@ -14871,14 +15542,14 @@ import { platform } from "node:os";
|
|
|
14871
15542
|
// src/report/breadcrumb.ts
|
|
14872
15543
|
import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
|
|
14873
15544
|
import { homedir as homedir6 } from "node:os";
|
|
14874
|
-
import { join as
|
|
15545
|
+
import { join as join19, dirname as dirname12 } from "node:path";
|
|
14875
15546
|
function breadcrumbPath() {
|
|
14876
|
-
return
|
|
15547
|
+
return join19(homedir6(), ".launchpad", "last-run.json");
|
|
14877
15548
|
}
|
|
14878
15549
|
function writeBreadcrumb(verb, exit) {
|
|
14879
15550
|
try {
|
|
14880
15551
|
const p = breadcrumbPath();
|
|
14881
|
-
mkdirSync6(
|
|
15552
|
+
mkdirSync6(dirname12(p), { recursive: true });
|
|
14882
15553
|
writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
|
|
14883
15554
|
} catch {}
|
|
14884
15555
|
}
|
|
@@ -15038,6 +15709,12 @@ function makeReportCommand(kind, deps = realDeps5()) {
|
|
|
15038
15709
|
};
|
|
15039
15710
|
}
|
|
15040
15711
|
|
|
15712
|
+
// src/http/preflight.ts
|
|
15713
|
+
async function preflightMinimumCliVersion(config, fetcher = fetch) {
|
|
15714
|
+
const detachedFetcher = fetcher;
|
|
15715
|
+
await apiRaw(config, { method: "POST", path: "/cli/preflight" }, detachedFetcher);
|
|
15716
|
+
}
|
|
15717
|
+
|
|
15041
15718
|
// src/dispatcher.ts
|
|
15042
15719
|
var COMMANDS = [
|
|
15043
15720
|
loginCommand,
|
|
@@ -15077,7 +15754,7 @@ var COMMANDS = [
|
|
|
15077
15754
|
refreshUpdateCacheCommand,
|
|
15078
15755
|
emitTelemetryCommand
|
|
15079
15756
|
];
|
|
15080
|
-
async function dispatch(argv, io, commands = COMMANDS) {
|
|
15757
|
+
async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
|
|
15081
15758
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
15082
15759
|
printHelp6(io, commands);
|
|
15083
15760
|
return 0;
|
|
@@ -15095,12 +15772,55 @@ async function dispatch(argv, io, commands = COMMANDS) {
|
|
|
15095
15772
|
io.err("Run `launchpad --help` for the list of available commands.");
|
|
15096
15773
|
return 64;
|
|
15097
15774
|
}
|
|
15775
|
+
const preflightExit = await runCliFloorPreflight(verb, rest, io, deps);
|
|
15776
|
+
if (preflightExit !== null)
|
|
15777
|
+
return preflightExit;
|
|
15098
15778
|
const code = await cmd.run(rest, io);
|
|
15099
15779
|
if (!cmd.hidden && verb !== "bug" && verb !== "feature") {
|
|
15100
15780
|
writeBreadcrumb(verb, code);
|
|
15101
15781
|
}
|
|
15102
15782
|
return code;
|
|
15103
15783
|
}
|
|
15784
|
+
async function runCliFloorPreflight(verb, args, io, deps) {
|
|
15785
|
+
if (!requiresCliFloorPreflight(verb, args))
|
|
15786
|
+
return null;
|
|
15787
|
+
try {
|
|
15788
|
+
const preflight = deps.preflight ?? preflightMinimumCliVersion;
|
|
15789
|
+
await preflight(loadConfig());
|
|
15790
|
+
return null;
|
|
15791
|
+
} catch (error) {
|
|
15792
|
+
if (error instanceof UpgradeRequiredError) {
|
|
15793
|
+
io.err(error.message);
|
|
15794
|
+
return EXIT_CLI_UPGRADE_REQUIRED;
|
|
15795
|
+
}
|
|
15796
|
+
io.err(`launchpad ${verb}: version preflight failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
15797
|
+
return 1;
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15800
|
+
function requiresCliFloorPreflight(verb, args) {
|
|
15801
|
+
if (args.includes("--help") || args.includes("-h"))
|
|
15802
|
+
return false;
|
|
15803
|
+
if ([
|
|
15804
|
+
"create",
|
|
15805
|
+
"deploy",
|
|
15806
|
+
"redeploy",
|
|
15807
|
+
"merge",
|
|
15808
|
+
"destroy",
|
|
15809
|
+
"recover",
|
|
15810
|
+
"rollback",
|
|
15811
|
+
"grant-editor",
|
|
15812
|
+
"revoke-editor",
|
|
15813
|
+
"add-owner",
|
|
15814
|
+
"remove-owner"
|
|
15815
|
+
].includes(verb)) {
|
|
15816
|
+
return true;
|
|
15817
|
+
}
|
|
15818
|
+
if (verb === "envvars")
|
|
15819
|
+
return args[0] === "set" || args[0] === "rm";
|
|
15820
|
+
if (verb === "secrets")
|
|
15821
|
+
return args[0] === "push";
|
|
15822
|
+
return false;
|
|
15823
|
+
}
|
|
15104
15824
|
function printHelp6(io, commands) {
|
|
15105
15825
|
io.out("launchpad — manage Launchpad-deployed apps from the command line.");
|
|
15106
15826
|
io.out("");
|