@m-kopa/launchpad-cli 0.54.0 → 0.54.1

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 CHANGED
@@ -8,6 +8,14 @@ pre-1.0 minor bumps may carry breaking changes per ADR 0005.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.54.1 - 2026-07-29
12
+
13
+ **Deleted application files now stay deleted after deployment.**
14
+
15
+ - The legacy clone deploy path excludes tracked files that no longer exist in the working tree, allowing the server to derive their deletion from the complete upload.
16
+ - The platform applies updates and proved app-owned deletions in one Git tree while preserving governance, workflow, ownership, Wrangler, Launchpad metadata, and out-of-boundary files.
17
+ - The server uses exact managed-tree enumeration before absence can authorize deletion and returns a retryable failure when that proof is unavailable.
18
+
11
19
  ## 0.54.0 - 2026-07-19
12
20
 
13
21
  **Server-visible CLI compatibility, with dormant minimum-version enforcement.**
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.54.0";
22
+ var CLI_VERSION = "0.54.1";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -1867,7 +1867,7 @@ function describe10(e) {
1867
1867
  }
1868
1868
 
1869
1869
  // src/commands/deploy.ts
1870
- import { existsSync as existsSync6 } from "node:fs";
1870
+ import { existsSync as existsSync7 } from "node:fs";
1871
1871
  import * as path8 from "node:path";
1872
1872
 
1873
1873
  // src/bundle/orchestrate.ts
@@ -2657,6 +2657,7 @@ var ManifestSchema = z.object({
2657
2657
  app: AppBoundarySchema.optional(),
2658
2658
  verify: VerifySchema.optional(),
2659
2659
  confine_origin: z.boolean().optional(),
2660
+ origin: z.enum(["pages", "dispatch"]).optional(),
2660
2661
  catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
2661
2662
  sensitive: z.boolean().optional()
2662
2663
  }).strict().superRefine((m, ctx) => {
@@ -3047,7 +3048,9 @@ var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
3047
3048
  ".github/scripts/parse-tsc.mjs",
3048
3049
  ".github/scripts/parse-vitest.mjs",
3049
3050
  ".github/scripts/parse-gitleaks.mjs",
3050
- ".github/scripts/parse-bun-audit.mjs"
3051
+ ".github/scripts/parse-bun-audit.mjs",
3052
+ ".github/scripts/parse-html-validate.mjs",
3053
+ ".github/scripts/parse-hadolint.mjs"
3051
3054
  ]);
