@m-kopa/launchpad-cli 0.27.1 → 0.27.3
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 +24 -0
- package/dist/bundle/orchestrate.d.ts +3 -0
- package/dist/bundle/orchestrate.d.ts.map +1 -1
- package/dist/bundle/upload.d.ts +18 -1
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +505 -202
- package/dist/clone/base-sha-marker.d.ts +25 -0
- package/dist/clone/base-sha-marker.d.ts.map +1 -0
- package/dist/clone/git-init.d.ts.map +1 -1
- package/dist/commands/clone.d.ts.map +1 -1
- package/dist/commands/deploy-flags.d.ts +8 -0
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/update.d.ts +12 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/update-notifier.d.ts +27 -0
- package/dist/update-notifier.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +1 -1
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -1
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.27.
|
|
22
|
+
var CLI_VERSION = "0.27.3";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -994,8 +994,8 @@ function describe8(e) {
|
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
// src/commands/clone.ts
|
|
997
|
-
import * as
|
|
998
|
-
import * as
|
|
997
|
+
import * as path5 from "node:path";
|
|
998
|
+
import * as fs4 from "node:fs/promises";
|
|
999
999
|
|
|
1000
1000
|
// src/clone/tar-extract.ts
|
|
1001
1001
|
import * as fs2 from "node:fs/promises";
|
|
@@ -1208,6 +1208,57 @@ function runGit(sp, cwd, args) {
|
|
|
1208
1208
|
});
|
|
1209
1209
|
}
|
|
1210
1210
|
|
|
1211
|
+
// src/clone/base-sha-marker.ts
|
|
1212
|
+
import * as path4 from "node:path";
|
|
1213
|
+
import * as fs3 from "node:fs/promises";
|
|
1214
|
+
var BASE_SHA_HEADER = "x-launchpad-base-sha";
|
|
1215
|
+
var MARKER_DIR = ".launchpad";
|
|
1216
|
+
var MARKER_RELPATH = `${MARKER_DIR}/base-sha`;
|
|
1217
|
+
var EXCLUDE_RELPATH = path4.join(".git", "info", "exclude");
|
|
1218
|
+
var EXCLUDE_LINE = `${MARKER_DIR}/`;
|
|
1219
|
+
var SHA_RE = /^[0-9a-f]{40}$/;
|
|
1220
|
+
function isValidBaseSha(value) {
|
|
1221
|
+
return SHA_RE.test(value);
|
|
1222
|
+
}
|
|
1223
|
+
async function writeBaseShaMarker(cwd, sha) {
|
|
1224
|
+
if (!isValidBaseSha(sha)) {
|
|
1225
|
+
throw new Error(`refusing to stamp a non-SHA base marker: "${sha}"`);
|
|
1226
|
+
}
|
|
1227
|
+
await ensureExcluded(cwd);
|
|
1228
|
+
const dir = path4.resolve(cwd, MARKER_DIR);
|
|
1229
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
1230
|
+
await fs3.writeFile(path4.resolve(cwd, MARKER_RELPATH), `${sha}
|
|
1231
|
+
`, "utf8");
|
|
1232
|
+
}
|
|
1233
|
+
async function readBaseShaMarker(cwd) {
|
|
1234
|
+
let raw;
|
|
1235
|
+
try {
|
|
1236
|
+
raw = await fs3.readFile(path4.resolve(cwd, MARKER_RELPATH), "utf8");
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
const sha = raw.trim();
|
|
1241
|
+
return isValidBaseSha(sha) ? sha : null;
|
|
1242
|
+
}
|
|
1243
|
+
async function ensureExcluded(cwd) {
|
|
1244
|
+
const excludePath = path4.resolve(cwd, EXCLUDE_RELPATH);
|
|
1245
|
+
let current = "";
|
|
1246
|
+
try {
|
|
1247
|
+
current = await fs3.readFile(excludePath, "utf8");
|
|
1248
|
+
} catch {
|
|
1249
|
+
await fs3.mkdir(path4.dirname(excludePath), { recursive: true });
|
|
1250
|
+
}
|
|
1251
|
+
const already = current.split(`
|
|
1252
|
+
`).some((line) => line.trim() === EXCLUDE_LINE);
|
|
1253
|
+
if (already)
|
|
1254
|
+
return;
|
|
1255
|
+
const sep = current.length > 0 && !current.endsWith(`
|
|
1256
|
+
`) ? `
|
|
1257
|
+
` : "";
|
|
1258
|
+
await fs3.writeFile(excludePath, `${current}${sep}${EXCLUDE_LINE}
|
|
1259
|
+
`, "utf8");
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1211
1262
|
// src/commands/clone.ts
|
|
1212
1263
|
var cloneCommand = {
|
|
1213
1264
|
name: "clone",
|
|
@@ -1230,9 +1281,9 @@ async function runClone(args, io) {
|
|
|
1230
1281
|
let destPreExisted = false;
|
|
1231
1282
|
try {
|
|
1232
1283
|
cfg = loadConfig();
|
|
1233
|
-
destDir =
|
|
1284
|
+
destDir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
|
|
1234
1285
|
try {
|
|
1235
|
-
await
|
|
1286
|
+
await fs4.access(destDir);
|
|
1236
1287
|
destPreExisted = true;
|
|
1237
1288
|
} catch {
|
|
1238
1289
|
destPreExisted = false;
|
|
@@ -1243,13 +1294,28 @@ async function runClone(args, io) {
|
|
|
1243
1294
|
io.err(`launchpad clone: bot returned no body for /apps/${slug}/source-bundle`);
|
|
1244
1295
|
return 1;
|
|
1245
1296
|
}
|
|
1297
|
+
const headerSha = res.headers.get(BASE_SHA_HEADER);
|
|
1298
|
+
const baseSha = headerSha !== null && isValidBaseSha(headerSha) ? headerSha : null;
|
|
1246
1299
|
const stats = await extractToDir(res.body, destDir, { stripComponents: 1 });
|
|
1247
1300
|
await initGitRepo({
|
|
1248
1301
|
cwd: destDir,
|
|
1249
1302
|
initialCommitMessage: `Initial baseline from launchpad clone ${slug}`
|
|
1250
1303
|
});
|
|
1304
|
+
let stamped = false;
|
|
1305
|
+
if (baseSha !== null) {
|
|
1306
|
+
try {
|
|
1307
|
+
await writeBaseShaMarker(destDir, baseSha);
|
|
1308
|
+
stamped = true;
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
io.err(`launchpad clone: warning — could not record base revision: ${describe9(e)}`);
|
|
1311
|
+
io.err("(the clone is usable, but deploy cannot detect divergence)");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1251
1314
|
io.out("");
|
|
1252
1315
|
io.out(`Cloned ${stats.fileCount} files (${formatBytes(stats.byteCount)}) into ${destDir}`);
|
|
1316
|
+
if (stamped && baseSha !== null) {
|
|
1317
|
+
io.out(`Base revision ${baseSha.slice(0, 12)} recorded — \`launchpad deploy\` will warn if main moves past it.`);
|
|
1318
|
+
}
|
|
1253
1319
|
io.out(`No remote configured — use \`launchpad deploy\` to ship changes back.`);
|
|
1254
1320
|
return 0;
|
|
1255
1321
|
} catch (e) {
|
|
@@ -1291,8 +1357,8 @@ async function runClone(args, io) {
|
|
|
1291
1357
|
}
|
|
1292
1358
|
}
|
|
1293
1359
|
async function cleanupBestEffort(slug) {
|
|
1294
|
-
const dir =
|
|
1295
|
-
await
|
|
1360
|
+
const dir = path5.resolve(process.cwd(), `launchpad-app-${slug}`);
|
|
1361
|
+
await fs4.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1296
1362
|
return;
|
|
1297
1363
|
});
|
|
1298
1364
|
}
|
|
@@ -1460,15 +1526,15 @@ function describe10(e) {
|
|
|
1460
1526
|
|
|
1461
1527
|
// src/commands/deploy.ts
|
|
1462
1528
|
import { existsSync as existsSync5 } from "node:fs";
|
|
1463
|
-
import * as
|
|
1529
|
+
import * as path8 from "node:path";
|
|
1464
1530
|
|
|
1465
1531
|
// src/bundle/orchestrate.ts
|
|
1466
1532
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1467
|
-
import { join as
|
|
1533
|
+
import { join as join7 } from "node:path";
|
|
1468
1534
|
|
|
1469
1535
|
// src/deploy/tar-pack.ts
|
|
1470
|
-
import * as
|
|
1471
|
-
import * as
|
|
1536
|
+
import * as fs5 from "node:fs/promises";
|
|
1537
|
+
import * as path6 from "node:path";
|
|
1472
1538
|
var MAX_COMPRESSED_TARBALL_BYTES = 50 * 1024 * 1024;
|
|
1473
1539
|
var MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
1474
1540
|
|
|
@@ -1483,10 +1549,10 @@ async function packTarGz(cwd, files) {
|
|
|
1483
1549
|
for (const rel of files) {
|
|
1484
1550
|
if (rel.length === 0)
|
|
1485
1551
|
continue;
|
|
1486
|
-
const abs =
|
|
1552
|
+
const abs = path6.join(cwd, rel);
|
|
1487
1553
|
let stat;
|
|
1488
1554
|
try {
|
|
1489
|
-
stat = await
|
|
1555
|
+
stat = await fs5.lstat(abs);
|
|
1490
1556
|
} catch (e) {
|
|
1491
1557
|
if (isErrno3(e) && e.code === "ENOENT")
|
|
1492
1558
|
continue;
|
|
@@ -1499,7 +1565,7 @@ async function packTarGz(cwd, files) {
|
|
|
1499
1565
|
if (stat.size > MAX_FILE_BYTES) {
|
|
1500
1566
|
throw new TarPackError(`file exceeds ${MAX_FILE_BYTES} bytes: ${rel} (${stat.size} bytes)`);
|
|
1501
1567
|
}
|
|
1502
|
-
const bytes = await
|
|
1568
|
+
const bytes = await fs5.readFile(abs);
|
|
1503
1569
|
const mode = (stat.mode & 64) !== 0 ? 493 : 420;
|
|
1504
1570
|
blocks.push(buildHeader({ path: rel, size: bytes.length, mode }, enc));
|
|
1505
1571
|
blocks.push(bytes);
|
|
@@ -1618,7 +1684,7 @@ function isErrno3(e) {
|
|
|
1618
1684
|
}
|
|
1619
1685
|
|
|
1620
1686
|
// src/bundle/upload.ts
|
|
1621
|
-
async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact) {
|
|
1687
|
+
async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact, baseSha, overrideStale) {
|
|
1622
1688
|
const formData = new FormData;
|
|
1623
1689
|
formData.append("manifest", manifestYaml);
|
|
1624
1690
|
const buf = new ArrayBuffer(bundleTarGz.byteLength);
|
|
@@ -1642,6 +1708,10 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1642
1708
|
authorization: `Bearer ${accessToken}`,
|
|
1643
1709
|
accept: "application/json"
|
|
1644
1710
|
};
|
|
1711
|
+
if (baseSha)
|
|
1712
|
+
headers["x-launchpad-base-sha"] = baseSha;
|
|
1713
|
+
if (overrideStale)
|
|
1714
|
+
headers["x-launchpad-stale-override"] = overrideStale;
|
|
1645
1715
|
let res;
|
|
1646
1716
|
try {
|
|
1647
1717
|
res = await fetch(url, { method: "POST", headers, body: formData });
|
|
@@ -1684,7 +1754,7 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1684
1754
|
|
|
1685
1755
|
// src/bundle/cwd-walker.ts
|
|
1686
1756
|
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
1687
|
-
import { join as
|
|
1757
|
+
import { join as join4, relative as relative2, sep } from "node:path";
|
|
1688
1758
|
var DEFAULT_IGNORE = [
|
|
1689
1759
|
".git",
|
|
1690
1760
|
"node_modules",
|
|
@@ -1733,7 +1803,7 @@ function walkCwd(cwd, options = {}) {
|
|
|
1733
1803
|
return { files, skipped };
|
|
1734
1804
|
}
|
|
1735
1805
|
function walkInto(cwd, relDir, ignore, out, skipped, maxFiles) {
|
|
1736
|
-
const absDir = relDir === "" ? cwd :
|
|
1806
|
+
const absDir = relDir === "" ? cwd : join4(cwd, relDir);
|
|
1737
1807
|
let entries;
|
|
1738
1808
|
try {
|
|
1739
1809
|
entries = readdirSync(absDir);
|
|
@@ -1746,7 +1816,7 @@ function walkInto(cwd, relDir, ignore, out, skipped, maxFiles) {
|
|
|
1746
1816
|
}
|
|
1747
1817
|
for (const name of entries) {
|
|
1748
1818
|
const relPath = relDir === "" ? name : `${relDir}/${name}`;
|
|
1749
|
-
const absPath =
|
|
1819
|
+
const absPath = join4(absDir, name);
|
|
1750
1820
|
if (DEFAULT_IGNORE_SET.has(name)) {
|
|
1751
1821
|
skipped.push({ path: relPath, reason: "default-ignore" });
|
|
1752
1822
|
continue;
|
|
@@ -1782,7 +1852,7 @@ function walkInto(cwd, relDir, ignore, out, skipped, maxFiles) {
|
|
|
1782
1852
|
}
|
|
1783
1853
|
}
|
|
1784
1854
|
function loadIgnore(cwd) {
|
|
1785
|
-
const file =
|
|
1855
|
+
const file = join4(cwd, ".gitignore");
|
|
1786
1856
|
if (!existsSync(file)) {
|
|
1787
1857
|
return { matches: () => false };
|
|
1788
1858
|
}
|
|
@@ -1863,7 +1933,7 @@ function globToRegExp(glob) {
|
|
|
1863
1933
|
|
|
1864
1934
|
// src/bundle/cron-bundle.ts
|
|
1865
1935
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1866
|
-
import { dirname as
|
|
1936
|
+
import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
|
|
1867
1937
|
import { parse as parseYaml } from "yaml";
|
|
1868
1938
|
import { build as esbuild } from "esbuild";
|
|
1869
1939
|
function parseWranglerTopLevel(tomlText) {
|
|
@@ -1939,7 +2009,7 @@ function locateWorkerConfig(cwd, walkFiles, script) {
|
|
|
1939
2009
|
for (const rel of walkFiles) {
|
|
1940
2010
|
if (!rel.endsWith("wrangler.toml"))
|
|
1941
2011
|
continue;
|
|
1942
|
-
const abs =
|
|
2012
|
+
const abs = resolve5(cwd, rel);
|
|
1943
2013
|
let text;
|
|
1944
2014
|
try {
|
|
1945
2015
|
text = readFileSync2(abs, "utf8");
|
|
@@ -1951,8 +2021,8 @@ function locateWorkerConfig(cwd, walkFiles, script) {
|
|
|
1951
2021
|
continue;
|
|
1952
2022
|
if (cfg.main === undefined || cfg.main === "")
|
|
1953
2023
|
continue;
|
|
1954
|
-
const workerDir =
|
|
1955
|
-
return { entryAbs:
|
|
2024
|
+
const workerDir = dirname4(abs);
|
|
2025
|
+
return { entryAbs: join5(workerDir, cfg.main), workerDir, cfg };
|
|
1956
2026
|
}
|
|
1957
2027
|
return null;
|
|
1958
2028
|
}
|
|
@@ -2022,7 +2092,7 @@ async function buildWorkerArtifact(cwd, manifestYaml, walkFiles) {
|
|
|
2022
2092
|
|
|
2023
2093
|
// src/bundle/boundary.ts
|
|
2024
2094
|
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2025
|
-
import { join as
|
|
2095
|
+
import { join as join6 } from "node:path";
|
|
2026
2096
|
import { parse as parseYaml2 } from "yaml";
|
|
2027
2097
|
|
|
2028
2098
|
// ../launchpad-engine/dist/schema.js
|
|
@@ -2484,43 +2554,43 @@ var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
|
|
|
2484
2554
|
".github/scripts/parse-gitleaks.mjs",
|
|
2485
2555
|
".github/scripts/parse-bun-audit.mjs"
|
|
2486
2556
|
]);
|
|
2487
|
-
function checkDenyList(
|
|
2488
|
-
const segments =
|
|
2557
|
+
function checkDenyList(path7) {
|
|
2558
|
+
const segments = path7.split("/");
|
|
2489
2559
|
const basename = segments[segments.length - 1];
|
|
2490
2560
|
const isExample = basename.endsWith(".example");
|
|
2491
2561
|
if (basename.startsWith(".env") && !isExample) {
|
|
2492
|
-
return { path:
|
|
2562
|
+
return { path: path7, rule: "env-file", message: `'${path7}' is an environment file (.env*) — never shippable` };
|
|
2493
2563
|
}
|
|
2494
2564
|
if (basename === ".npmrc") {
|
|
2495
|
-
return { path:
|
|
2565
|
+
return { path: path7, rule: "npmrc", message: `'${path7}' (.npmrc) may carry registry auth — never shippable` };
|
|
2496
2566
|
}
|
|
2497
2567
|
if (basename.startsWith(".dev.vars") && !isExample) {
|
|
2498
|
-
return { path:
|
|
2568
|
+
return { path: path7, rule: "dev-vars", message: `'${path7}' is a wrangler dev-vars file — never shippable` };
|
|
2499
2569
|
}
|
|
2500
2570
|
if (segments.includes(".claude")) {
|
|
2501
|
-
return { path:
|
|
2571
|
+
return { path: path7, rule: "claude-dir", message: `'${path7}' is under a .claude/ directory (AI-workspace state) — never shippable` };
|
|
2502
2572
|
}
|
|
2503
2573
|
if (segments.includes(".git")) {
|
|
2504
|
-
return { path:
|
|
2574
|
+
return { path: path7, rule: "git-dir", message: `'${path7}' is under .git/ — never shippable` };
|
|
2505
2575
|
}
|
|
2506
2576
|
if (segments[0] === ".github") {
|
|
2507
|
-
if (PLATFORM_SEEDED_GITHUB_PATHS.has(
|
|
2577
|
+
if (PLATFORM_SEEDED_GITHUB_PATHS.has(path7))
|
|
2508
2578
|
return "seeded";
|
|
2509
2579
|
return {
|
|
2510
|
-
path:
|
|
2580
|
+
path: path7,
|
|
2511
2581
|
rule: "github-dir",
|
|
2512
|
-
message: `'${
|
|
2582
|
+
message: `'${path7}' — repo-root .github/ is platform-owned (workflows execute under the M-KOPA Actions identity); developer-supplied .github/** is not shippable`
|
|
2513
2583
|
};
|
|
2514
2584
|
}
|
|
2515
2585
|
return null;
|
|
2516
2586
|
}
|
|
2517
|
-
function checkSecretShape(
|
|
2518
|
-
const segments =
|
|
2587
|
+
function checkSecretShape(path7, readFileContent) {
|
|
2588
|
+
const segments = path7.split("/");
|
|
2519
2589
|
const basename = segments[segments.length - 1];
|
|
2520
2590
|
const deny = (what) => ({
|
|
2521
|
-
path:
|
|
2591
|
+
path: path7,
|
|
2522
2592
|
rule: "secret-shape",
|
|
2523
|
-
message: `'${
|
|
2593
|
+
message: `'${path7}' looks like ${what} — never shippable; if this is a false positive, rename the file`
|
|
2524
2594
|
});
|
|
2525
2595
|
if (basename.endsWith(".pem"))
|
|
2526
2596
|
return deny("PEM key material (*.pem)");
|
|
@@ -2536,7 +2606,7 @@ function checkSecretShape(path6, readFileContent) {
|
|
|
2536
2606
|
if (segments.includes(".aws"))
|
|
2537
2607
|
return deny("AWS credentials (.aws/**)");
|
|
2538
2608
|
if (basename.endsWith(".json") && readFileContent !== undefined) {
|
|
2539
|
-
const content = readFileContent(
|
|
2609
|
+
const content = readFileContent(path7);
|
|
2540
2610
|
if (content !== undefined && /"private_key"\s*:/.test(content)) {
|
|
2541
2611
|
return deny('service-account-shaped JSON (carries a "private_key" field)');
|
|
2542
2612
|
}
|
|
@@ -2550,41 +2620,41 @@ function applyAppBoundary(contract, files, options = {}) {
|
|
|
2550
2620
|
const excluded = [];
|
|
2551
2621
|
const denied = [];
|
|
2552
2622
|
const rootPrefix = contract.root === "." ? "" : `${contract.root}/`;
|
|
2553
|
-
for (const
|
|
2554
|
-
if (
|
|
2555
|
-
if (rootPrefix !== "" && !
|
|
2556
|
-
excluded.push({ path:
|
|
2623
|
+
for (const path7 of [...files].sort()) {
|
|
2624
|
+
if (path7 !== ALWAYS_SHIP) {
|
|
2625
|
+
if (rootPrefix !== "" && !path7.startsWith(rootPrefix)) {
|
|
2626
|
+
excluded.push({ path: path7, reason: "outside-root" });
|
|
2557
2627
|
continue;
|
|
2558
2628
|
}
|
|
2559
|
-
const rel = rootPrefix === "" ?
|
|
2629
|
+
const rel = rootPrefix === "" ? path7 : path7.slice(rootPrefix.length);
|
|
2560
2630
|
if (!contract.include.some((p) => matchContractPattern(p, rel))) {
|
|
2561
|
-
excluded.push({ path:
|
|
2631
|
+
excluded.push({ path: path7, reason: "not-included" });
|
|
2562
2632
|
continue;
|
|
2563
2633
|
}
|
|
2564
2634
|
if (contract.exclude.some((p) => matchContractPattern(p, rel))) {
|
|
2565
|
-
excluded.push({ path:
|
|
2635
|
+
excluded.push({ path: path7, reason: "exclude" });
|
|
2566
2636
|
continue;
|
|
2567
2637
|
}
|
|
2568
|
-
if (ignorePatterns.some((p) => matchIgnorePattern(p,
|
|
2569
|
-
excluded.push({ path:
|
|
2638
|
+
if (ignorePatterns.some((p) => matchIgnorePattern(p, path7))) {
|
|
2639
|
+
excluded.push({ path: path7, reason: "launchpadignore" });
|
|
2570
2640
|
continue;
|
|
2571
2641
|
}
|
|
2572
2642
|
}
|
|
2573
|
-
const denial = checkDenyList(
|
|
2643
|
+
const denial = checkDenyList(path7);
|
|
2574
2644
|
if (denial === "seeded") {
|
|
2575
|
-
excluded.push({ path:
|
|
2645
|
+
excluded.push({ path: path7, reason: "platform-seeded" });
|
|
2576
2646
|
continue;
|
|
2577
2647
|
}
|
|
2578
2648
|
if (denial !== null) {
|
|
2579
2649
|
denied.push(denial);
|
|
2580
2650
|
continue;
|
|
2581
2651
|
}
|
|
2582
|
-
const secret = checkSecretShape(
|
|
2652
|
+
const secret = checkSecretShape(path7, options.readFileContent);
|
|
2583
2653
|
if (secret !== null) {
|
|
2584
2654
|
denied.push(secret);
|
|
2585
2655
|
continue;
|
|
2586
2656
|
}
|
|
2587
|
-
included.push(
|
|
2657
|
+
included.push(path7);
|
|
2588
2658
|
}
|
|
2589
2659
|
return { files: included, excluded, denied };
|
|
2590
2660
|
}
|
|
@@ -2612,7 +2682,7 @@ function verifyBuildInputs(build, files) {
|
|
|
2612
2682
|
return issues;
|
|
2613
2683
|
}
|
|
2614
2684
|
let cwd = rootDir === "." ? "" : rootDir;
|
|
2615
|
-
const
|
|
2685
|
+
const resolve6 = (p) => {
|
|
2616
2686
|
const cleaned = p.startsWith("./") ? p.slice(2) : p;
|
|
2617
2687
|
const trimmed = cleaned.replace(/\/+$/, "");
|
|
2618
2688
|
return cwd === "" ? trimmed : `${cwd}/${trimmed}`;
|
|
@@ -2632,7 +2702,7 @@ function verifyBuildInputs(build, files) {
|
|
|
2632
2702
|
cwd = null;
|
|
2633
2703
|
continue;
|
|
2634
2704
|
}
|
|
2635
|
-
const resolved =
|
|
2705
|
+
const resolved = resolve6(target);
|
|
2636
2706
|
if (!dirSet.has(resolved)) {
|
|
2637
2707
|
issues.push({
|
|
2638
2708
|
input: target,
|
|
@@ -2659,7 +2729,7 @@ function verifyBuildInputs(build, files) {
|
|
|
2659
2729
|
for (const src of sources) {
|
|
2660
2730
|
if (!PLAIN_PATH.test(src))
|
|
2661
2731
|
continue;
|
|
2662
|
-
const resolved =
|
|
2732
|
+
const resolved = resolve6(src);
|
|
2663
2733
|
if (!exists(resolved)) {
|
|
2664
2734
|
issues.push({
|
|
2665
2735
|
input: src,
|
|
@@ -3419,7 +3489,7 @@ function applyBoundaryToFiles(args) {
|
|
|
3419
3489
|
}
|
|
3420
3490
|
const contract = resolveAppBoundary({ appType: input.appType, app: input.app });
|
|
3421
3491
|
let launchpadIgnore = [];
|
|
3422
|
-
const ignorePath =
|
|
3492
|
+
const ignorePath = join6(cwd, ".launchpadignore");
|
|
3423
3493
|
if (existsSync2(ignorePath)) {
|
|
3424
3494
|
try {
|
|
3425
3495
|
launchpadIgnore = parseLaunchpadIgnore(readFileSync3(ignorePath, "utf8"));
|
|
@@ -3427,9 +3497,9 @@ function applyBoundaryToFiles(args) {
|
|
|
3427
3497
|
}
|
|
3428
3498
|
const result = applyAppBoundary(contract, files, {
|
|
3429
3499
|
launchpadIgnore,
|
|
3430
|
-
readFileContent: (
|
|
3500
|
+
readFileContent: (path7) => {
|
|
3431
3501
|
try {
|
|
3432
|
-
const abs =
|
|
3502
|
+
const abs = join6(cwd, path7);
|
|
3433
3503
|
const stat = lstatSync2(abs);
|
|
3434
3504
|
if (!stat.isFile() || stat.size > MAX_CONTENT_PROBE_BYTES)
|
|
3435
3505
|
return;
|
|
@@ -3481,8 +3551,8 @@ ${lines}`
|
|
|
3481
3551
|
|
|
3482
3552
|
// src/bundle/orchestrate.ts
|
|
3483
3553
|
async function bundleAndDeploy(args) {
|
|
3484
|
-
const { cfg, cwd, slug } = args;
|
|
3485
|
-
const manifestPath =
|
|
3554
|
+
const { cfg, cwd, slug, overrideStale } = args;
|
|
3555
|
+
const manifestPath = join7(cwd, "launchpad.yaml");
|
|
3486
3556
|
let manifestYaml;
|
|
3487
3557
|
try {
|
|
3488
3558
|
manifestYaml = readFileSync4(manifestPath, "utf8");
|
|
@@ -3526,7 +3596,8 @@ async function bundleAndDeploy(args) {
|
|
|
3526
3596
|
return { kind: "worker-build-error", message: workerBuild.message };
|
|
3527
3597
|
}
|
|
3528
3598
|
const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
|
|
3529
|
-
const
|
|
3599
|
+
const baseSha = await readBaseShaMarker(cwd);
|
|
3600
|
+
const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact, baseSha, overrideStale);
|
|
3530
3601
|
if (uploadResult.kind !== "ok") {
|
|
3531
3602
|
return {
|
|
3532
3603
|
kind: "upload-error",
|
|
@@ -3579,7 +3650,7 @@ async function listDeployFiles(opts) {
|
|
|
3579
3650
|
return [...paths].sort();
|
|
3580
3651
|
}
|
|
3581
3652
|
function runGit2(sp, cwd, args) {
|
|
3582
|
-
return new Promise((
|
|
3653
|
+
return new Promise((resolve6, reject) => {
|
|
3583
3654
|
const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
|
|
3584
3655
|
const child = sp("git", [...args], spawnOpts);
|
|
3585
3656
|
let stdout = "";
|
|
@@ -3595,7 +3666,7 @@ function runGit2(sp, cwd, args) {
|
|
|
3595
3666
|
});
|
|
3596
3667
|
child.once("close", (code) => {
|
|
3597
3668
|
if (code === 0) {
|
|
3598
|
-
|
|
3669
|
+
resolve6(stdout);
|
|
3599
3670
|
return;
|
|
3600
3671
|
}
|
|
3601
3672
|
reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
|
|
@@ -3625,6 +3696,9 @@ function parseDeployFlags(args) {
|
|
|
3625
3696
|
let timeoutMinutes = null;
|
|
3626
3697
|
let atSha = null;
|
|
3627
3698
|
let modeFlagsSeen = 0;
|
|
3699
|
+
let allowStale = false;
|
|
3700
|
+
let rebaseOntoHead = false;
|
|
3701
|
+
let overrideStale = null;
|
|
3628
3702
|
let i = 0;
|
|
3629
3703
|
while (i < args.length) {
|
|
3630
3704
|
const a = args[i] ?? "";
|
|
@@ -3752,6 +3826,27 @@ function parseDeployFlags(args) {
|
|
|
3752
3826
|
i += 2;
|
|
3753
3827
|
continue;
|
|
3754
3828
|
}
|
|
3829
|
+
if (a === "--allow-stale") {
|
|
3830
|
+
allowStale = true;
|
|
3831
|
+
i += 1;
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
if (a === "--rebase-onto-head") {
|
|
3835
|
+
rebaseOntoHead = true;
|
|
3836
|
+
i += 1;
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
if (a === "--override-stale") {
|
|
3840
|
+
const v = args[i + 1];
|
|
3841
|
+
if (v === undefined)
|
|
3842
|
+
return `missing value for ${a}`;
|
|
3843
|
+
if (v.length < 7) {
|
|
3844
|
+
return `invalid --override-stale "${v}" — echo at least the first 7 chars of the head SHA from the block message`;
|
|
3845
|
+
}
|
|
3846
|
+
overrideStale = v;
|
|
3847
|
+
i += 2;
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3755
3850
|
if (a === "--timeout-seconds") {
|
|
3756
3851
|
const v = args[i + 1];
|
|
3757
3852
|
if (v === undefined)
|
|
@@ -3804,6 +3899,12 @@ function parseDeployFlags(args) {
|
|
|
3804
3899
|
if (modeFlagsSeen > 1) {
|
|
3805
3900
|
return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
|
|
3806
3901
|
}
|
|
3902
|
+
if (mode !== "content" && (allowStale || rebaseOntoHead || overrideStale !== null)) {
|
|
3903
|
+
return "--allow-stale / --rebase-onto-head / --override-stale are only valid for a content deploy";
|
|
3904
|
+
}
|
|
3905
|
+
if (rebaseOntoHead && overrideStale !== null) {
|
|
3906
|
+
return "--rebase-onto-head and --override-stale are mutually exclusive — re-stamp to head, or override in place, not both";
|
|
3907
|
+
}
|
|
3807
3908
|
if (mode === "dry-run") {
|
|
3808
3909
|
if (slug !== null)
|
|
3809
3910
|
return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
|
|
@@ -3892,7 +3993,7 @@ function parseDeployFlags(args) {
|
|
|
3892
3993
|
return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
|
|
3893
3994
|
}
|
|
3894
3995
|
return {
|
|
3895
|
-
mode: { kind: "content", slug },
|
|
3996
|
+
mode: { kind: "content", slug, allowStale, rebaseOntoHead, overrideStale },
|
|
3896
3997
|
message,
|
|
3897
3998
|
timeoutSeconds
|
|
3898
3999
|
};
|
|
@@ -3945,31 +4046,31 @@ function parseDeployFlags(args) {
|
|
|
3945
4046
|
|
|
3946
4047
|
// src/deploy/apply.ts
|
|
3947
4048
|
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
3948
|
-
import { resolve as resolvePath, join as
|
|
4049
|
+
import { resolve as resolvePath, join as join8 } from "node:path";
|
|
3949
4050
|
|
|
3950
4051
|
// src/manifest/load.ts
|
|
3951
4052
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3952
4053
|
import { parse as parseYaml3, YAMLParseError } from "yaml";
|
|
3953
|
-
function loadManifest(
|
|
4054
|
+
function loadManifest(path7) {
|
|
3954
4055
|
let raw;
|
|
3955
4056
|
try {
|
|
3956
|
-
raw = readFileSync5(
|
|
4057
|
+
raw = readFileSync5(path7, "utf8");
|
|
3957
4058
|
} catch (err) {
|
|
3958
4059
|
const e = err;
|
|
3959
4060
|
if (e.code === "ENOENT") {
|
|
3960
|
-
return { kind: "file-not-found", path:
|
|
4061
|
+
return { kind: "file-not-found", path: path7 };
|
|
3961
4062
|
}
|
|
3962
|
-
return { kind: "read-error", path:
|
|
4063
|
+
return { kind: "read-error", path: path7, message: e.message ?? String(err) };
|
|
3963
4064
|
}
|
|
3964
|
-
return parseManifest2(raw,
|
|
4065
|
+
return parseManifest2(raw, path7);
|
|
3965
4066
|
}
|
|
3966
|
-
function parseManifest2(yamlText,
|
|
4067
|
+
function parseManifest2(yamlText, path7) {
|
|
3967
4068
|
let parsed;
|
|
3968
4069
|
try {
|
|
3969
4070
|
parsed = parseYaml3(yamlText);
|
|
3970
4071
|
} catch (err) {
|
|
3971
4072
|
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
3972
|
-
return { kind: "yaml-parse-error", path:
|
|
4073
|
+
return { kind: "yaml-parse-error", path: path7, message };
|
|
3973
4074
|
}
|
|
3974
4075
|
const result = ManifestSchema.safeParse(parsed);
|
|
3975
4076
|
if (result.success) {
|
|
@@ -3977,7 +4078,7 @@ function parseManifest2(yamlText, path6) {
|
|
|
3977
4078
|
}
|
|
3978
4079
|
return {
|
|
3979
4080
|
kind: "schema-error",
|
|
3980
|
-
path:
|
|
4081
|
+
path: path7,
|
|
3981
4082
|
issues: formatIssues(result.error)
|
|
3982
4083
|
};
|
|
3983
4084
|
}
|
|
@@ -4175,7 +4276,7 @@ function sleep(ms) {
|
|
|
4175
4276
|
return new Promise((res) => setTimeout(res, ms));
|
|
4176
4277
|
}
|
|
4177
4278
|
function deletePinIfPresent(cfg, slug, io) {
|
|
4178
|
-
const pinPath =
|
|
4279
|
+
const pinPath = join8(cfg.stateDir, slug, "group.json");
|
|
4179
4280
|
if (!existsSync3(pinPath))
|
|
4180
4281
|
return;
|
|
4181
4282
|
try {
|
|
@@ -4257,12 +4358,12 @@ function renderManifestError(loaded, io) {
|
|
|
4257
4358
|
|
|
4258
4359
|
// src/deploy/dry-run.ts
|
|
4259
4360
|
import { execFileSync } from "node:child_process";
|
|
4260
|
-
import { resolve as
|
|
4361
|
+
import { resolve as resolve6 } from "node:path";
|
|
4261
4362
|
var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
|
|
4262
4363
|
async function runDeployDryRun(opts, io, deps = {}) {
|
|
4263
4364
|
const cfg = deps.config ?? loadConfig();
|
|
4264
4365
|
const fetcher = deps.fetcher ?? fetch;
|
|
4265
|
-
const manifestPath =
|
|
4366
|
+
const manifestPath = resolve6(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
4266
4367
|
const loaded = loadManifest(manifestPath);
|
|
4267
4368
|
if (loaded.kind !== "ok") {
|
|
4268
4369
|
return renderManifestError2(loaded, opts.json, io);
|
|
@@ -4608,12 +4709,12 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
4608
4709
|
}
|
|
4609
4710
|
|
|
4610
4711
|
// src/commands/infer-slug.ts
|
|
4611
|
-
import * as
|
|
4712
|
+
import * as path7 from "node:path";
|
|
4612
4713
|
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
4613
4714
|
import { parse as parseYaml4 } from "yaml";
|
|
4614
4715
|
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4615
4716
|
function inferSlugFromCwd(cwd) {
|
|
4616
|
-
const base =
|
|
4717
|
+
const base = path7.basename(cwd);
|
|
4617
4718
|
const m = base.match(DIRNAME_RE);
|
|
4618
4719
|
return m === null ? null : m[1];
|
|
4619
4720
|
}
|
|
@@ -4638,11 +4739,11 @@ function inferSlugFromManifestFile(manifestPath) {
|
|
|
4638
4739
|
}
|
|
4639
4740
|
}
|
|
4640
4741
|
function inferSlug(opts) {
|
|
4641
|
-
const manifestPath =
|
|
4742
|
+
const manifestPath = path7.resolve(opts.cwd, opts.file ?? "launchpad.yaml");
|
|
4642
4743
|
const fromManifest = inferSlugFromManifestFile(manifestPath);
|
|
4643
4744
|
const fromDir = inferSlugFromCwd(opts.cwd);
|
|
4644
4745
|
if (fromManifest !== null && fromDir !== null && fromManifest !== fromDir) {
|
|
4645
|
-
opts.warn?.(`note: directory name suggests "${fromDir}" but ${
|
|
4746
|
+
opts.warn?.(`note: directory name suggests "${fromDir}" but ${path7.basename(manifestPath)} ` + `declares "${fromManifest}" — using the manifest. ` + `(Pass <slug> or --slug to override.)`);
|
|
4646
4747
|
}
|
|
4647
4748
|
return fromManifest ?? fromDir;
|
|
4648
4749
|
}
|
|
@@ -4698,9 +4799,18 @@ async function runDeploy(args, io) {
|
|
|
4698
4799
|
}, io);
|
|
4699
4800
|
}
|
|
4700
4801
|
const cwd = process.cwd();
|
|
4701
|
-
const manifestPath =
|
|
4802
|
+
const manifestPath = path8.join(cwd, "launchpad.yaml");
|
|
4702
4803
|
if (existsSync5(manifestPath)) {
|
|
4703
|
-
|
|
4804
|
+
const contentMode = flags.mode.kind === "content" ? flags.mode : { allowStale: false, rebaseOntoHead: false, overrideStale: null };
|
|
4805
|
+
return runModelADeploy({
|
|
4806
|
+
cwd,
|
|
4807
|
+
manifestPath,
|
|
4808
|
+
argv: args,
|
|
4809
|
+
io,
|
|
4810
|
+
allowStale: contentMode.allowStale,
|
|
4811
|
+
rebaseOntoHead: contentMode.rebaseOntoHead,
|
|
4812
|
+
overrideStale: contentMode.overrideStale
|
|
4813
|
+
});
|
|
4704
4814
|
}
|
|
4705
4815
|
const parsed = parseArgs2(args);
|
|
4706
4816
|
if (parsed === null) {
|
|
@@ -4713,7 +4823,7 @@ async function runDeploy(args, io) {
|
|
|
4713
4823
|
} else {
|
|
4714
4824
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
4715
4825
|
if (inferred === null) {
|
|
4716
|
-
io.err(`launchpad deploy: could not infer slug from cwd (${
|
|
4826
|
+
io.err(`launchpad deploy: could not infer slug from cwd (${path8.basename(process.cwd())});
|
|
4717
4827
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
4718
4828
|
return 64;
|
|
4719
4829
|
}
|
|
@@ -4733,7 +4843,7 @@ async function runDeploy(args, io) {
|
|
|
4733
4843
|
return 1;
|
|
4734
4844
|
}
|
|
4735
4845
|
let contentManifestYaml = null;
|
|
4736
|
-
const contentManifestPath =
|
|
4846
|
+
const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
|
|
4737
4847
|
if (existsSync5(contentManifestPath)) {
|
|
4738
4848
|
try {
|
|
4739
4849
|
contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
|
|
@@ -4885,7 +4995,16 @@ function formatBytes2(n) {
|
|
|
4885
4995
|
function describe13(e) {
|
|
4886
4996
|
return e instanceof Error ? e.message : String(e);
|
|
4887
4997
|
}
|
|
4888
|
-
function surfaceDeployExtras(body, io, slug) {
|
|
4998
|
+
function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
4999
|
+
const st = body.staleness;
|
|
5000
|
+
if (st !== undefined && !allowStale) {
|
|
5001
|
+
if (st.kind === "stale") {
|
|
5002
|
+
io.err(`warning: main has moved since you cloned (base ${st.base_sha.slice(0, 8)} → head ${(st.head_sha ?? "").slice(0, 8)}).`);
|
|
5003
|
+
io.err(` A teammate may have shipped changes you don't have. Review \`launchpad status ${slug}\` before relying on this deploy.`);
|
|
5004
|
+
} else {
|
|
5005
|
+
io.err(`warning: could not verify whether main has moved since you cloned (base ${st.base_sha.slice(0, 8)}); proceeding.`);
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
4889
5008
|
if (body.boundary_stripped !== undefined && body.boundary_stripped.length > 0) {
|
|
4890
5009
|
io.err(`warning: the bot stripped ${body.boundary_stripped.length} never-shippable file(s) server-side:`);
|
|
4891
5010
|
for (const p of body.boundary_stripped.slice(0, 10)) {
|
|
@@ -4907,6 +5026,32 @@ function surfaceDeployExtras(body, io, slug) {
|
|
|
4907
5026
|
io.out(` Full list: \`launchpad status ${slug}\`.`);
|
|
4908
5027
|
}
|
|
4909
5028
|
}
|
|
5029
|
+
function isStaleBlock(body) {
|
|
5030
|
+
const code = body?.error;
|
|
5031
|
+
return code === "stale_overlap" || code === "stale_unverifiable";
|
|
5032
|
+
}
|
|
5033
|
+
function surfaceStaleBlock(body, io) {
|
|
5034
|
+
const code = String(body.error);
|
|
5035
|
+
const head = typeof body.head_sha === "string" ? body.head_sha : null;
|
|
5036
|
+
if (code === "stale_overlap" && Array.isArray(body.paths)) {
|
|
5037
|
+
io.err(`launchpad deploy: BLOCKED — this deploy would roll back ${body.paths.length} file(s) a teammate changed on main since you cloned:`);
|
|
5038
|
+
for (const p of body.paths.slice(0, 20)) {
|
|
5039
|
+
io.err(` - ${String(p)}`);
|
|
5040
|
+
}
|
|
5041
|
+
if (body.paths.length > 20) {
|
|
5042
|
+
io.err(` … and ${body.paths.length - 20} more`);
|
|
5043
|
+
}
|
|
5044
|
+
} else {
|
|
5045
|
+
io.err(`launchpad deploy: BLOCKED — main moved since you cloned and the overlap cannot be proven` + (typeof body.reason === "string" ? ` (${body.reason})` : "") + `; treating as unsafe.`);
|
|
5046
|
+
}
|
|
5047
|
+
io.err("");
|
|
5048
|
+
io.err(" To recover (after reconciling the listed file(s) with main):");
|
|
5049
|
+
io.err(" launchpad deploy --rebase-onto-head");
|
|
5050
|
+
if (head !== null) {
|
|
5051
|
+
io.err(" Or, to deliberately overwrite them in place:");
|
|
5052
|
+
io.err(` launchpad deploy --override-stale ${head.slice(0, 8)}`);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
4910
5055
|
async function runModelADeploy(args) {
|
|
4911
5056
|
const { cwd, manifestPath, io } = args;
|
|
4912
5057
|
let slug;
|
|
@@ -4935,7 +5080,23 @@ async function runModelADeploy(args) {
|
|
|
4935
5080
|
}
|
|
4936
5081
|
let result;
|
|
4937
5082
|
try {
|
|
4938
|
-
result = await bundleAndDeploy({
|
|
5083
|
+
result = await bundleAndDeploy({
|
|
5084
|
+
cwd,
|
|
5085
|
+
cfg,
|
|
5086
|
+
slug,
|
|
5087
|
+
overrideStale: args.overrideStale
|
|
5088
|
+
});
|
|
5089
|
+
if (args.rebaseOntoHead && result.kind === "upload-error" && result.status === 409 && isStaleBlock(result.body)) {
|
|
5090
|
+
const head = result.body.head_sha;
|
|
5091
|
+
if (typeof head === "string" && isValidBaseSha(head)) {
|
|
5092
|
+
await writeBaseShaMarker(cwd, head);
|
|
5093
|
+
io.out(`Re-stamped base revision to current head ${head.slice(0, 12)}; retrying deploy …`);
|
|
5094
|
+
result = await bundleAndDeploy({ cwd, cfg, slug });
|
|
5095
|
+
} else {
|
|
5096
|
+
io.err("launchpad deploy: --rebase-onto-head could not read a head SHA from the block; re-clone the app instead.");
|
|
5097
|
+
return 1;
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
4939
5100
|
} catch (e) {
|
|
4940
5101
|
if (e instanceof UnauthenticatedError) {
|
|
4941
5102
|
io.err(`launchpad deploy: ${e.message}`);
|
|
@@ -4971,6 +5132,10 @@ async function runModelADeploy(args) {
|
|
|
4971
5132
|
}
|
|
4972
5133
|
return 1;
|
|
4973
5134
|
}
|
|
5135
|
+
if (result.status === 409 && isStaleBlock(body)) {
|
|
5136
|
+
surfaceStaleBlock(body, io);
|
|
5137
|
+
return 1;
|
|
5138
|
+
}
|
|
4974
5139
|
io.err(`launchpad deploy: bot rejected the upload (HTTP ${result.status}, ${errorCode}).`);
|
|
4975
5140
|
if (typeof body.message === "string") {
|
|
4976
5141
|
io.err(` ${body.message}`);
|
|
@@ -5004,7 +5169,7 @@ async function runModelADeploy(args) {
|
|
|
5004
5169
|
if (typeof success.head_sha === "string") {
|
|
5005
5170
|
io.out(` (managed main @ ${success.head_sha.slice(0, 8)} matches the bundle)`);
|
|
5006
5171
|
}
|
|
5007
|
-
surfaceDeployExtras(success, io, slug);
|
|
5172
|
+
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
5008
5173
|
return 0;
|
|
5009
5174
|
}
|
|
5010
5175
|
if (success.status === "provisioning_started") {
|
|
@@ -5041,7 +5206,7 @@ async function runModelADeploy(args) {
|
|
|
5041
5206
|
if (typeof success.message === "string") {
|
|
5042
5207
|
io.out(` ${success.message}`);
|
|
5043
5208
|
}
|
|
5044
|
-
surfaceDeployExtras(success, io, slug);
|
|
5209
|
+
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
5045
5210
|
io.out("");
|
|
5046
5211
|
io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
|
|
5047
5212
|
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
@@ -5051,7 +5216,7 @@ async function runModelADeploy(args) {
|
|
|
5051
5216
|
}
|
|
5052
5217
|
|
|
5053
5218
|
// src/commands/envvars.ts
|
|
5054
|
-
import * as
|
|
5219
|
+
import * as path9 from "node:path";
|
|
5055
5220
|
var envvarsCommand = {
|
|
5056
5221
|
name: "envvars",
|
|
5057
5222
|
summary: "list / set / remove production env vars (slug-scoped)",
|
|
@@ -5071,7 +5236,7 @@ async function runEnvvars(args, io) {
|
|
|
5071
5236
|
} else {
|
|
5072
5237
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
5073
5238
|
if (inferred === null) {
|
|
5074
|
-
io.err(`launchpad envvars: could not infer slug from cwd (${
|
|
5239
|
+
io.err(`launchpad envvars: could not infer slug from cwd (${path9.basename(process.cwd())});
|
|
5075
5240
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
5076
5241
|
return 64;
|
|
5077
5242
|
}
|
|
@@ -5242,7 +5407,7 @@ function describe14(e) {
|
|
|
5242
5407
|
|
|
5243
5408
|
// src/commands/generate.ts
|
|
5244
5409
|
import { mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
|
|
5245
|
-
import { dirname as
|
|
5410
|
+
import { dirname as dirname5, resolve as resolve8, relative as relative3 } from "node:path";
|
|
5246
5411
|
var generateCommand = {
|
|
5247
5412
|
name: "generate",
|
|
5248
5413
|
summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
|
|
@@ -5255,14 +5420,14 @@ async function runGenerate(args, io) {
|
|
|
5255
5420
|
io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
|
|
5256
5421
|
return 64;
|
|
5257
5422
|
}
|
|
5258
|
-
const manifestPath =
|
|
5423
|
+
const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
5259
5424
|
const result = loadManifest(manifestPath);
|
|
5260
5425
|
if (result.kind !== "ok") {
|
|
5261
5426
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
5262
5427
|
}
|
|
5263
|
-
const appRoot =
|
|
5264
|
-
const wranglerPath =
|
|
5265
|
-
const workflowPath =
|
|
5428
|
+
const appRoot = dirname5(manifestPath);
|
|
5429
|
+
const wranglerPath = resolve8(appRoot, "container", "wrangler.toml");
|
|
5430
|
+
const workflowPath = resolve8(appRoot, ".github", "workflows", "deploy.yml");
|
|
5266
5431
|
const wranglerOut = generateWranglerToml(result.manifest);
|
|
5267
5432
|
const workflowOut = generateGithubDeployWorkflow(result.manifest);
|
|
5268
5433
|
if (flags.dryRun) {
|
|
@@ -5277,7 +5442,7 @@ async function runGenerate(args, io) {
|
|
|
5277
5442
|
];
|
|
5278
5443
|
return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
|
|
5279
5444
|
}
|
|
5280
|
-
function applyOne(artefact,
|
|
5445
|
+
function applyOne(artefact, path10, out, force) {
|
|
5281
5446
|
if (out.kind === "not-applicable") {
|
|
5282
5447
|
return {
|
|
5283
5448
|
artefact,
|
|
@@ -5285,32 +5450,32 @@ function applyOne(artefact, path9, out, force) {
|
|
|
5285
5450
|
warnings: []
|
|
5286
5451
|
};
|
|
5287
5452
|
}
|
|
5288
|
-
const existing = readIfExists(
|
|
5453
|
+
const existing = readIfExists(path10);
|
|
5289
5454
|
if (existing.kind === "read-error") {
|
|
5290
5455
|
return {
|
|
5291
5456
|
artefact,
|
|
5292
|
-
action: { kind: "write-error", path:
|
|
5457
|
+
action: { kind: "write-error", path: path10, message: existing.message },
|
|
5293
5458
|
warnings: out.warnings
|
|
5294
5459
|
};
|
|
5295
5460
|
}
|
|
5296
5461
|
if (existing.kind === "ok" && existing.content === out.content) {
|
|
5297
|
-
return { artefact, action: { kind: "unchanged", path:
|
|
5462
|
+
return { artefact, action: { kind: "unchanged", path: path10 }, warnings: out.warnings };
|
|
5298
5463
|
}
|
|
5299
5464
|
if (existing.kind === "ok" && existing.content !== out.content && !force) {
|
|
5300
5465
|
return {
|
|
5301
5466
|
artefact,
|
|
5302
|
-
action: { kind: "would-overwrite", path:
|
|
5467
|
+
action: { kind: "would-overwrite", path: path10 },
|
|
5303
5468
|
warnings: out.warnings
|
|
5304
5469
|
};
|
|
5305
5470
|
}
|
|
5306
5471
|
try {
|
|
5307
|
-
mkdirSync(
|
|
5308
|
-
writeFileSync(
|
|
5472
|
+
mkdirSync(dirname5(path10), { recursive: true });
|
|
5473
|
+
writeFileSync(path10, out.content, "utf8");
|
|
5309
5474
|
return {
|
|
5310
5475
|
artefact,
|
|
5311
5476
|
action: {
|
|
5312
5477
|
kind: "written",
|
|
5313
|
-
path:
|
|
5478
|
+
path: path10,
|
|
5314
5479
|
bytes: Buffer.byteLength(out.content, "utf8")
|
|
5315
5480
|
},
|
|
5316
5481
|
warnings: out.warnings
|
|
@@ -5320,16 +5485,16 @@ function applyOne(artefact, path9, out, force) {
|
|
|
5320
5485
|
artefact,
|
|
5321
5486
|
action: {
|
|
5322
5487
|
kind: "write-error",
|
|
5323
|
-
path:
|
|
5488
|
+
path: path10,
|
|
5324
5489
|
message: err.message ?? String(err)
|
|
5325
5490
|
},
|
|
5326
5491
|
warnings: out.warnings
|
|
5327
5492
|
};
|
|
5328
5493
|
}
|
|
5329
5494
|
}
|
|
5330
|
-
function readIfExists(
|
|
5495
|
+
function readIfExists(path10) {
|
|
5331
5496
|
try {
|
|
5332
|
-
return { kind: "ok", content: readFileSync8(
|
|
5497
|
+
return { kind: "ok", content: readFileSync8(path10, "utf8") };
|
|
5333
5498
|
} catch (err) {
|
|
5334
5499
|
const e = err;
|
|
5335
5500
|
if (e.code === "ENOENT")
|
|
@@ -5541,12 +5706,12 @@ function parseFlags(args) {
|
|
|
5541
5706
|
|
|
5542
5707
|
// src/groups/client.ts
|
|
5543
5708
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5544
|
-
import { dirname as
|
|
5709
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
5545
5710
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
5546
5711
|
var CACHE_FILENAME = "groups.json";
|
|
5547
5712
|
async function fetchGroups(cfg, opts = {}) {
|
|
5548
5713
|
const now = opts.now ?? Date.now;
|
|
5549
|
-
const cachePath =
|
|
5714
|
+
const cachePath = join10(cfg.cacheDir, CACHE_FILENAME);
|
|
5550
5715
|
if (opts.forceRefresh !== true) {
|
|
5551
5716
|
const cached = readCache(cachePath);
|
|
5552
5717
|
if (cached !== null) {
|
|
@@ -5581,10 +5746,10 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
5581
5746
|
writeCache(cachePath, { fetchedAt, groups });
|
|
5582
5747
|
return { kind: "ok", source: "fresh", fetchedAt, groups };
|
|
5583
5748
|
}
|
|
5584
|
-
function readCache(
|
|
5749
|
+
function readCache(path10) {
|
|
5585
5750
|
let raw;
|
|
5586
5751
|
try {
|
|
5587
|
-
raw = readFileSync9(
|
|
5752
|
+
raw = readFileSync9(path10, "utf8");
|
|
5588
5753
|
} catch {
|
|
5589
5754
|
return null;
|
|
5590
5755
|
}
|
|
@@ -5612,10 +5777,10 @@ function isEntraGroup(value) {
|
|
|
5612
5777
|
const g = value;
|
|
5613
5778
|
return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
|
|
5614
5779
|
}
|
|
5615
|
-
function writeCache(
|
|
5780
|
+
function writeCache(path10, envelope) {
|
|
5616
5781
|
try {
|
|
5617
|
-
mkdirSync2(
|
|
5618
|
-
writeFileSync2(
|
|
5782
|
+
mkdirSync2(dirname6(path10), { recursive: true });
|
|
5783
|
+
writeFileSync2(path10, JSON.stringify(envelope), "utf8");
|
|
5619
5784
|
} catch {}
|
|
5620
5785
|
}
|
|
5621
5786
|
function describe15(e) {
|
|
@@ -6086,16 +6251,16 @@ function describe18(e) {
|
|
|
6086
6251
|
// src/commands/init.ts
|
|
6087
6252
|
import { createInterface } from "node:readline/promises";
|
|
6088
6253
|
import { existsSync as existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6089
|
-
import { resolve as
|
|
6254
|
+
import { resolve as resolve9 } from "node:path";
|
|
6090
6255
|
import { stringify as yamlStringify } from "yaml";
|
|
6091
6256
|
|
|
6092
6257
|
// src/detect/index.ts
|
|
6093
6258
|
import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
6094
|
-
import { join as
|
|
6259
|
+
import { join as join11 } from "node:path";
|
|
6095
6260
|
function detectAppShape(cwd) {
|
|
6096
|
-
const hasPackageJson = existsSync6(
|
|
6097
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(
|
|
6098
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(
|
|
6261
|
+
const hasPackageJson = existsSync6(join11(cwd, "package.json"));
|
|
6262
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join11(cwd, file)));
|
|
6263
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join11(cwd, n)));
|
|
6099
6264
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
6100
6265
|
return {
|
|
6101
6266
|
kind: "not-applicable",
|
|
@@ -6141,7 +6306,7 @@ var LOCKFILES = [
|
|
|
6141
6306
|
{ file: "yarn.lock", pm: "yarn" }
|
|
6142
6307
|
];
|
|
6143
6308
|
function detectPackageManager(cwd) {
|
|
6144
|
-
const present = LOCKFILES.filter(({ file }) => existsSync6(
|
|
6309
|
+
const present = LOCKFILES.filter(({ file }) => existsSync6(join11(cwd, file)));
|
|
6145
6310
|
if (present.length === 0) {
|
|
6146
6311
|
return {
|
|
6147
6312
|
kind: "ambiguous",
|
|
@@ -6163,7 +6328,7 @@ function detectPackageManager(cwd) {
|
|
|
6163
6328
|
}
|
|
6164
6329
|
function detectVitePresence(cwd) {
|
|
6165
6330
|
for (const name of VITE_CONFIG_NAMES) {
|
|
6166
|
-
const p =
|
|
6331
|
+
const p = join11(cwd, name);
|
|
6167
6332
|
if (existsSync6(p)) {
|
|
6168
6333
|
return { kind: "ok", value: { path: p } };
|
|
6169
6334
|
}
|
|
@@ -6174,7 +6339,7 @@ function detectVitePresence(cwd) {
|
|
|
6174
6339
|
};
|
|
6175
6340
|
}
|
|
6176
6341
|
function detectAppType(cwd) {
|
|
6177
|
-
const fnDir =
|
|
6342
|
+
const fnDir = join11(cwd, "functions");
|
|
6178
6343
|
let hasFunctionsDir = false;
|
|
6179
6344
|
if (existsSync6(fnDir)) {
|
|
6180
6345
|
try {
|
|
@@ -6186,7 +6351,7 @@ function detectAppType(cwd) {
|
|
|
6186
6351
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
6187
6352
|
}
|
|
6188
6353
|
function detectBuildCommand(cwd, pm) {
|
|
6189
|
-
const pkgJsonPath =
|
|
6354
|
+
const pkgJsonPath = join11(cwd, "package.json");
|
|
6190
6355
|
if (!existsSync6(pkgJsonPath)) {
|
|
6191
6356
|
return {
|
|
6192
6357
|
kind: "ambiguous",
|
|
@@ -6259,7 +6424,7 @@ async function runInit(args, io, prompt) {
|
|
|
6259
6424
|
return 64;
|
|
6260
6425
|
}
|
|
6261
6426
|
const { inputs, options } = parsed;
|
|
6262
|
-
const outPath =
|
|
6427
|
+
const outPath = resolve9(process.cwd(), options.out);
|
|
6263
6428
|
if (existsSync7(outPath) && !options.force) {
|
|
6264
6429
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
6265
6430
|
io.err("Pass --force to overwrite.");
|
|
@@ -6307,7 +6472,7 @@ async function runInit(args, io, prompt) {
|
|
|
6307
6472
|
}
|
|
6308
6473
|
io.out(`✓ wrote ${outPath}`);
|
|
6309
6474
|
if (options.gitignore) {
|
|
6310
|
-
const gitignorePath =
|
|
6475
|
+
const gitignorePath = resolve9(process.cwd(), ".gitignore");
|
|
6311
6476
|
try {
|
|
6312
6477
|
const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
|
|
6313
6478
|
if (changed.length > 0) {
|
|
@@ -6676,10 +6841,10 @@ function buildManifest(inputs, detected) {
|
|
|
6676
6841
|
function renderYaml(manifest) {
|
|
6677
6842
|
return yamlStringify(manifest, { lineWidth: 0 });
|
|
6678
6843
|
}
|
|
6679
|
-
function ensureGitignoreEntries(
|
|
6844
|
+
function ensureGitignoreEntries(path10, entries) {
|
|
6680
6845
|
let current = "";
|
|
6681
|
-
if (existsSync7(
|
|
6682
|
-
current = readFileSync11(
|
|
6846
|
+
if (existsSync7(path10)) {
|
|
6847
|
+
current = readFileSync11(path10, "utf8");
|
|
6683
6848
|
}
|
|
6684
6849
|
const lines = current.split(/\r?\n/);
|
|
6685
6850
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -6697,7 +6862,7 @@ function ensureGitignoreEntries(path9, entries) {
|
|
|
6697
6862
|
}
|
|
6698
6863
|
}
|
|
6699
6864
|
if (added.length > 0) {
|
|
6700
|
-
writeFileSync3(
|
|
6865
|
+
writeFileSync3(path10, out, { encoding: "utf8" });
|
|
6701
6866
|
}
|
|
6702
6867
|
return added;
|
|
6703
6868
|
}
|
|
@@ -6724,7 +6889,11 @@ function makeLoginCommand(deps = REAL_DEPS) {
|
|
|
6724
6889
|
run: (args, io) => runLogin(args, io, deps)
|
|
6725
6890
|
};
|
|
6726
6891
|
}
|
|
6727
|
-
async function runLogin(
|
|
6892
|
+
async function runLogin(args, io, deps) {
|
|
6893
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
6894
|
+
printLoginHelp(io);
|
|
6895
|
+
return 0;
|
|
6896
|
+
}
|
|
6728
6897
|
try {
|
|
6729
6898
|
const cfg = loadConfig();
|
|
6730
6899
|
if (cfg.authLegacy) {
|
|
@@ -6775,6 +6944,27 @@ async function runLegacyLogin(io, botUrl, sessionPath, deps) {
|
|
|
6775
6944
|
io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
|
|
6776
6945
|
return 0;
|
|
6777
6946
|
}
|
|
6947
|
+
function printLoginHelp(io) {
|
|
6948
|
+
io.out("launchpad login — authenticate and store a session.");
|
|
6949
|
+
io.out("");
|
|
6950
|
+
io.out("Usage:");
|
|
6951
|
+
io.out(" launchpad login Sign in via the browser and store a session");
|
|
6952
|
+
io.out("");
|
|
6953
|
+
io.out("Opens your browser to sign in with your M-KOPA Microsoft account");
|
|
6954
|
+
io.out("through the Launchpad auth gateway (loopback PKCE), then writes the");
|
|
6955
|
+
io.out("session to ~/.launchpad/session.json. The short-lived access token");
|
|
6956
|
+
io.out("refreshes silently as you use the CLI.");
|
|
6957
|
+
io.out("");
|
|
6958
|
+
io.out("Environment:");
|
|
6959
|
+
io.out(" LAUNCHPAD_AUTH_LEGACY=1 Force the legacy Cloudflare Access flow");
|
|
6960
|
+
io.out(" (deprecated; removed when the dual-auth");
|
|
6961
|
+
io.out(" window closes)");
|
|
6962
|
+
io.out(" LAUNCHPAD_AUTH_GATEWAY_URL Override the gateway base URL (testing)");
|
|
6963
|
+
io.out("");
|
|
6964
|
+
io.out("Exit codes:");
|
|
6965
|
+
io.out(" 0 signed in / session stored");
|
|
6966
|
+
io.out(" 1 login failed (network, browser, or gateway error)");
|
|
6967
|
+
}
|
|
6778
6968
|
function describe19(e) {
|
|
6779
6969
|
return e instanceof Error ? e.message : String(e);
|
|
6780
6970
|
}
|
|
@@ -6789,7 +6979,11 @@ function makeLogoutCommand(deps = REAL_DEPS2) {
|
|
|
6789
6979
|
run: (args, io) => runLogout(args, io, deps)
|
|
6790
6980
|
};
|
|
6791
6981
|
}
|
|
6792
|
-
async function runLogout(
|
|
6982
|
+
async function runLogout(args, io, deps) {
|
|
6983
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
6984
|
+
printLogoutHelp(io);
|
|
6985
|
+
return 0;
|
|
6986
|
+
}
|
|
6793
6987
|
try {
|
|
6794
6988
|
const cfg = loadConfig();
|
|
6795
6989
|
let session = null;
|
|
@@ -6819,12 +7013,28 @@ async function runLogout(_args, io, deps) {
|
|
|
6819
7013
|
return 1;
|
|
6820
7014
|
}
|
|
6821
7015
|
}
|
|
7016
|
+
function printLogoutHelp(io) {
|
|
7017
|
+
io.out("launchpad logout — revoke the session server-side and clear it locally.");
|
|
7018
|
+
io.out("");
|
|
7019
|
+
io.out("Usage:");
|
|
7020
|
+
io.out(" launchpad logout Revoke server-side, then clear the local session");
|
|
7021
|
+
io.out("");
|
|
7022
|
+
io.out("Gateway (v2) sessions are revoked server-side (the refresh token dies");
|
|
7023
|
+
io.out("immediately; any in-flight access token expires within ~15 minutes),");
|
|
7024
|
+
io.out("then the local ~/.launchpad/session.json is cleared. Logout also works");
|
|
7025
|
+
io.out("offline: if the gateway is unreachable the local session is cleared");
|
|
7026
|
+
io.out("anyway with a warning, and the exit code stays 0.");
|
|
7027
|
+
io.out("");
|
|
7028
|
+
io.out("Exit codes:");
|
|
7029
|
+
io.out(" 0 logged out (or already logged out)");
|
|
7030
|
+
io.out(" 1 could not clear the local session (file IO / config error)");
|
|
7031
|
+
}
|
|
6822
7032
|
function describe20(e) {
|
|
6823
7033
|
return e instanceof Error ? e.message : String(e);
|
|
6824
7034
|
}
|
|
6825
7035
|
|
|
6826
7036
|
// src/commands/logs.ts
|
|
6827
|
-
import * as
|
|
7037
|
+
import * as path10 from "node:path";
|
|
6828
7038
|
var logsCommand = {
|
|
6829
7039
|
name: "logs",
|
|
6830
7040
|
summary: "show recent Pages deployment history (slug-scoped)",
|
|
@@ -6846,7 +7056,7 @@ async function runLogs(args, io) {
|
|
|
6846
7056
|
} else {
|
|
6847
7057
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6848
7058
|
if (inferred === null) {
|
|
6849
|
-
io.err(`launchpad logs: could not infer slug from cwd (${
|
|
7059
|
+
io.err(`launchpad logs: could not infer slug from cwd (${path10.basename(process.cwd())});
|
|
6850
7060
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
6851
7061
|
return 64;
|
|
6852
7062
|
}
|
|
@@ -6974,7 +7184,7 @@ function describe21(e) {
|
|
|
6974
7184
|
}
|
|
6975
7185
|
|
|
6976
7186
|
// src/commands/merge.ts
|
|
6977
|
-
import * as
|
|
7187
|
+
import * as path11 from "node:path";
|
|
6978
7188
|
var mergeCommand = {
|
|
6979
7189
|
name: "merge",
|
|
6980
7190
|
summary: "squash-merge a review-passed PR (slug-scoped)",
|
|
@@ -6995,7 +7205,7 @@ async function runMerge(args, io) {
|
|
|
6995
7205
|
} else {
|
|
6996
7206
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6997
7207
|
if (inferred === null) {
|
|
6998
|
-
io.err(`launchpad merge: could not infer slug from cwd (${
|
|
7208
|
+
io.err(`launchpad merge: could not infer slug from cwd (${path11.basename(process.cwd())});
|
|
6999
7209
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
7000
7210
|
return 64;
|
|
7001
7211
|
}
|
|
@@ -7147,7 +7357,7 @@ function describe22(e) {
|
|
|
7147
7357
|
}
|
|
7148
7358
|
|
|
7149
7359
|
// src/commands/plan.ts
|
|
7150
|
-
import { resolve as
|
|
7360
|
+
import { resolve as resolve10 } from "node:path";
|
|
7151
7361
|
var planCommand = {
|
|
7152
7362
|
name: "plan",
|
|
7153
7363
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -7160,7 +7370,7 @@ async function runPlan(args, io) {
|
|
|
7160
7370
|
io.err("Usage: launchpad plan [--file <path>] [--json]");
|
|
7161
7371
|
return 64;
|
|
7162
7372
|
}
|
|
7163
|
-
const manifestPath =
|
|
7373
|
+
const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
7164
7374
|
const result = loadManifest(manifestPath);
|
|
7165
7375
|
return flags.json ? renderJson(result, io) : renderHuman(result, io);
|
|
7166
7376
|
}
|
|
@@ -7605,8 +7815,8 @@ import { stringify as stringifyYaml } from "yaml";
|
|
|
7605
7815
|
|
|
7606
7816
|
// src/deploy/manifest-state.ts
|
|
7607
7817
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
7608
|
-
const
|
|
7609
|
-
const raw = await apiJson(cfg, { path:
|
|
7818
|
+
const path12 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
|
|
7819
|
+
const raw = await apiJson(cfg, { path: path12 }, fetcher);
|
|
7610
7820
|
return {
|
|
7611
7821
|
slug: raw.slug,
|
|
7612
7822
|
hasAppFile: raw.hasAppFile,
|
|
@@ -7619,8 +7829,8 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
|
7619
7829
|
|
|
7620
7830
|
// src/deploy/manifest-status.ts
|
|
7621
7831
|
async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
7622
|
-
const
|
|
7623
|
-
return apiJson(cfg, { path:
|
|
7832
|
+
const path12 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
|
|
7833
|
+
return apiJson(cfg, { path: path12 }, fetcher);
|
|
7624
7834
|
}
|
|
7625
7835
|
|
|
7626
7836
|
// src/deploy/deployment-status.ts
|
|
@@ -8188,17 +8398,17 @@ async function runStatus(args, io) {
|
|
|
8188
8398
|
}
|
|
8189
8399
|
function computeDrift(local, deployed) {
|
|
8190
8400
|
const diffs = [];
|
|
8191
|
-
const cmp = (
|
|
8401
|
+
const cmp = (path12, l, d) => {
|
|
8192
8402
|
const li = l ?? null;
|
|
8193
8403
|
const di = d ?? null;
|
|
8194
8404
|
if (typeof li === "object" || typeof di === "object") {
|
|
8195
8405
|
if (JSON.stringify(li) !== JSON.stringify(di)) {
|
|
8196
|
-
diffs.push({ path:
|
|
8406
|
+
diffs.push({ path: path12, local: l, deployed: d });
|
|
8197
8407
|
}
|
|
8198
8408
|
return;
|
|
8199
8409
|
}
|
|
8200
8410
|
if (li !== di) {
|
|
8201
|
-
diffs.push({ path:
|
|
8411
|
+
diffs.push({ path: path12, local: l, deployed: d });
|
|
8202
8412
|
}
|
|
8203
8413
|
};
|
|
8204
8414
|
cmp("metadata.name", local.metadata.name, deployed.metadata.name);
|
|
@@ -8502,7 +8712,7 @@ function isEnoent(e) {
|
|
|
8502
8712
|
}
|
|
8503
8713
|
|
|
8504
8714
|
// src/commands/review.ts
|
|
8505
|
-
import * as
|
|
8715
|
+
import * as path12 from "node:path";
|
|
8506
8716
|
var reviewCommand = {
|
|
8507
8717
|
name: "review",
|
|
8508
8718
|
summary: "show the review state for a PR (slug-scoped)",
|
|
@@ -8522,7 +8732,7 @@ async function runReview(args, io) {
|
|
|
8522
8732
|
} else {
|
|
8523
8733
|
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
8524
8734
|
if (inferred === null) {
|
|
8525
|
-
io.err(`launchpad review: could not infer slug from cwd (${
|
|
8735
|
+
io.err(`launchpad review: could not infer slug from cwd (${path12.basename(process.cwd())});
|
|
8526
8736
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
8527
8737
|
return 64;
|
|
8528
8738
|
}
|
|
@@ -8779,16 +8989,16 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8779
8989
|
yes: true
|
|
8780
8990
|
}, io, applyDeps);
|
|
8781
8991
|
}
|
|
8782
|
-
function readCurrentManifest(
|
|
8783
|
-
if (!existsSync8(
|
|
8992
|
+
function readCurrentManifest(path13) {
|
|
8993
|
+
if (!existsSync8(path13))
|
|
8784
8994
|
return null;
|
|
8785
8995
|
let raw;
|
|
8786
8996
|
try {
|
|
8787
|
-
raw = readFileSync13(
|
|
8997
|
+
raw = readFileSync13(path13, "utf8");
|
|
8788
8998
|
} catch {
|
|
8789
8999
|
return null;
|
|
8790
9000
|
}
|
|
8791
|
-
const parsed = parseManifest2(raw,
|
|
9001
|
+
const parsed = parseManifest2(raw, path13);
|
|
8792
9002
|
if (parsed.kind !== "ok")
|
|
8793
9003
|
return null;
|
|
8794
9004
|
return summarise(parsed.manifest);
|
|
@@ -9283,23 +9493,23 @@ var CELL_LABEL2 = {
|
|
|
9283
9493
|
not_deployed: "NOT_DEPLOYED"
|
|
9284
9494
|
};
|
|
9285
9495
|
function loadSet(fleetFile, setName, io) {
|
|
9286
|
-
const
|
|
9287
|
-
if (!existsSync10(
|
|
9288
|
-
io.err(`✗ ${
|
|
9496
|
+
const path13 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
9497
|
+
if (!existsSync10(path13)) {
|
|
9498
|
+
io.err(`✗ ${path13}`);
|
|
9289
9499
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
9290
9500
|
return 2;
|
|
9291
9501
|
}
|
|
9292
9502
|
let obj;
|
|
9293
9503
|
try {
|
|
9294
|
-
obj = parseYaml6(readFileSync15(
|
|
9504
|
+
obj = parseYaml6(readFileSync15(path13, "utf8"));
|
|
9295
9505
|
} catch (e) {
|
|
9296
|
-
io.err(`✗ ${
|
|
9506
|
+
io.err(`✗ ${path13}`);
|
|
9297
9507
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
9298
9508
|
return 1;
|
|
9299
9509
|
}
|
|
9300
9510
|
const parsed = parseFleetSecretSets(obj);
|
|
9301
9511
|
if (!parsed.ok) {
|
|
9302
|
-
io.err(`✗ ${
|
|
9512
|
+
io.err(`✗ ${path13}`);
|
|
9303
9513
|
io.err(` ${parsed.issues.length} schema issue(s):`);
|
|
9304
9514
|
for (const i of parsed.issues)
|
|
9305
9515
|
io.err(` ${i.path}: ${i.message}`);
|
|
@@ -9308,7 +9518,7 @@ function loadSet(fleetFile, setName, io) {
|
|
|
9308
9518
|
const set = parsed.manifest.secretSets.find((s) => s.name === setName);
|
|
9309
9519
|
if (set === undefined) {
|
|
9310
9520
|
const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
|
|
9311
|
-
io.err(`✗ secret-set "${setName}" not found in ${
|
|
9521
|
+
io.err(`✗ secret-set "${setName}" not found in ${path13}`);
|
|
9312
9522
|
io.err(` declared sets: ${names}`);
|
|
9313
9523
|
return 1;
|
|
9314
9524
|
}
|
|
@@ -9528,7 +9738,7 @@ function setPushExit(e) {
|
|
|
9528
9738
|
|
|
9529
9739
|
// src/commands/secrets-template.ts
|
|
9530
9740
|
import { existsSync as existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9531
|
-
import { resolve as
|
|
9741
|
+
import { resolve as resolve11 } from "node:path";
|
|
9532
9742
|
async function runSecretsTemplate(args, io) {
|
|
9533
9743
|
const flags = parseFlags4(args);
|
|
9534
9744
|
if (flags.kind === "usage-error") {
|
|
@@ -9536,7 +9746,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
9536
9746
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
9537
9747
|
return 64;
|
|
9538
9748
|
}
|
|
9539
|
-
const manifestPath =
|
|
9749
|
+
const manifestPath = resolve11(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
9540
9750
|
const result = loadManifest(manifestPath);
|
|
9541
9751
|
const renderResult = renderManifest(result, io);
|
|
9542
9752
|
if (renderResult.kind !== "ok") {
|
|
@@ -9550,7 +9760,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
9550
9760
|
}
|
|
9551
9761
|
return 0;
|
|
9552
9762
|
}
|
|
9553
|
-
const outPath =
|
|
9763
|
+
const outPath = resolve11(process.cwd(), flags.out);
|
|
9554
9764
|
if (existsSync11(outPath) && !flags.force) {
|
|
9555
9765
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
9556
9766
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
@@ -9839,8 +10049,8 @@ function printHelp2(io) {
|
|
|
9839
10049
|
|
|
9840
10050
|
// src/commands/skills.ts
|
|
9841
10051
|
import { fileURLToPath } from "node:url";
|
|
9842
|
-
import { dirname as
|
|
9843
|
-
import { promises as
|
|
10052
|
+
import { dirname as dirname7, join as join12, resolve as resolve12 } from "node:path";
|
|
10053
|
+
import { promises as fs6, existsSync as existsSync12 } from "node:fs";
|
|
9844
10054
|
import { homedir as homedir2 } from "node:os";
|
|
9845
10055
|
var BUNDLE_PREFIX = "launchpad-";
|
|
9846
10056
|
var BUNDLED_SKILLS = [
|
|
@@ -9901,17 +10111,17 @@ function printHelp3(io) {
|
|
|
9901
10111
|
}
|
|
9902
10112
|
function resolveInstallEnv() {
|
|
9903
10113
|
const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
|
|
9904
|
-
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
10114
|
+
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join12(homedir2(), ".claude", "skills");
|
|
9905
10115
|
return { bundleDir, userSkillsDir };
|
|
9906
10116
|
}
|
|
9907
10117
|
function defaultBundleDir() {
|
|
9908
|
-
const here =
|
|
10118
|
+
const here = dirname7(fileURLToPath(import.meta.url));
|
|
9909
10119
|
const candidates = [
|
|
9910
|
-
|
|
9911
|
-
|
|
10120
|
+
resolve12(here, "..", "skills"),
|
|
10121
|
+
resolve12(here, "..", "..", "skills")
|
|
9912
10122
|
];
|
|
9913
10123
|
for (const c of candidates) {
|
|
9914
|
-
if (existsSync12(
|
|
10124
|
+
if (existsSync12(join12(c, "launchpad-onboard", "SKILL.md"))) {
|
|
9915
10125
|
return c;
|
|
9916
10126
|
}
|
|
9917
10127
|
}
|
|
@@ -9927,19 +10137,19 @@ async function doInstall(io) {
|
|
|
9927
10137
|
return 1;
|
|
9928
10138
|
}
|
|
9929
10139
|
if (!await isDir(env.userSkillsDir)) {
|
|
9930
|
-
await
|
|
10140
|
+
await fs6.mkdir(env.userSkillsDir, { recursive: true });
|
|
9931
10141
|
io.out(`Created ${env.userSkillsDir}.`);
|
|
9932
10142
|
}
|
|
9933
10143
|
let installed = 0;
|
|
9934
10144
|
for (const skill of BUNDLED_SKILLS) {
|
|
9935
|
-
const src =
|
|
10145
|
+
const src = join12(env.bundleDir, skill);
|
|
9936
10146
|
if (!await isDir(src)) {
|
|
9937
10147
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
9938
10148
|
return 1;
|
|
9939
10149
|
}
|
|
9940
|
-
const dest =
|
|
9941
|
-
await
|
|
9942
|
-
await
|
|
10150
|
+
const dest = join12(env.userSkillsDir, skill);
|
|
10151
|
+
await fs6.rm(dest, { recursive: true, force: true });
|
|
10152
|
+
await fs6.cp(src, dest, { recursive: true });
|
|
9943
10153
|
installed++;
|
|
9944
10154
|
io.out(`✓ ${skill} → ${dest}`);
|
|
9945
10155
|
}
|
|
@@ -9955,15 +10165,15 @@ async function doUninstall(io) {
|
|
|
9955
10165
|
io.out(`launchpad skills uninstall: ${env.userSkillsDir} does not exist — nothing to do.`);
|
|
9956
10166
|
return 0;
|
|
9957
10167
|
}
|
|
9958
|
-
const entries = await
|
|
10168
|
+
const entries = await fs6.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
9959
10169
|
let removed = 0;
|
|
9960
10170
|
for (const entry of entries) {
|
|
9961
10171
|
if (!entry.isDirectory())
|
|
9962
10172
|
continue;
|
|
9963
10173
|
if (!isBundleManaged(entry.name))
|
|
9964
10174
|
continue;
|
|
9965
|
-
const target =
|
|
9966
|
-
await
|
|
10175
|
+
const target = join12(env.userSkillsDir, entry.name);
|
|
10176
|
+
await fs6.rm(target, { recursive: true, force: true });
|
|
9967
10177
|
removed++;
|
|
9968
10178
|
io.out(`✗ removed ${target}`);
|
|
9969
10179
|
}
|
|
@@ -9977,7 +10187,7 @@ async function doList(io) {
|
|
|
9977
10187
|
io.out("(none — ~/.claude/skills/ does not exist)");
|
|
9978
10188
|
return 0;
|
|
9979
10189
|
}
|
|
9980
|
-
const entries = await
|
|
10190
|
+
const entries = await fs6.readdir(env.userSkillsDir, { withFileTypes: true });
|
|
9981
10191
|
const managedDirs = entries.filter((e) => e.isDirectory() && isBundleManaged(e.name)).map((e) => e.name).sort();
|
|
9982
10192
|
if (managedDirs.length === 0) {
|
|
9983
10193
|
io.out("(none — run `launchpad skills install`)");
|
|
@@ -9985,16 +10195,16 @@ async function doList(io) {
|
|
|
9985
10195
|
}
|
|
9986
10196
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
9987
10197
|
for (const name of managedDirs) {
|
|
9988
|
-
const skillFile =
|
|
10198
|
+
const skillFile = join12(env.userSkillsDir, name, "SKILL.md");
|
|
9989
10199
|
const version = await readVersion(skillFile);
|
|
9990
10200
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
9991
10201
|
}
|
|
9992
10202
|
return 0;
|
|
9993
10203
|
}
|
|
9994
|
-
async function readVersion(
|
|
10204
|
+
async function readVersion(path13) {
|
|
9995
10205
|
let text;
|
|
9996
10206
|
try {
|
|
9997
|
-
text = await
|
|
10207
|
+
text = await fs6.readFile(path13, "utf8");
|
|
9998
10208
|
} catch {
|
|
9999
10209
|
return null;
|
|
10000
10210
|
}
|
|
@@ -10004,9 +10214,9 @@ async function readVersion(path12) {
|
|
|
10004
10214
|
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
10005
10215
|
return m === null ? null : m[1] ?? null;
|
|
10006
10216
|
}
|
|
10007
|
-
async function isDir(
|
|
10217
|
+
async function isDir(path13) {
|
|
10008
10218
|
try {
|
|
10009
|
-
const stat = await
|
|
10219
|
+
const stat = await fs6.stat(path13);
|
|
10010
10220
|
return stat.isDirectory();
|
|
10011
10221
|
} catch {
|
|
10012
10222
|
return false;
|
|
@@ -10020,7 +10230,7 @@ function describe29(e) {
|
|
|
10020
10230
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
10021
10231
|
import { promisify } from "node:util";
|
|
10022
10232
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10023
|
-
import { dirname as
|
|
10233
|
+
import { dirname as dirname8, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as join13 } from "node:path";
|
|
10024
10234
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
10025
10235
|
import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
10026
10236
|
|
|
@@ -10065,9 +10275,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
10065
10275
|
resolveCode(code);
|
|
10066
10276
|
}
|
|
10067
10277
|
});
|
|
10068
|
-
const bound = await new Promise((
|
|
10069
|
-
server.once("error", () =>
|
|
10070
|
-
server.listen(0, "127.0.0.1", () =>
|
|
10278
|
+
const bound = await new Promise((resolve13) => {
|
|
10279
|
+
server.once("error", () => resolve13(false));
|
|
10280
|
+
server.listen(0, "127.0.0.1", () => resolve13(true));
|
|
10071
10281
|
});
|
|
10072
10282
|
if (!bound)
|
|
10073
10283
|
return null;
|
|
@@ -10143,7 +10353,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
10143
10353
|
var REGISTRY = "https://registry.npmjs.org";
|
|
10144
10354
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
10145
10355
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
10146
|
-
var CHANNEL_MARKER =
|
|
10356
|
+
var CHANNEL_MARKER = join13(homedir3(), ".launchpad", "channel");
|
|
10147
10357
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
10148
10358
|
var UPGRADE_ARGS = {
|
|
10149
10359
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -10210,6 +10420,7 @@ function compareVersions(a, b) {
|
|
|
10210
10420
|
}
|
|
10211
10421
|
return 0;
|
|
10212
10422
|
}
|
|
10423
|
+
var NOTIFIER_OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
10213
10424
|
function defaultDeps() {
|
|
10214
10425
|
const channel = detectInstallChannel();
|
|
10215
10426
|
return {
|
|
@@ -10218,16 +10429,42 @@ function defaultDeps() {
|
|
|
10218
10429
|
fetchLatestVersion: channel === "platform" ? fetchLatestFromChannel : fetchLatestVersion,
|
|
10219
10430
|
detectPm: detectPackageManager2,
|
|
10220
10431
|
runUpgrade,
|
|
10221
|
-
runChannelInstaller
|
|
10432
|
+
runChannelInstaller,
|
|
10433
|
+
syncSkills: runSkillsSync
|
|
10222
10434
|
};
|
|
10223
10435
|
}
|
|
10436
|
+
async function runSkillsSync() {
|
|
10437
|
+
const cliPath = process.argv[1];
|
|
10438
|
+
if (!cliPath)
|
|
10439
|
+
return { ok: false };
|
|
10440
|
+
return await new Promise((resolvePromise) => {
|
|
10441
|
+
try {
|
|
10442
|
+
const child = spawn5(process.execPath, [cliPath, "skills", "update"], {
|
|
10443
|
+
stdio: "ignore",
|
|
10444
|
+
env: { ...process.env, [NOTIFIER_OPT_OUT_ENV]: "1" }
|
|
10445
|
+
});
|
|
10446
|
+
child.on("error", () => resolvePromise({ ok: false }));
|
|
10447
|
+
child.on("close", (code) => resolvePromise({ ok: code === 0 }));
|
|
10448
|
+
} catch {
|
|
10449
|
+
resolvePromise({ ok: false });
|
|
10450
|
+
}
|
|
10451
|
+
});
|
|
10452
|
+
}
|
|
10453
|
+
async function syncSkillsAfterUpgrade(io, deps) {
|
|
10454
|
+
const result = await deps.syncSkills();
|
|
10455
|
+
if (result.ok) {
|
|
10456
|
+
io.out("✓ Claude Code skill bundle re-synced to the new version.");
|
|
10457
|
+
} else {
|
|
10458
|
+
io.err("Note: couldn't auto-sync the skill bundle — run `launchpad skills update` to finish.");
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10224
10461
|
async function openSystemBrowser(url) {
|
|
10225
10462
|
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
10226
10463
|
await execFileAsync(opener, [url]);
|
|
10227
10464
|
}
|
|
10228
10465
|
async function runInstallerScript(script) {
|
|
10229
|
-
const dir = mkdtempSync(
|
|
10230
|
-
const file =
|
|
10466
|
+
const dir = mkdtempSync(join13(tmpdir(), "launchpad-update-"));
|
|
10467
|
+
const file = join13(dir, "install.sh");
|
|
10231
10468
|
try {
|
|
10232
10469
|
writeFileSync7(file, script, { mode: 448 });
|
|
10233
10470
|
return await new Promise((resolvePromise) => {
|
|
@@ -10314,7 +10551,7 @@ function errStderr(e) {
|
|
|
10314
10551
|
return "";
|
|
10315
10552
|
}
|
|
10316
10553
|
async function detectPackageManager2() {
|
|
10317
|
-
const pkgRoot =
|
|
10554
|
+
const pkgRoot = resolve13(dirname8(fileURLToPath2(import.meta.url)), "..", "..");
|
|
10318
10555
|
const candidates = [];
|
|
10319
10556
|
const npmRoot = await pmRoot("npm");
|
|
10320
10557
|
if (npmRoot !== null)
|
|
@@ -10322,7 +10559,7 @@ async function detectPackageManager2() {
|
|
|
10322
10559
|
const pnpmRoot = await pmRoot("pnpm");
|
|
10323
10560
|
if (pnpmRoot !== null)
|
|
10324
10561
|
candidates.push(["pnpm", pnpmRoot]);
|
|
10325
|
-
candidates.push(["bun",
|
|
10562
|
+
candidates.push(["bun", resolve13(homedir3(), ".bun/install/global/node_modules")]);
|
|
10326
10563
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
10327
10564
|
return matches.length === 1 ? matches[0][0] : null;
|
|
10328
10565
|
}
|
|
@@ -10394,6 +10631,7 @@ async function runUpdate(args, io, deps) {
|
|
|
10394
10631
|
io.out(`✓ launchpad-cli updated to ${latest.version}.`);
|
|
10395
10632
|
io.out("If `launchpad --version` still shows the old version, open a new");
|
|
10396
10633
|
io.out("shell (or check for a PATH-shadowing bun/pnpm global install).");
|
|
10634
|
+
await syncSkillsAfterUpgrade(io, deps);
|
|
10397
10635
|
return 0;
|
|
10398
10636
|
}
|
|
10399
10637
|
io.err("");
|
|
@@ -10430,8 +10668,7 @@ async function runUpdate(args, io, deps) {
|
|
|
10430
10668
|
}
|
|
10431
10669
|
io.out("");
|
|
10432
10670
|
io.out(`✓ launchpad-cli updated to ${latest.version}.`);
|
|
10433
|
-
io
|
|
10434
|
-
io.out("(the CLI and the skill bundle are version-locked together).");
|
|
10671
|
+
await syncSkillsAfterUpgrade(io, deps);
|
|
10435
10672
|
return 0;
|
|
10436
10673
|
}
|
|
10437
10674
|
function printHelp4(io) {
|
|
@@ -10459,7 +10696,7 @@ function printHelp4(io) {
|
|
|
10459
10696
|
|
|
10460
10697
|
// src/commands/validate.ts
|
|
10461
10698
|
import { readFileSync as readFileSync17 } from "node:fs";
|
|
10462
|
-
import { dirname as
|
|
10699
|
+
import { dirname as dirname9, resolve as resolve14 } from "node:path";
|
|
10463
10700
|
var validateCommand = {
|
|
10464
10701
|
name: "validate",
|
|
10465
10702
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -10473,17 +10710,17 @@ async function runValidate(args, io) {
|
|
|
10473
10710
|
return 64;
|
|
10474
10711
|
}
|
|
10475
10712
|
const { file, json, strictGroups } = parseResult;
|
|
10476
|
-
const
|
|
10477
|
-
const result = loadManifest(
|
|
10713
|
+
const path13 = resolve14(process.cwd(), file ?? "launchpad.yaml");
|
|
10714
|
+
const result = loadManifest(path13);
|
|
10478
10715
|
if (result.kind !== "ok") {
|
|
10479
10716
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
10480
10717
|
}
|
|
10481
|
-
const boundary = checkBoundary(
|
|
10718
|
+
const boundary = checkBoundary(path13, result.manifest.app !== undefined);
|
|
10482
10719
|
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
|
|
10483
10720
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
10484
10721
|
}
|
|
10485
10722
|
function checkBoundary(manifestPath, declared) {
|
|
10486
|
-
const dir =
|
|
10723
|
+
const dir = dirname9(manifestPath);
|
|
10487
10724
|
let files;
|
|
10488
10725
|
try {
|
|
10489
10726
|
files = walkCwd(dir).files;
|
|
@@ -10806,12 +11043,18 @@ function describe31(e) {
|
|
|
10806
11043
|
// src/update-notifier.ts
|
|
10807
11044
|
import { spawn as spawn6 } from "node:child_process";
|
|
10808
11045
|
import { homedir as homedir4 } from "node:os";
|
|
10809
|
-
import { join as
|
|
10810
|
-
import {
|
|
11046
|
+
import { join as join14 } from "node:path";
|
|
11047
|
+
import {
|
|
11048
|
+
existsSync as existsSync13,
|
|
11049
|
+
mkdirSync as mkdirSync3,
|
|
11050
|
+
readFileSync as readFileSync18,
|
|
11051
|
+
readdirSync as readdirSync2,
|
|
11052
|
+
writeFileSync as writeFileSync8
|
|
11053
|
+
} from "node:fs";
|
|
10811
11054
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
10812
11055
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10813
11056
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
10814
|
-
var CACHE_FILE =
|
|
11057
|
+
var CACHE_FILE = join14(homedir4(), ".launchpad", "update-check.json");
|
|
10815
11058
|
function readCache2() {
|
|
10816
11059
|
try {
|
|
10817
11060
|
const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
|
|
@@ -10827,11 +11070,53 @@ function readCache2() {
|
|
|
10827
11070
|
}
|
|
10828
11071
|
function writeCache2(state) {
|
|
10829
11072
|
try {
|
|
10830
|
-
mkdirSync3(
|
|
11073
|
+
mkdirSync3(join14(homedir4(), ".launchpad"), { recursive: true });
|
|
10831
11074
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
10832
11075
|
`, { mode: 384 });
|
|
10833
11076
|
} catch {}
|
|
10834
11077
|
}
|
|
11078
|
+
var SKILL_PREFIX = "launchpad-";
|
|
11079
|
+
var SKILLS_HINT_MARKER = join14(homedir4(), ".launchpad", "skills-hint-shown");
|
|
11080
|
+
function skillsTargetDir() {
|
|
11081
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir4(), ".claude", "skills");
|
|
11082
|
+
}
|
|
11083
|
+
function readInstalledSkills() {
|
|
11084
|
+
try {
|
|
11085
|
+
const dir = skillsTargetDir();
|
|
11086
|
+
const bundle = readdirSync2(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
|
|
11087
|
+
if (!bundle)
|
|
11088
|
+
return { present: false, version: null };
|
|
11089
|
+
return { present: true, version: readSkillVersion(join14(dir, bundle.name, "SKILL.md")) };
|
|
11090
|
+
} catch {
|
|
11091
|
+
return { present: false, version: null };
|
|
11092
|
+
}
|
|
11093
|
+
}
|
|
11094
|
+
function readSkillVersion(path13) {
|
|
11095
|
+
try {
|
|
11096
|
+
const text = readFileSync18(path13, "utf8");
|
|
11097
|
+
const fenceEnd = text.indexOf(`
|
|
11098
|
+
---`, 4);
|
|
11099
|
+
const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
|
|
11100
|
+
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
11101
|
+
return m === null ? null : m[1] ?? null;
|
|
11102
|
+
} catch {
|
|
11103
|
+
return null;
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
11106
|
+
function absentHintShown() {
|
|
11107
|
+
try {
|
|
11108
|
+
return existsSync13(SKILLS_HINT_MARKER);
|
|
11109
|
+
} catch {
|
|
11110
|
+
return false;
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
function markAbsentHintShown() {
|
|
11114
|
+
try {
|
|
11115
|
+
mkdirSync3(join14(homedir4(), ".launchpad"), { recursive: true });
|
|
11116
|
+
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
11117
|
+
`, { mode: 384 });
|
|
11118
|
+
} catch {}
|
|
11119
|
+
}
|
|
10835
11120
|
function isSuppressed(ctx) {
|
|
10836
11121
|
if (ctx.env[OPT_OUT_ENV])
|
|
10837
11122
|
return true;
|
|
@@ -10850,6 +11135,12 @@ function noticeLine(current, latest, channel) {
|
|
|
10850
11135
|
const how = channel === "platform" ? `re-run the installer at ${CHANNEL_INSTALL_URL}` : "run `launchpad update`";
|
|
10851
11136
|
return `▲ launchpad ${current} → ${latest} available — ${how} (silence with ${OPT_OUT_ENV}=1).`;
|
|
10852
11137
|
}
|
|
11138
|
+
function skillsDriftLine(bundleVersion, cliVersion) {
|
|
11139
|
+
return `▲ launchpad skill bundle ${bundleVersion} is behind the CLI ${cliVersion} — run \`launchpad skills update\` (silence with ${OPT_OUT_ENV}=1).`;
|
|
11140
|
+
}
|
|
11141
|
+
function skillsAbsentLine() {
|
|
11142
|
+
return `▲ launchpad Claude Code skills aren't installed — run \`launchpad skills install\` to add them (silence with ${OPT_OUT_ENV}=1).`;
|
|
11143
|
+
}
|
|
10853
11144
|
function maybeNotify(io, ctx, deps) {
|
|
10854
11145
|
if (isSuppressed(ctx))
|
|
10855
11146
|
return;
|
|
@@ -10857,6 +11148,15 @@ function maybeNotify(io, ctx, deps) {
|
|
|
10857
11148
|
if (cache?.latest && compareVersions(cache.latest, deps.cliVersion) === 1) {
|
|
10858
11149
|
io.err(noticeLine(deps.cliVersion, cache.latest, deps.channel()));
|
|
10859
11150
|
}
|
|
11151
|
+
const skills = deps.readInstalledSkills();
|
|
11152
|
+
if (!skills.present) {
|
|
11153
|
+
if (!deps.absentHintShown()) {
|
|
11154
|
+
io.err(skillsAbsentLine());
|
|
11155
|
+
deps.markAbsentHintShown();
|
|
11156
|
+
}
|
|
11157
|
+
} else if (skills.version && compareVersions(deps.cliVersion, skills.version) === 1) {
|
|
11158
|
+
io.err(skillsDriftLine(skills.version, deps.cliVersion));
|
|
11159
|
+
}
|
|
10860
11160
|
if (cache === null || deps.now() - cache.checkedAt > CHECK_INTERVAL_MS) {
|
|
10861
11161
|
deps.spawnRefresh();
|
|
10862
11162
|
}
|
|
@@ -10880,7 +11180,10 @@ function notifyAfterCommand(io, argv, cliPath = process.argv[1]) {
|
|
|
10880
11180
|
channel: detectInstallChannel,
|
|
10881
11181
|
readCache: readCache2,
|
|
10882
11182
|
now: Date.now,
|
|
10883
|
-
spawnRefresh: () => spawnDetachedRefresh(cliPath)
|
|
11183
|
+
spawnRefresh: () => spawnDetachedRefresh(cliPath),
|
|
11184
|
+
readInstalledSkills,
|
|
11185
|
+
absentHintShown,
|
|
11186
|
+
markAbsentHintShown
|
|
10884
11187
|
});
|
|
10885
11188
|
} catch {}
|
|
10886
11189
|
}
|