3052
3055
  function checkDenyList(path7) {
3053
3056
  const segments = path7.split("/");
@@ -3290,6 +3293,7 @@ var SpecSchema = z2.object({
3290
3293
  app: AppBoundarySchema.optional(),
3291
3294
  verify: VerifySchema.optional(),
3292
3295
  confine_origin: z2.boolean().optional(),
3296
+ origin: z2.enum(["pages", "dispatch"]).optional(),
3293
3297
  sensitive: z2.boolean().optional(),
3294
3298
  catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
3295
3299
  env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
@@ -3335,6 +3339,43 @@ var SpecSchema = z2.object({
3335
3339
  });
3336
3340
  }
3337
3341
  }
3342
+ if (s.origin === "dispatch") {
3343
+ if (isContainer) {
3344
+ ctx.addIssue({
3345
+ code: z2.ZodIssueCode.custom,
3346
+ path: ["origin"],
3347
+ message: "origin: dispatch is not available for container apps — they run on the container runtime (ADR 0015/0016), not in a Workers dispatch namespace."
3348
+ });
3349
+ }
3350
+ if ((s.auth ?? "access") !== "gateway") {
3351
+ ctx.addIssue({
3352
+ code: z2.ZodIssueCode.custom,
3353
+ path: ["origin"],
3354
+ message: "origin: dispatch requires an explicit auth: gateway (auth defaults to 'access' when omitted). A dispatch-namespace Worker has no public hostname for Cloudflare Access to gate, so the gateway must be the sole gate."
3355
+ });
3356
+ }
3357
+ if (s.confine_origin !== undefined) {
3358
+ ctx.addIssue({
3359
+ code: z2.ZodIssueCode.custom,
3360
+ path: ["confine_origin"],
3361
+ message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
3362
+ });
3363
+ }
3364
+ if (s.env_vars !== undefined) {
3365
+ ctx.addIssue({
3366
+ code: z2.ZodIssueCode.custom,
3367
+ path: ["env_vars"],
3368
+ message: "env_vars are not yet supported with origin: dispatch — a dispatch workspace has no Pages module to apply them, and accepting them here would silently drop them. Worker-level vars land with the v2 deploy path."
3369
+ });
3370
+ }
3371
+ if (s.build !== undefined) {
3372
+ ctx.addIssue({
3373
+ code: z2.ZodIssueCode.custom,
3374
+ path: ["build"],
3375
+ message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
3376
+ });
3377
+ }
3378
+ }
3338
3379
  if (!isContainer && s.containers !== undefined) {
3339
3380
  ctx.addIssue({
3340
3381
  code: z2.ZodIssueCode.custom,
@@ -6185,6 +6226,8 @@ import { readFileSync as readFileSync10 } from "node:fs";
6185
6226
 
6186
6227
  // src/deploy/git-files.ts
6187
6228
  import { spawn as spawn3 } from "node:child_process";
6229
+ import { existsSync as existsSync5 } from "node:fs";
6230
+ import { resolve as resolve7 } from "node:path";
6188
6231
 
6189
6232
  class GitFilesError extends Error {
6190
6233
  code = "git_files_error";
@@ -6202,6 +6245,7 @@ var PROTECTED_PATHS = new Set([
6202
6245
  ]);
6203
6246
  async function listDeployFiles(opts) {
6204
6247
  const sp = opts.spawner ?? spawn3;
6248
+ const exists = opts.exists ?? existsSync5;
6205
6249
  const stdout = await runGit2(sp, opts.cwd, [
6206
6250
  "ls-files",
6207
6251
  "-c",
@@ -6209,11 +6253,11 @@ async function listDeployFiles(opts) {
6209
6253
  "--exclude-standard",
6210
6254
  "-z"
6211
6255
  ]);
6212
- const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
6256
+ const paths = stdout.split("\x00").filter((p) => p.length > 0 && exists(resolve7(opts.cwd, p)) && !PROTECTED_PATHS.has(p));
6213
6257
  return [...paths].sort();
6214
6258
  }
6215
6259
  function runGit2(sp, cwd, args) {
6216
- return new Promise((resolve7, reject) => {
6260
+ return new Promise((resolve8, reject) => {
6217
6261
  const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
6218
6262
  const child = sp("git", [...args], spawnOpts);
6219
6263
  let stdout = "";
@@ -6229,7 +6273,7 @@ function runGit2(sp, cwd, args) {
6229
6273
  });
6230
6274
  child.once("close", (code) => {
6231
6275
  if (code === 0) {
6232
- resolve7(stdout);
6276
+ resolve8(stdout);
6233
6277
  return;
6234
6278
  }
6235
6279
  reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
@@ -6806,7 +6850,7 @@ function parseDeployFlags(args) {
6806
6850
  }
6807
6851
 
6808
6852
  // src/deploy/apply.ts
6809
- import { existsSync as existsSync5, rmSync } from "node:fs";
6853
+ import { existsSync as existsSync6, rmSync } from "node:fs";
6810
6854
  import { resolve as resolvePath, join as join10 } from "node:path";
6811
6855
 
6812
6856
  // src/manifest/load.ts
@@ -7173,7 +7217,7 @@ function sleep(ms) {
7173
7217
  }
7174
7218
  function deletePinIfPresent(cfg, slug, io) {
7175
7219
  const pinPath = join10(cfg.stateDir, slug, "group.json");
7176
- if (!existsSync5(pinPath))
7220
+ if (!existsSync6(pinPath))
7177
7221
  return;
7178
7222
  try {
7179
7223
  rmSync(pinPath, { force: true });
@@ -7311,12 +7355,12 @@ async function handleAccessDrift(params) {
7311
7355
 
7312
7356
  // src/deploy/dry-run.ts
7313
7357
  import { execFileSync } from "node:child_process";
7314
- import { resolve as resolve7 } from "node:path";
7358
+ import { resolve as resolve8 } from "node:path";
7315
7359
  var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
7316
7360
  async function runDeployDryRun(opts, io, deps = {}) {
7317
7361
  const cfg = deps.config ?? loadConfig();
7318
7362
  const fetcher = deps.fetcher ?? fetch;
7319
- const manifestPath = resolve7(process.cwd(), opts.file ?? "launchpad.yaml");
7363
+ const manifestPath = resolve8(process.cwd(), opts.file ?? "launchpad.yaml");
7320
7364
  const loaded = loadManifest(manifestPath);
7321
7365
  if (loaded.kind !== "ok") {
7322
7366
  return renderManifestError2(loaded, opts.json, io);
@@ -7726,7 +7770,7 @@ async function runDeploy(args, io) {
7726
7770
  }
7727
7771
  const cwd = process.cwd();
7728
7772
  const manifestPath = path8.join(cwd, "launchpad.yaml");
7729
- if (existsSync6(manifestPath)) {
7773
+ if (existsSync7(manifestPath)) {
7730
7774
  const contentMode = flags.mode.kind === "content" ? flags.mode : {
7731
7775
  allowStale: false,
7732
7776
  rebaseOntoHead: false,
@@ -7780,7 +7824,7 @@ async function runDeploy(args, io) {
7780
7824
  }
7781
7825
  let contentManifestYaml = null;
7782
7826
  const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
7783
- if (existsSync6(contentManifestPath)) {
7827
+ if (existsSync7(contentManifestPath)) {
7784
7828
  try {
7785
7829
  contentManifestYaml = readFileSync10(contentManifestPath, "utf8");
7786
7830
  } catch {
@@ -8003,11 +8047,11 @@ function surfaceStaleBlock(body, io) {
8003
8047
  }
8004
8048
  var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
8005
8049
  function withTimeout(p, ms) {
8006
- return new Promise((resolve8, reject) => {
8050
+ return new Promise((resolve9, reject) => {
8007
8051
  const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
8008
8052
  p.then((v) => {
8009
8053
  clearTimeout(timer);
8010
- resolve8(v);
8054
+ resolve9(v);
8011
8055
  }, (e) => {
8012
8056
  clearTimeout(timer);
8013
8057
  reject(e instanceof Error ? e : new Error(String(e)));
@@ -8366,7 +8410,7 @@ function pendingReconcileOutcome(body) {
8366
8410
  }
8367
8411
 
8368
8412
  // src/commands/redeploy.ts
8369
- import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
8413
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
8370
8414
  import { resolve as resolvePath2 } from "node:path";
8371
8415
  import { parse as parseYaml7 } from "yaml";
8372
8416
  var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
@@ -8444,7 +8488,7 @@ async function runRedeploy(opts, io, deps = {}) {
8444
8488
  }
8445
8489
  function readLocalAppType(cwd, file) {
8446
8490
  const manifestPath = resolvePath2(cwd, file ?? "launchpad.yaml");
8447
- if (!existsSync7(manifestPath))
8491
+ if (!existsSync8(manifestPath))
8448
8492
  return null;
8449
8493
  try {
8450
8494
  const obj = parseYaml7(readFileSync11(manifestPath, "utf8"));
@@ -8732,7 +8776,7 @@ function describe16(e) {
8732
8776
 
8733
8777
  // src/commands/generate.ts
8734
8778
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
8735
- import { dirname as dirname6, resolve as resolve8, relative as relative3 } from "node:path";
8779
+ import { dirname as dirname6, resolve as resolve9, relative as relative3 } from "node:path";
8736
8780
  var generateCommand = {
8737
8781
  name: "generate",
8738
8782
  summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
@@ -8745,14 +8789,14 @@ async function runGenerate(args, io) {
8745
8789
  io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
8746
8790
  return 64;
8747
8791
  }
8748
- const manifestPath = resolve8(process.cwd(), flags.file ?? "launchpad.yaml");
8792
+ const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
8749
8793
  const result = loadManifest(manifestPath);
8750
8794
  if (result.kind !== "ok") {
8751
8795
  return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
8752
8796
  }
8753
8797
  const appRoot = dirname6(manifestPath);
8754
- const wranglerPath = resolve8(appRoot, "container", "wrangler.toml");
8755
- const workflowPath = resolve8(appRoot, ".github", "workflows", "deploy.yml");
8798
+ const wranglerPath = resolve9(appRoot, "container", "wrangler.toml");
8799
+ const workflowPath = resolve9(appRoot, ".github", "workflows", "deploy.yml");
8756
8800
  const wranglerOut = generateWranglerToml(result.manifest);
8757
8801
  const workflowOut = generateGithubDeployWorkflow(result.manifest);
8758
8802
  if (flags.dryRun) {
@@ -9557,17 +9601,17 @@ function describe19(e) {
9557
9601
 
9558
9602
  // src/commands/init.ts
9559
9603
  import { createInterface } from "node:readline/promises";
9560
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9561
- import { dirname as dirname7, resolve as resolve9 } from "node:path";
9604
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
9605
+ import { dirname as dirname7, resolve as resolve10 } from "node:path";
9562
9606
  import { stringify as yamlStringify } from "yaml";
9563
9607
 
9564
9608
  // src/detect/index.ts
9565
- import { existsSync as existsSync8, readFileSync as readFileSync13, statSync } from "node:fs";
9609
+ import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
9566
9610
  import { join as join12 } from "node:path";
9567
9611
  function detectAppShape(cwd) {
9568
- const hasPackageJson = existsSync8(join12(cwd, "package.json"));
9569
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync8(join12(cwd, file)));
9570
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync8(join12(cwd, n)));
9612
+ const hasPackageJson = existsSync9(join12(cwd, "package.json"));
9613
+ const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join12(cwd, file)));
9614
+ const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join12(cwd, n)));
9571
9615
  if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
9572
9616
  return {
9573
9617
  kind: "not-applicable",
@@ -9613,7 +9657,7 @@ var LOCKFILES = [
9613
9657
  { file: "yarn.lock", pm: "yarn" }
9614
9658
  ];
9615
9659
  function detectPackageManager(cwd) {
9616
- const present = LOCKFILES.filter(({ file }) => existsSync8(join12(cwd, file)));
9660
+ const present = LOCKFILES.filter(({ file }) => existsSync9(join12(cwd, file)));
9617
9661
  if (present.length === 0) {
9618
9662
  return {
9619
9663
  kind: "ambiguous",
@@ -9636,7 +9680,7 @@ function detectPackageManager(cwd) {
9636
9680
  function detectVitePresence(cwd) {
9637
9681
  for (const name of VITE_CONFIG_NAMES) {
9638
9682
  const p = join12(cwd, name);
9639
- if (existsSync8(p)) {
9683
+ if (existsSync9(p)) {
9640
9684
  return { kind: "ok", value: { path: p } };
9641
9685
  }
9642
9686
  }
@@ -9648,7 +9692,7 @@ function detectVitePresence(cwd) {
9648
9692
  function detectAppType(cwd) {
9649
9693
  const fnDir = join12(cwd, "functions");
9650
9694
  let hasFunctionsDir = false;
9651
- if (existsSync8(fnDir)) {
9695
+ if (existsSync9(fnDir)) {
9652
9696
  try {
9653
9697
  hasFunctionsDir = statSync(fnDir).isDirectory();
9654
9698
  } catch {
@@ -9659,7 +9703,7 @@ function detectAppType(cwd) {
9659
9703
  }
9660
9704
  function detectBuildCommand(cwd, pm) {
9661
9705
  const pkgJsonPath = join12(cwd, "package.json");
9662
- if (!existsSync8(pkgJsonPath)) {
9706
+ if (!existsSync9(pkgJsonPath)) {
9663
9707
  return {
9664
9708
  kind: "ambiguous",
9665
9709
  reason: "no package.json at repo root. Run your package manager's `init` first."
@@ -9731,8 +9775,8 @@ async function runInit(args, io, prompt) {
9731
9775
  return 64;
9732
9776
  }
9733
9777
  const { inputs, options } = parsed;
9734
- const outPath = resolve9(process.cwd(), options.out);
9735
- if (existsSync9(outPath) && !options.force) {
9778
+ const outPath = resolve10(process.cwd(), options.out);
9779
+ if (existsSync10(outPath) && !options.force) {
9736
9780
  io.err(`launchpad init: ${outPath} already exists`);
9737
9781
  io.err("Pass --force to overwrite.");
9738
9782
  return 64;
@@ -9779,7 +9823,7 @@ async function runInit(args, io, prompt) {
9779
9823
  }
9780
9824
  io.out(`✓ wrote ${outPath}`);
9781
9825
  if (options.gitignore) {
9782
- const gitignorePath = resolve9(process.cwd(), ".gitignore");
9826
+ const gitignorePath = resolve10(process.cwd(), ".gitignore");
9783
9827
  try {
9784
9828
  const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
9785
9829
  if (changed.length > 0) {
@@ -9794,9 +9838,9 @@ async function runInit(args, io, prompt) {
9794
9838
  }
9795
9839
  }
9796
9840
  if (fillResult.inputs.type === "static") {
9797
- const serveDir = resolve9(dirname7(outPath), STATIC_SERVE_DIR);
9798
- const indexPath = resolve9(serveDir, "index.html");
9799
- if (!existsSync9(indexPath)) {
9841
+ const serveDir = resolve10(dirname7(outPath), STATIC_SERVE_DIR);
9842
+ const indexPath = resolve10(serveDir, "index.html");
9843
+ if (!existsSync10(indexPath)) {
9800
9844
  try {
9801
9845
  mkdirSync3(serveDir, { recursive: true });
9802
9846
  writeFileSync3(indexPath, starterIndexHtml(fillResult.inputs.name), {
@@ -10188,7 +10232,7 @@ function renderYaml(manifest) {
10188
10232
  }
10189
10233
  function ensureGitignoreEntries(path10, entries) {
10190
10234
  let current = "";
10191
- if (existsSync9(path10)) {
10235
+ if (existsSync10(path10)) {
10192
10236
  current = readFileSync14(path10, "utf8");
10193
10237
  }
10194
10238
  const lines = current.split(/\r?\n/);
@@ -10717,7 +10761,7 @@ function describe23(e) {
10717
10761
  }
10718
10762
 
10719
10763
  // src/commands/plan.ts
10720
- import { resolve as resolve10 } from "node:path";
10764
+ import { resolve as resolve11 } from "node:path";
10721
10765
  var planCommand = {
10722
10766
  name: "plan",
10723
10767
  summary: "summarise what the manifest would deploy (offline)",
@@ -10730,7 +10774,7 @@ async function runPlan(args, io) {
10730
10774
  io.err("Usage: launchpad plan [--file <path>] [--json]");
10731
10775
  return 64;
10732
10776
  }
10733
- const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
10777
+ const manifestPath = resolve11(process.cwd(), flags.file ?? "launchpad.yaml");
10734
10778
  const result = loadManifest(manifestPath);
10735
10779
  return flags.json ? renderJson(result, io) : renderHuman(result, io);
10736
10780
  }
@@ -11320,12 +11364,12 @@ async function listFiles(root, relative5 = "") {
11320
11364
  return files.sort();
11321
11365
  }
11322
11366
  function gitOutput(cwd, args) {
11323
- return new Promise((resolve12, reject) => {
11367
+ return new Promise((resolve13, reject) => {
11324
11368
  execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
11325
11369
  if (error !== null)
11326
11370
  reject(error);
11327
11371
  else
11328
- resolve12(stdout);
11372
+ resolve13(stdout);
11329
11373
  });
11330
11374
  });
11331
11375
  }
@@ -12260,7 +12304,7 @@ function describe26(e) {
12260
12304
  }
12261
12305
 
12262
12306
  // src/deploy/rollback.ts
12263
- import { existsSync as existsSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
12307
+ import { existsSync as existsSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
12264
12308
  import { resolve as resolvePath3 } from "node:path";
12265
12309
  import { createInterface as createInterface3 } from "node:readline/promises";
12266
12310
 
@@ -12385,7 +12429,7 @@ async function runRollback(opts, io, deps = {}) {
12385
12429
  }, io, applyDeps);
12386
12430
  }
12387
12431
  function readCurrentManifest(path14) {
12388
- if (!existsSync10(path14))
12432
+ if (!existsSync11(path14))
12389
12433
  return null;
12390
12434
  let raw;
12391
12435
  try {
@@ -12545,7 +12589,7 @@ function parseArgs12(args) {
12545
12589
  }
12546
12590
 
12547
12591
  // src/secrets/push.ts
12548
- import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
12592
+ import { existsSync as existsSync12, readFileSync as readFileSync16 } from "node:fs";
12549
12593
  import { resolve as resolvePath4 } from "node:path";
12550
12594
  import { createInterface as createInterface4 } from "node:readline/promises";
12551
12595
  import { stdin as input2, stdout as output2 } from "node:process";
@@ -12602,7 +12646,7 @@ async function runSecretsPush(opts, io, deps = {}) {
12602
12646
  return 0;
12603
12647
  }
12604
12648
  const envPath = resolvePath4(process.cwd(), opts.env ?? ".env");
12605
- if (!existsSync11(envPath)) {
12649
+ if (!existsSync12(envPath)) {
12606
12650
  io.err(`launchpad secrets push: ${envPath}`);
12607
12651
  io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
12608
12652
  return 2;
@@ -12934,7 +12978,7 @@ function renderManifestError5(result, io) {
12934
12978
  }
12935
12979
 
12936
12980
  // src/secrets/set.ts
12937
- import { existsSync as existsSync12, readFileSync as readFileSync17 } from "node:fs";
12981
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
12938
12982
  import { resolve as resolvePath6 } from "node:path";
12939
12983
  import { parse as parseYaml8 } from "yaml";
12940
12984
  var CELL_LABEL2 = {
@@ -12944,7 +12988,7 @@ var CELL_LABEL2 = {
12944
12988
  };
12945
12989
  function loadSet(fleetFile, setName, io) {
12946
12990
  const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
12947
- if (!existsSync12(path14)) {
12991
+ if (!existsSync13(path14)) {
12948
12992
  io.err(`✗ ${path14}`);
12949
12993
  io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
12950
12994
  return 2;
@@ -13036,7 +13080,7 @@ async function runSecretsPushSet(opts, io, deps = {}) {
13036
13080
  if (typeof set === "number")
13037
13081
  return set;
13038
13082
  const envPath = resolvePath6(process.cwd(), opts.env ?? ".env");
13039
- if (!existsSync12(envPath)) {
13083
+ if (!existsSync13(envPath)) {
13040
13084
  io.err(`launchpad secrets push --set: ${envPath} not found.`);
13041
13085
  io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
13042
13086
  return 2;
@@ -13187,8 +13231,8 @@ function setPushExit(e) {
13187
13231
  }
13188
13232
 
13189
13233
  // src/commands/secrets-template.ts
13190
- import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "node:fs";
13191
- import { resolve as resolve12 } from "node:path";
13234
+ import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
13235
+ import { resolve as resolve13 } from "node:path";
13192
13236
  async function runSecretsTemplate(args, io) {
13193
13237
  const flags = parseFlags4(args);
13194
13238
  if (flags.kind === "usage-error") {
@@ -13196,7 +13240,7 @@ async function runSecretsTemplate(args, io) {
13196
13240
  io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
13197
13241
  return 64;
13198
13242
  }
13199
- const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
13243
+ const manifestPath = resolve13(process.cwd(), flags.file ?? "launchpad.yaml");
13200
13244
  const result = loadManifest(manifestPath);
13201
13245
  const renderResult = renderManifest(result, io);
13202
13246
  if (renderResult.kind !== "ok") {
@@ -13210,8 +13254,8 @@ async function runSecretsTemplate(args, io) {
13210
13254
  }
13211
13255
  return 0;
13212
13256
  }
13213
- const outPath = resolve12(process.cwd(), flags.out);
13214
- if (existsSync13(outPath) && !flags.force) {
13257
+ const outPath = resolve13(process.cwd(), flags.out);
13258
+ if (existsSync14(outPath) && !flags.force) {
13215
13259
  io.err(`launchpad secrets template: ${outPath} already exists`);
13216
13260
  io.err("Pass --force to overwrite, or --stdout to print without writing.");
13217
13261
  return 64;
@@ -13507,8 +13551,8 @@ function printHelp3(io) {
13507
13551
 
13508
13552
  // src/commands/skills.ts
13509
13553
  import { fileURLToPath } from "node:url";
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";
13554
+ import { dirname as dirname9, join as join15, resolve as resolve14 } from "node:path";
13555
+ import { promises as fs7, existsSync as existsSync15 } from "node:fs";
13512
13556
 
13513
13557
  // src/skills-bundle.ts
13514
13558
  import { homedir as homedir2 } from "node:os";
@@ -13613,11 +13657,11 @@ function resolveInstallEnv() {
13613
13657
  function defaultBundleDir() {
13614
13658
  const here = dirname9(fileURLToPath(import.meta.url));
13615
13659
  const candidates = [
13616
- resolve13(here, "..", "skills"),
13617
- resolve13(here, "..", "..", "skills")
13660
+ resolve14(here, "..", "skills"),
13661
+ resolve14(here, "..", "..", "skills")
13618
13662
  ];
13619
13663
  for (const c of candidates) {
13620
- if (existsSync14(join15(c, "launchpad-onboard", "SKILL.md"))) {
13664
+ if (existsSync15(join15(c, "launchpad-onboard", "SKILL.md"))) {
13621
13665
  return c;
13622
13666
  }
13623
13667
  }
@@ -13726,7 +13770,7 @@ function describe29(e) {
13726
13770
  import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
13727
13771
  import { promisify } from "node:util";
13728
13772
  import { fileURLToPath as fileURLToPath2 } from "node:url";
13729
- import { dirname as dirname10, resolve as resolve14, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
13773
+ import { dirname as dirname10, resolve as resolve15, relative as relative5, isAbsolute as isAbsolute3, join as join16 } from "node:path";
13730
13774
  import { homedir as homedir3, tmpdir } from "node:os";
13731
13775
  import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
13732
13776
 
@@ -13771,9 +13815,9 @@ async function startLoopback(state, timeoutMs) {
13771
13815
  resolveCode(code);
13772
13816
  }
13773
13817
  });
13774
- const bound = await new Promise((resolve14) => {
13775
- server.once("error", () => resolve14(false));
13776
- server.listen(0, "127.0.0.1", () => resolve14(true));
13818
+ const bound = await new Promise((resolve15) => {
13819
+ server.once("error", () => resolve15(false));
13820
+ server.listen(0, "127.0.0.1", () => resolve15(true));
13777
13821
  });
13778
13822
  if (!bound)
13779
13823
  return null;
@@ -14066,7 +14110,7 @@ function errStderr(e) {
14066
14110
  return "";
14067
14111
  }
14068
14112
  async function detectPackageManager2() {
14069
- const pkgRoot = resolve14(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
14113
+ const pkgRoot = resolve15(dirname10(fileURLToPath2(import.meta.url)), "..", "..");
14070
14114
  const candidates = [];
14071
14115
  const npmRoot = await pmRoot("npm");
14072
14116
  if (npmRoot !== null)
@@ -14074,7 +14118,7 @@ async function detectPackageManager2() {
14074
14118
  const pnpmRoot = await pmRoot("pnpm");
14075
14119
  if (pnpmRoot !== null)
14076
14120
  candidates.push(["pnpm", pnpmRoot]);
14077
- candidates.push(["bun", resolve14(homedir3(), ".bun/install/global/node_modules")]);
14121
+ candidates.push(["bun", resolve15(homedir3(), ".bun/install/global/node_modules")]);
14078
14122
  const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
14079
14123
  return matches.length === 1 ? matches[0][0] : null;
14080
14124
  }
@@ -14211,8 +14255,8 @@ function printHelp5(io) {
14211
14255
  }
14212
14256
 
14213
14257
  // src/commands/validate.ts
14214
- import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14215
- import { dirname as dirname11, resolve as resolve15 } from "node:path";
14258
+ import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
14259
+ import { dirname as dirname11, resolve as resolve16 } from "node:path";
14216
14260
  var validateCommand = {
14217
14261
  name: "validate",
14218
14262
  summary: "validate launchpad.yaml against the v1alpha1 schema",
@@ -14226,7 +14270,7 @@ async function runValidate(args, io) {
14226
14270
  return 64;
14227
14271
  }
14228
14272
  const { file, json, strictGroups } = parseResult;
14229
- const path14 = resolve15(process.cwd(), file ?? "launchpad.yaml");
14273
+ const path14 = resolve16(process.cwd(), file ?? "launchpad.yaml");
14230
14274
  const result = loadManifest(path14);
14231
14275
  if (result.kind !== "ok") {
14232
14276
  return json ? renderJsonError(result, io) : renderHumanError(result, io);
@@ -14273,8 +14317,8 @@ function checkStaticServeDir(manifestPath, manifest) {
14273
14317
  ];
14274
14318
  }
14275
14319
  const dir = dirname11(manifestPath);
14276
- const abs = resolve15(dir, serveDir);
14277
- if (!existsSync15(abs)) {
14320
+ const abs = resolve16(dir, serveDir);
14321
+ if (!existsSync16(abs)) {
14278
14322
  return [
14279
14323
  `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).`
14280
14324
  ];
@@ -14299,7 +14343,7 @@ function checkStaticServeDir(manifestPath, manifest) {
14299
14343
  `static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
14300
14344
  ];
14301
14345
  }
14302
- if (!existsSync15(resolve15(abs, "index.html"))) {
14346
+ if (!existsSync16(resolve16(abs, "index.html"))) {
14303
14347
  return [
14304
14348
  `static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
14305
14349
  ];
@@ -15170,7 +15214,7 @@ import { spawn as spawn6 } from "node:child_process";
15170
15214
  import { homedir as homedir4 } from "node:os";
15171
15215
  import { join as join17 } from "node:path";
15172
15216
  import {
15173
- existsSync as existsSync16,
15217
+ existsSync as existsSync17,
15174
15218
  mkdirSync as mkdirSync4,
15175
15219
  readFileSync as readFileSync21,
15176
15220
  writeFileSync as writeFileSync8
@@ -15202,7 +15246,7 @@ function writeCache2(state) {
15202
15246
  var SKILLS_HINT_MARKER = join17(homedir4(), ".launchpad", "skills-hint-shown");
15203
15247
  function absentHintShown() {
15204
15248
  try {
15205
- return existsSync16(SKILLS_HINT_MARKER);
15249
+ return existsSync17(SKILLS_HINT_MARKER);
15206
15250
  } catch {
15207
15251
  return false;
15208
15252
  }
@@ -15313,7 +15357,7 @@ var refreshUpdateCacheCommand = {
15313
15357
  import { spawn as spawn7 } from "node:child_process";
15314
15358
  import { homedir as homedir5 } from "node:os";
15315
15359
  import { join as join18 } from "node:path";
15316
- import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
15360
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
15317
15361
  import { randomUUID } from "node:crypto";
15318
15362
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
15319
15363
  var POSTHOG_HOST = "https://us.i.posthog.com";
@@ -15467,7 +15511,7 @@ function telemetryNoticeLine() {
15467
15511
  }
15468
15512
  function firstRunNoticeShown() {
15469
15513
  try {
15470
- return existsSync17(FIRST_RUN_MARKER);
15514
+ return existsSync18(FIRST_RUN_MARKER);
15471
15515
  } catch {
15472
15516
  return false;
15473
15517
  }
@@ -3,23 +3,19 @@ export declare class GitFilesError extends Error {
3
3
  readonly code: "git_files_error";
4
4
  }
5
5
  /**
6
- * Paths the bot owns and rejects unconditionally on `deploy-pr`
7
- * (see `PROTECTED_PATHS` in `portal-bot/src/deploy-pr.ts`). Mirrored
8
- * here so the CLI excludes them up front rather than shipping a
9
- * tarball the bot will reject.
6
+ * Legacy convenience filter for known bot-owned files.
10
7
  *
11
- * The bot uses `base_tree` mode on the commit, so files we omit
12
- * from the tarball keep their base-branch contents — there's no
13
- * destructive consequence to filtering these out.
14
- *
15
- * Keep in lock-step with the bot's list. A future endpoint that
16
- * exposes this list would let the CLI fetch it dynamically; for
17
- * v1 the set is small and rarely changes.
8
+ * This list is not a security boundary and does not grant deletion
9
+ * authority. The server owns the canonical, family-based path policy
10
+ * and preserves platform files even when an older or newer client
11
+ * sends a different set.
18
12
  */
19
13
  export declare const PROTECTED_PATHS: ReadonlySet<string>;
20
14
  export interface ListFilesOptions {
21
15
  readonly cwd: string;
22
16
  readonly spawner?: typeof spawn;
17
+ /** Test seam for the worktree existence check. Receives an absolute path. */
18
+ readonly exists?: (path: string) => boolean;
23
19
  }
24
20
  /**
25
21
  * Return the deploy-eligible file paths under `cwd`, relative to
@@ -1 +1 @@
1
- {"version":3,"file":"git-files.d.ts","sourceRoot":"","sources":["../../src/deploy/git-files.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAE9D,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;CAC5C;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAU9C,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAqBxF"}
1
+ {"version":3,"file":"git-files.d.ts","sourceRoot":"","sources":["../../src/deploy/git-files.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAI9D,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAG,iBAAiB,CAAU;CAC5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAU9C,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CAC7C;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CA4BxF"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.54.0";
1
+ export declare const CLI_VERSION = "0.54.1";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.54.0",
3
+ "version": "0.54.1",
4
4
  "description": "Launchpad CLI — clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-content-pr
3
3
  description: Push a content change to a Launchpad app via `launchpad deploy` and verify it shipped via `launchpad status`. Covers the post-first-deploy iteration loop (edit → deploy → verify) — subsequent deploys commit directly to the app repo's main and the Pages build runs asynchronously, so verification is its own step. Use when someone says "push a content change", "ship an update", "/launchpad-content-pr", "verify my deploy", or after `/launchpad-deploy` reports `done` and they want to follow up with an edit.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy
3
3
  description: Walk a Launchpad user through deploying an app from their local working directory (Model A — `launchpad init` + `launchpad deploy`). Wraps the CLI verbs end-to-end: detects the app shape, scaffolds `launchpad.yaml`, resolves the allowed Entra group via `launchpad groups`, bundles the CWD via `launchpad deploy`, and watches the rollout via `launchpad status`. Use when someone says "deploy a new app", "ship my app to Launchpad", "/launchpad-deploy", "I have an app locally — get it on Launchpad", or any variant. Resume/abandon for legacy in-flight provisioning is at the bottom.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy-status
3
3
  description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket), or watch provisioning live with `launchpad watch`. Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "watch my deploy go live", "watch provisioning", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-destroy
3
3
  description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-identity
3
3
  description: Teach an app author how to use the signed-in user's identity inside a Launchpad app — read the gateway-forwarded X-Launchpad-User-Assertion in a Pages Function, VERIFY it with @m-kopa/platform-auth (fail-closed), and show who's logged in (sub/email/name). Use when someone says "who is logged in", "show the current user", "get the user's email in my app", "auth in my launchpad app", "read the user identity", "/launchpad-identity", or is wiring up an /api/me for a gateway-fronted app.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-onboard
3
3
  description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-report
3
3
  description: File a bug report or feature request to the Launchpad team's tracker from the CLI. Use when someone reports something broken, hits an error in a launchpad command, or wishes a feature existed — e.g. "this is broken", "report a bug", "can you file that", "I wish launchpad could…", "/launchpad-bug", "/launchpad-feature". Always confirm and show exactly what you'll send before filing; never file silently.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-status
3
3
  description: Safely refresh a Launchpad clone, show manifest drift, or read the deployed manifest. Wraps bare `launchpad pull` for source sync, `launchpad pull --manifest` for deployed YAML, and `launchpad status` for drift. Use for "sync my clone", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or post-deploy verification.
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->