@m-kopa/launchpad-cli 0.46.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,45 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
7
7
  pre-1.0 minor bumps may carry breaking changes per ADR 0005.
8
8
 
9
+ ## 0.47.0 — 2026-07-07
10
+
11
+ **`launchpad deploy` offers to reconcile access drift inline (PS-2071).** When a
12
+ plain deploy is refused because the manifest's access groups diverge from what
13
+ the gateway enforces, an **interactive terminal** now *offers to reconcile right
14
+ there* — answer `y` and it routes straight into the gated `launchpad deploy
15
+ --apply` flow instead of leaving you at a dead-end error to re-read, find the
16
+ flag, and re-run. This is convenience only: it changes no control. The apply
17
+ still shows the exact plan and prompts again before opening the PR, and the
18
+ byte-equal validator, the second-person `allowed-groups-change` label gate for
19
+ any access-widening change, and Terraform-as-sole-writer all still apply. A
20
+ **non-interactive** session (CI, a pipe, redirected stdin) never auto-runs an
21
+ access change — it prints the manual `--apply` route and exits non-zero so a
22
+ script fails loudly. After a successful reconcile the CLI points you back at
23
+ `launchpad deploy` to ship your content.
24
+
25
+ **`launchpad update` resyncs drifted skills even when the CLI is already current
26
+ (PS-2058).** Previously the skill bundle was re-synced only after an actual CLI
27
+ upgrade, so a machine already on the latest CLI could sit on a stale skill
28
+ bundle indefinitely with no warning. Now, when `update` finds the CLI already
29
+ current, it also checks the installed skill bundle and:
30
+
31
+ - **`launchpad update`** auto-resyncs a drifted bundle (best-effort; the update
32
+ still exits `0` regardless — a skills hiccup never fails it).
33
+ - **`launchpad update --check`** reports the drift as an informational line
34
+ pointing at `launchpad skills update`; exit codes are unchanged (`10` stays
35
+ CLI-update-only).
36
+
37
+ An in-sync bundle stays silent; an absent bundle is left to the passive
38
+ notifier's one-time hint.
39
+
40
+ ## 0.46.2 — 2026-07-07
41
+
42
+ Fix: the apply-status poll no longer mistakes the endpoint's own
43
+ `no_apply_found` 404 (a momentary GitHub PR-list lag right after the
44
+ apply call) for a bot that lacks the endpoint. The poll now retries in
45
+ place; only a genuine route-absent 404 degrades to the legacy
46
+ merge-poll. Surfaced by the first live end-to-end apply.
47
+
9
48
  ## 0.46.1 — 2026-07-07
10
49
 
11
50
  Copy fixes in the access-drift guidance: consistent voice in the deploy
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.46.1";
22
+ var CLI_VERSION = "0.47.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -6262,6 +6262,7 @@ async function runDeployApply(opts, io, deps = {}) {
6262
6262
  return outcomeToExit(outcome2, undefined, io);
6263
6263
  deletePinIfPresent(cfg, slug2, io);
6264
6264
  io.out(`✓ Applied ${slug2}.`);
6265
+ deps.onApplied?.();
6265
6266
  return 0;
6266
6267
  }
6267
6268
  const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
@@ -6344,6 +6345,7 @@ async function runDeployApply(opts, io, deps = {}) {
6344
6345
  deletePinIfPresent(cfg, slug, io);
6345
6346
  io.out("");
6346
6347
  io.out(`✓ Applied ${slug}. (See ${apply.prUrl} for the merged PR.)`);
6348
+ deps.onApplied?.();
6347
6349
  return 0;
6348
6350
  }
6349
6351
  async function pollUntilApplied(args) {
@@ -6368,6 +6370,10 @@ async function pollUntilApplied(args) {
6368
6370
  }, args.fetcher);
6369
6371
  } catch (e) {
6370
6372
  if (e instanceof NotFoundError) {
6373
+ if (e.message.includes("no_apply_found")) {
6374
+ await sleep(args.pollIntervalSec * 1000);
6375
+ continue;
6376
+ }
6371
6377
  breakDots();
6372
6378
  args.io.err("! bot has no apply-status endpoint yet — falling back to the coarse merge-poll (upgrade pending bot deploy)");
6373
6379
  mode = "legacy";
@@ -6552,6 +6558,80 @@ function renderManifestError(loaded, io) {
6552
6558
  }
6553
6559
  }
6554
6560
 
6561
+ // src/deploy/access-drift-offer.ts
6562
+ var asStrings = (v) => Array.isArray(v) ? v.map((x) => String(x)) : [];
6563
+ function renderAccessDriftSummary(slug, body) {
6564
+ const lines = [`launchpad deploy: refused — ACCESS DRIFT on "${slug}".`];
6565
+ const liveOnly = asStrings(body.live_only);
6566
+ const manifestOnly = asStrings(body.manifest_only);
6567
+ if (liveOnly.length > 0) {
6568
+ lines.push(` enforced but NOT declared (over-grant): ${liveOnly.join(", ")}`);
6569
+ }
6570
+ if (manifestOnly.length > 0) {
6571
+ lines.push(` declared but NOT enforced: ${manifestOnly.join(", ")}`);
6572
+ }
6573
+ lines.push("");
6574
+ lines.push(" Your manifest's access groups don't match what the gateway enforces.");
6575
+ lines.push(" `launchpad deploy` ships content only and cannot change access.");
6576
+ return lines;
6577
+ }
6578
+ function renderManualApplyGuidance() {
6579
+ return [
6580
+ " Run `launchpad deploy --apply` to reconcile — it opens the gated TF PR",
6581
+ " (an access-widening change needs the allowed-groups-change label from a",
6582
+ " second person) and waits for the apply verdict. Then redeploy your content.",
6583
+ " Nothing was committed by this attempt."
6584
+ ];
6585
+ }
6586
+ function isYes(answer) {
6587
+ const a = answer.trim().toLowerCase();
6588
+ return a === "y" || a === "yes";
6589
+ }
6590
+ async function handleAccessDrift(params) {
6591
+ const { slug, manifestPath, body, io, deps } = params;
6592
+ for (const line of renderAccessDriftSummary(slug, body))
6593
+ io.err(line);
6594
+ if (!deps.interactive) {
6595
+ for (const line of renderManualApplyGuidance())
6596
+ io.err(line);
6597
+ return 1;
6598
+ }
6599
+ io.err("");
6600
+ const answer = await deps.prompt("Reconcile access now via the gated apply PR? You'll review the exact plan next. [y/N] ");
6601
+ if (!isYes(answer)) {
6602
+ io.err("");
6603
+ io.err("Skipped — no access change was made.");
6604
+ for (const line of renderManualApplyGuidance())
6605
+ io.err(line);
6606
+ return 1;
6607
+ }
6608
+ io.out("");
6609
+ io.out("Reconciling access via `launchpad deploy --apply` …");
6610
+ let applied = false;
6611
+ const exit = await deps.runApply({ file: manifestPath, platformRepo: null, rePin: false, yes: false }, io, { onApplied: () => {
6612
+ applied = true;
6613
+ } });
6614
+ if (applied) {
6615
+ io.out("");
6616
+ io.out("✓ Access reconciled. Re-run `launchpad deploy` to ship your content change.");
6617
+ }
6618
+ return exit;
6619
+ }
6620
+
6621
+ // src/io/prompt.ts
6622
+ function isInteractive() {
6623
+ return Boolean(process.stdin.isTTY);
6624
+ }
6625
+ async function defaultPrompt2(question) {
6626
+ const { createInterface } = await import("node:readline/promises");
6627
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
6628
+ try {
6629
+ return await rl.question(question);
6630
+ } finally {
6631
+ rl.close();
6632
+ }
6633
+ }
6634
+
6555
6635
  // src/deploy/dry-run.ts
6556
6636
  import { execFileSync } from "node:child_process";
6557
6637
  import { resolve as resolve7 } from "node:path";
@@ -7398,23 +7478,17 @@ async function runModelADeploy(args) {
7398
7478
  return 1;
7399
7479
  }
7400
7480
  if (result.status === 409 && errorCode === "access_drift") {
7401
- io.err(`launchpad deploy: refused — ACCESS DRIFT on "${slug}".`);
7402
- const liveOnly = Array.isArray(body.live_only) ? body.live_only : [];
7403
- const manifestOnly = Array.isArray(body.manifest_only) ? body.manifest_only : [];
7404
- if (liveOnly.length > 0) {
7405
- io.err(` enforced but NOT declared (over-grant): ${liveOnly.map(String).join(", ")}`);
7406
- }
7407
- if (manifestOnly.length > 0) {
7408
- io.err(` declared but NOT enforced: ${manifestOnly.map(String).join(", ")}`);
7409
- }
7410
- io.err("");
7411
- io.err(" Your manifest's access groups don't match what the gateway enforces.");
7412
- io.err(" `launchpad deploy` ships content only and cannot change access.");
7413
- io.err(" Run `launchpad deploy --apply` to reconcile — it opens the gated TF PR");
7414
- io.err(" (access-widening changes need the allowed-groups-change label from a");
7415
- io.err(" second person) and waits for the apply verdict. Then redeploy your content.");
7416
- io.err(" Nothing was committed by this attempt.");
7417
- return 1;
7481
+ return handleAccessDrift({
7482
+ slug,
7483
+ manifestPath,
7484
+ body,
7485
+ io,
7486
+ deps: {
7487
+ interactive: isInteractive(),
7488
+ prompt: defaultPrompt2,
7489
+ runApply: runDeployApply
7490
+ }
7491
+ });
7418
7492
  }
7419
7493
  io.err(`launchpad deploy: bot rejected the upload (HTTP ${result.status}, ${errorCode}).`);
7420
7494
  if (typeof body.message === "string") {
@@ -8881,7 +8955,7 @@ var SLUG_REGEX3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
8881
8955
  var initCommand = {
8882
8956
  name: "init",
8883
8957
  summary: "scaffold a new launchpad.yaml in the current directory",
8884
- run: (args, io) => runInit(args, io, defaultPrompt2)
8958
+ run: (args, io) => runInit(args, io, defaultPrompt3)
8885
8959
  };
8886
8960
  async function runInit(args, io, prompt) {
8887
8961
  const parsed = parseArgs6(args);
@@ -9371,7 +9445,7 @@ function ensureGitignoreEntries(path10, entries) {
9371
9445
  }
9372
9446
  return added;
9373
9447
  }
9374
- async function defaultPrompt2(question, fallback) {
9448
+ async function defaultPrompt3(question, fallback) {
9375
9449
  const rl = createInterface({ input: process.stdin, output: process.stdout });
9376
9450
  try {
9377
9451
  const suffix = fallback !== undefined ? ` [${fallback}]` : "";
@@ -10052,7 +10126,7 @@ import { stdin as input, stdout as output } from "node:process";
10052
10126
  var destroyCommand = {
10053
10127
  name: "destroy",
10054
10128
  summary: "tear down a Launchpad app (owner-only, destructive)",
10055
- run: (args, io) => runDestroy(args, io, defaultPrompt3, defaultIsTty)
10129
+ run: (args, io) => runDestroy(args, io, defaultPrompt4, defaultIsTty)
10056
10130
  };
10057
10131
  var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
10058
10132
  var SLUG_MIN_LENGTH = 3;
@@ -10284,7 +10358,7 @@ function renderBotError2(status, env, slug, io) {
10284
10358
  io.err(`launchpad destroy: bot returned HTTP ${status} (error=${errorCode}): ${message}`);
10285
10359
  }
10286
10360
  }
10287
- async function defaultPrompt3(question) {
10361
+ async function defaultPrompt4(question) {
10288
10362
  const rl = createInterface2({ input, output });
10289
10363
  try {
10290
10364
  return await rl.question(question);
@@ -10953,7 +11027,7 @@ async function runRollback(opts, io, deps = {}) {
10953
11027
  io.out(`Rollback target: ${verifiedSha.slice(0, 12)} (${manifestRelpath})`);
10954
11028
  renderDiff(current, parsed.manifest, io);
10955
11029
  if (!opts.yes) {
10956
- const prompt = deps.prompt ?? defaultPrompt4;
11030
+ const prompt = deps.prompt ?? defaultPrompt5;
10957
11031
  if (!process.stdin.isTTY && deps.prompt === undefined) {
10958
11032
  io.err("launchpad rollback: refusing to rollback non-interactively without --yes.");
10959
11033
  return 64;
@@ -11041,7 +11115,7 @@ function formatHostnames(hs) {
11041
11115
  return hs[0];
11042
11116
  return `[${hs.join(", ")}]`;
11043
11117
  }
11044
- async function defaultPrompt4(question) {
11118
+ async function defaultPrompt5(question) {
11045
11119
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
11046
11120
  try {
11047
11121
  return await rl.question(question);
@@ -11348,7 +11422,7 @@ async function offerRedeploy(slug, opts, io, deps) {
11348
11422
  const isTty = (deps.isTty ?? (() => process.stdin.isTTY === true))();
11349
11423
  let doRedeploy = wantsAuto;
11350
11424
  if (!wantsAuto && isTty) {
11351
- const prompt = deps.prompt ?? defaultPrompt5;
11425
+ const prompt = deps.prompt ?? defaultPrompt6;
11352
11426
  let answer;
11353
11427
  try {
11354
11428
  answer = (await prompt(`Redeploy ${slug} now to activate? [y/N] `)).trim().toLowerCase();
@@ -11369,7 +11443,7 @@ async function offerRedeploy(slug, opts, io, deps) {
11369
11443
  };
11370
11444
  return run({ slug, file: opts.file ?? null, verify: true, verifyTimeoutMs: 180000 }, io, rdeps);
11371
11445
  }
11372
- async function defaultPrompt5(question) {
11446
+ async function defaultPrompt6(question) {
11373
11447
  const rl = createInterface4({ input: input2, output: output2 });
11374
11448
  try {
11375
11449
  return await rl.question(question);
@@ -12110,9 +12184,45 @@ function printHelp3(io) {
12110
12184
 
12111
12185
  // src/commands/skills.ts
12112
12186
  import { fileURLToPath } from "node:url";
12113
- import { dirname as dirname8, join as join13, resolve as resolve12 } from "node:path";
12187
+ import { dirname as dirname8, join as join14, resolve as resolve12 } from "node:path";
12114
12188
  import { promises as fs6, existsSync as existsSync14 } from "node:fs";
12189
+
12190
+ // src/skills-bundle.ts
12115
12191
  import { homedir as homedir2 } from "node:os";
12192
+ import { join as join13 } from "node:path";
12193
+ import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
12194
+ var SKILL_PREFIX = "launchpad-";
12195
+ function skillsTargetDir() {
12196
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join13(homedir2(), ".claude", "skills");
12197
+ }
12198
+ function readInstalledSkills() {
12199
+ try {
12200
+ const dir = skillsTargetDir();
12201
+ const bundle = readdirSync2(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
12202
+ if (!bundle)
12203
+ return { present: false, version: null };
12204
+ return {
12205
+ present: true,
12206
+ version: readSkillVersion(join13(dir, bundle.name, "SKILL.md"))
12207
+ };
12208
+ } catch {
12209
+ return { present: false, version: null };
12210
+ }
12211
+ }
12212
+ function readSkillVersion(path13) {
12213
+ try {
12214
+ const text = readFileSync18(path13, "utf8");
12215
+ const fenceEnd = text.indexOf(`
12216
+ ---`, 4);
12217
+ const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
12218
+ const m = /^version:\s*(.+?)\s*$/m.exec(front);
12219
+ return m === null ? null : m[1] ?? null;
12220
+ } catch {
12221
+ return null;
12222
+ }
12223
+ }
12224
+
12225
+ // src/commands/skills.ts
12116
12226
  var BUNDLE_PREFIX = "launchpad-";
12117
12227
  var BUNDLED_SKILLS = [
12118
12228
  "launchpad-onboard",
@@ -12174,7 +12284,7 @@ function printHelp4(io) {
12174
12284
  }
12175
12285
  function resolveInstallEnv() {
12176
12286
  const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
12177
- const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join13(homedir2(), ".claude", "skills");
12287
+ const userSkillsDir = skillsTargetDir();
12178
12288
  return { bundleDir, userSkillsDir };
12179
12289
  }
12180
12290
  function defaultBundleDir() {
@@ -12184,7 +12294,7 @@ function defaultBundleDir() {
12184
12294
  resolve12(here, "..", "..", "skills")
12185
12295
  ];
12186
12296
  for (const c of candidates) {
12187
- if (existsSync14(join13(c, "launchpad-onboard", "SKILL.md"))) {
12297
+ if (existsSync14(join14(c, "launchpad-onboard", "SKILL.md"))) {
12188
12298
  return c;
12189
12299
  }
12190
12300
  }
@@ -12205,12 +12315,12 @@ async function doInstall(io) {
12205
12315
  }
12206
12316
  let installed = 0;
12207
12317
  for (const skill of BUNDLED_SKILLS) {
12208
- const src = join13(env.bundleDir, skill);
12318
+ const src = join14(env.bundleDir, skill);
12209
12319
  if (!await isDir(src)) {
12210
12320
  io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
12211
12321
  return 1;
12212
12322
  }
12213
- const dest = join13(env.userSkillsDir, skill);
12323
+ const dest = join14(env.userSkillsDir, skill);
12214
12324
  await fs6.rm(dest, { recursive: true, force: true });
12215
12325
  await fs6.cp(src, dest, { recursive: true });
12216
12326
  installed++;
@@ -12235,7 +12345,7 @@ async function doUninstall(io) {
12235
12345
  continue;
12236
12346
  if (!isBundleManaged(entry.name))
12237
12347
  continue;
12238
- const target = join13(env.userSkillsDir, entry.name);
12348
+ const target = join14(env.userSkillsDir, entry.name);
12239
12349
  await fs6.rm(target, { recursive: true, force: true });
12240
12350
  removed++;
12241
12351
  io.out(`✗ removed ${target}`);
@@ -12258,7 +12368,7 @@ async function doList(io) {
12258
12368
  }
12259
12369
  const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
12260
12370
  for (const name of managedDirs) {
12261
- const skillFile = join13(env.userSkillsDir, name, "SKILL.md");
12371
+ const skillFile = join14(env.userSkillsDir, name, "SKILL.md");
12262
12372
  const version = await readVersion(skillFile);
12263
12373
  io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
12264
12374
  }
@@ -12293,9 +12403,9 @@ function describe29(e) {
12293
12403
  import { execFile, spawn as spawn5 } from "node:child_process";
12294
12404
  import { promisify } from "node:util";
12295
12405
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12296
- import { dirname as dirname9, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as join14 } from "node:path";
12406
+ import { dirname as dirname9, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as join15 } from "node:path";
12297
12407
  import { homedir as homedir3, tmpdir } from "node:os";
12298
- import { readFileSync as readFileSync18, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
12408
+ import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
12299
12409
 
12300
12410
  // src/commands/channel-auth.ts
12301
12411
  import { createServer as createServer2 } from "node:http";
@@ -12416,7 +12526,7 @@ var PKG = "@m-kopa/launchpad-cli";
12416
12526
  var REGISTRY = "https://registry.npmjs.org";
12417
12527
  var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
12418
12528
  var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
12419
- var CHANNEL_MARKER = join14(homedir3(), ".launchpad", "channel");
12529
+ var CHANNEL_MARKER = join15(homedir3(), ".launchpad", "channel");
12420
12530
  var EXIT_UPDATE_AVAILABLE = 10;
12421
12531
  var UPGRADE_ARGS = {
12422
12532
  npm: ["install", "-g", `${PKG}@latest`],
@@ -12493,7 +12603,8 @@ function defaultDeps() {
12493
12603
  detectPm: detectPackageManager2,
12494
12604
  runUpgrade,
12495
12605
  runChannelInstaller,
12496
- syncSkills: runSkillsSync
12606
+ syncSkills: runSkillsSync,
12607
+ readInstalledSkills
12497
12608
  };
12498
12609
  }
12499
12610
  async function runSkillsSync() {
@@ -12516,7 +12627,25 @@ async function runSkillsSync() {
12516
12627
  async function syncSkillsAfterUpgrade(io, deps) {
12517
12628
  const result = await deps.syncSkills();
12518
12629
  if (result.ok) {
12519
- io.out("✓ Claude Code skill bundle re-synced to the new version.");
12630
+ io.out("✓ Claude Code skill bundle re-synced to match the CLI.");
12631
+ } else {
12632
+ io.err("Note: couldn't auto-sync the skill bundle — run `launchpad skills update` to finish.");
12633
+ }
12634
+ }
12635
+ async function reconcileSkillBundleWhenCurrent(io, deps, checkOnly) {
12636
+ const skills = deps.readInstalledSkills();
12637
+ if (!skills.present || skills.version === null)
12638
+ return;
12639
+ if (compareVersions(deps.cliVersion, skills.version) !== 1)
12640
+ return;
12641
+ if (checkOnly) {
12642
+ io.out(`Skill bundle ${skills.version} is behind the CLI ${deps.cliVersion} — run \`launchpad skills update\`.`);
12643
+ return;
12644
+ }
12645
+ io.out(`Skill bundle ${skills.version} is behind the CLI ${deps.cliVersion}; re-syncing…`);
12646
+ const result = await deps.syncSkills();
12647
+ if (result.ok) {
12648
+ io.out("✓ Claude Code skill bundle re-synced to match the CLI.");
12520
12649
  } else {
12521
12650
  io.err("Note: couldn't auto-sync the skill bundle — run `launchpad skills update` to finish.");
12522
12651
  }
@@ -12526,8 +12655,8 @@ async function openSystemBrowser(url) {
12526
12655
  await execFileAsync(opener, [url]);
12527
12656
  }
12528
12657
  async function runInstallerScript(script) {
12529
- const dir = mkdtempSync(join14(tmpdir(), "launchpad-update-"));
12530
- const file = join14(dir, "install.sh");
12658
+ const dir = mkdtempSync(join15(tmpdir(), "launchpad-update-"));
12659
+ const file = join15(dir, "install.sh");
12531
12660
  try {
12532
12661
  writeFileSync7(file, script, { mode: 448 });
12533
12662
  return await new Promise((resolvePromise) => {
@@ -12551,7 +12680,7 @@ function resolveLatestVersion() {
12551
12680
  }
12552
12681
  function detectInstallChannel() {
12553
12682
  try {
12554
- return readFileSync18(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
12683
+ return readFileSync19(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
12555
12684
  } catch {
12556
12685
  return "github";
12557
12686
  }
@@ -12677,6 +12806,7 @@ async function runUpdate(args, io, deps) {
12677
12806
  const current = deps.cliVersion;
12678
12807
  if (compareVersions(current, latest.version) >= 0) {
12679
12808
  io.out(`launchpad-cli is up to date (${current}).`);
12809
+ await reconcileSkillBundleWhenCurrent(io, deps, parsed.check);
12680
12810
  return 0;
12681
12811
  }
12682
12812
  if (parsed.check) {
@@ -12758,7 +12888,7 @@ function printHelp5(io) {
12758
12888
  }
12759
12889
 
12760
12890
  // src/commands/validate.ts
12761
- import { existsSync as existsSync15, readFileSync as readFileSync19, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
12891
+ import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
12762
12892
  import { dirname as dirname10, resolve as resolve14 } from "node:path";
12763
12893
  var validateCommand = {
12764
12894
  name: "validate",
@@ -12796,7 +12926,7 @@ function checkBoundary(manifestPath, declared) {
12796
12926
  }
12797
12927
  let manifestYaml;
12798
12928
  try {
12799
- manifestYaml = readFileSync19(manifestPath, "utf8");
12929
+ manifestYaml = readFileSync20(manifestPath, "utf8");
12800
12930
  } catch {
12801
12931
  manifestYaml = null;
12802
12932
  }
@@ -12837,7 +12967,7 @@ function checkStaticServeDir(manifestPath, manifest) {
12837
12967
  }
12838
12968
  let entries;
12839
12969
  try {
12840
- entries = readdirSync2(abs);
12970
+ entries = readdirSync3(abs);
12841
12971
  } catch (e) {
12842
12972
  return [`failed to read static served directory '${serveDir}/': ${describe30(e)}`];
12843
12973
  }
@@ -13711,21 +13841,20 @@ var listOwnersCommand = makeListOwnersCommand();
13711
13841
  // src/update-notifier.ts
13712
13842
  import { spawn as spawn6 } from "node:child_process";
13713
13843
  import { homedir as homedir4 } from "node:os";
13714
- import { join as join15 } from "node:path";
13844
+ import { join as join16 } from "node:path";
13715
13845
  import {
13716
13846
  existsSync as existsSync16,
13717
13847
  mkdirSync as mkdirSync4,
13718
- readFileSync as readFileSync20,
13719
- readdirSync as readdirSync3,
13848
+ readFileSync as readFileSync21,
13720
13849
  writeFileSync as writeFileSync8
13721
13850
  } from "node:fs";
13722
13851
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
13723
13852
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
13724
13853
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
13725
- var CACHE_FILE = join15(homedir4(), ".launchpad", "update-check.json");
13854
+ var CACHE_FILE = join16(homedir4(), ".launchpad", "update-check.json");
13726
13855
  function readCache2() {
13727
13856
  try {
13728
- const raw = JSON.parse(readFileSync20(CACHE_FILE, "utf8"));
13857
+ const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
13729
13858
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
13730
13859
  const latest = raw.latest;
13731
13860
  return {
@@ -13738,39 +13867,12 @@ function readCache2() {
13738
13867
  }
13739
13868
  function writeCache2(state) {
13740
13869
  try {
13741
- mkdirSync4(join15(homedir4(), ".launchpad"), { recursive: true });
13870
+ mkdirSync4(join16(homedir4(), ".launchpad"), { recursive: true });
13742
13871
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
13743
13872
  `, { mode: 384 });
13744
13873
  } catch {}
13745
13874
  }
13746
- var SKILL_PREFIX = "launchpad-";
13747
- var SKILLS_HINT_MARKER = join15(homedir4(), ".launchpad", "skills-hint-shown");
13748
- function skillsTargetDir() {
13749
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir4(), ".claude", "skills");
13750
- }
13751
- function readInstalledSkills() {
13752
- try {
13753
- const dir = skillsTargetDir();
13754
- const bundle = readdirSync3(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
13755
- if (!bundle)
13756
- return { present: false, version: null };
13757
- return { present: true, version: readSkillVersion(join15(dir, bundle.name, "SKILL.md")) };
13758
- } catch {
13759
- return { present: false, version: null };
13760
- }
13761
- }
13762
- function readSkillVersion(path13) {
13763
- try {
13764
- const text = readFileSync20(path13, "utf8");
13765
- const fenceEnd = text.indexOf(`
13766
- ---`, 4);
13767
- const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
13768
- const m = /^version:\s*(.+?)\s*$/m.exec(front);
13769
- return m === null ? null : m[1] ?? null;
13770
- } catch {
13771
- return null;
13772
- }
13773
- }
13875
+ var SKILLS_HINT_MARKER = join16(homedir4(), ".launchpad", "skills-hint-shown");
13774
13876
  function absentHintShown() {
13775
13877
  try {
13776
13878
  return existsSync16(SKILLS_HINT_MARKER);
@@ -13780,7 +13882,7 @@ function absentHintShown() {
13780
13882
  }
13781
13883
  function markAbsentHintShown() {
13782
13884
  try {
13783
- mkdirSync4(join15(homedir4(), ".launchpad"), { recursive: true });
13885
+ mkdirSync4(join16(homedir4(), ".launchpad"), { recursive: true });
13784
13886
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
13785
13887
  `, { mode: 384 });
13786
13888
  } catch {}
@@ -13883,8 +13985,8 @@ var refreshUpdateCacheCommand = {
13883
13985
  // src/telemetry.ts
13884
13986
  import { spawn as spawn7 } from "node:child_process";
13885
13987
  import { homedir as homedir5 } from "node:os";
13886
- import { join as join16 } from "node:path";
13887
- import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "node:fs";
13988
+ import { join as join17 } from "node:path";
13989
+ import { existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
13888
13990
  import { randomUUID } from "node:crypto";
13889
13991
  var INTERNAL_EMIT_VERB = "__emit-telemetry";
13890
13992
  var POSTHOG_HOST = "https://us.i.posthog.com";
@@ -13892,8 +13994,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
13892
13994
  var CAPTURE_PATH = "/i/v0/e/";
13893
13995
  var SEND_TIMEOUT_MS = 3000;
13894
13996
  var APP = "launchpad-cli";
13895
- var DEVICE_ID_FILE = join16(homedir5(), ".launchpad", "telemetry-id.json");
13896
- var FIRST_RUN_MARKER = join16(homedir5(), ".launchpad", "telemetry-notice-shown");
13997
+ var DEVICE_ID_FILE = join17(homedir5(), ".launchpad", "telemetry-id.json");
13998
+ var FIRST_RUN_MARKER = join17(homedir5(), ".launchpad", "telemetry-notice-shown");
13897
13999
  function buildEvent(ctx, identity, nowMs) {
13898
14000
  const properties = {
13899
14001
  app: APP,
@@ -13960,14 +14062,14 @@ async function resolveIdentityReal() {
13960
14062
  }
13961
14063
  function getOrCreateDeviceId() {
13962
14064
  try {
13963
- const raw = JSON.parse(readFileSync21(DEVICE_ID_FILE, "utf8"));
14065
+ const raw = JSON.parse(readFileSync22(DEVICE_ID_FILE, "utf8"));
13964
14066
  const id2 = raw?.deviceId;
13965
14067
  if (typeof id2 === "string" && id2)
13966
14068
  return id2;
13967
14069
  } catch {}
13968
14070
  const id = randomUUID();
13969
14071
  try {
13970
- mkdirSync5(join16(homedir5(), ".launchpad"), { recursive: true });
14072
+ mkdirSync5(join17(homedir5(), ".launchpad"), { recursive: true });
13971
14073
  writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
13972
14074
  `, {
13973
14075
  mode: 384
@@ -14045,7 +14147,7 @@ function firstRunNoticeShown() {
14045
14147
  }
14046
14148
  function markFirstRunNotice() {
14047
14149
  try {
14048
- mkdirSync5(join16(homedir5(), ".launchpad"), { recursive: true });
14150
+ mkdirSync5(join17(homedir5(), ".launchpad"), { recursive: true });
14049
14151
  writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
14050
14152
  `, { mode: 384 });
14051
14153
  } catch {}
@@ -14111,11 +14213,11 @@ import { createInterface as createInterface7 } from "node:readline/promises";
14111
14213
  import { platform } from "node:os";
14112
14214
 
14113
14215
  // src/report/breadcrumb.ts
14114
- import { writeFileSync as writeFileSync10, readFileSync as readFileSync22, mkdirSync as mkdirSync6 } from "node:fs";
14216
+ import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
14115
14217
  import { homedir as homedir6 } from "node:os";
14116
- import { join as join17, dirname as dirname11 } from "node:path";
14218
+ import { join as join18, dirname as dirname11 } from "node:path";
14117
14219
  function breadcrumbPath() {
14118
- return join17(homedir6(), ".launchpad", "last-run.json");
14220
+ return join18(homedir6(), ".launchpad", "last-run.json");
14119
14221
  }
14120
14222
  function writeBreadcrumb(verb, exit) {
14121
14223
  try {
@@ -14126,7 +14228,7 @@ function writeBreadcrumb(verb, exit) {
14126
14228
  }
14127
14229
  function readBreadcrumb() {
14128
14230
  try {
14129
- const raw = JSON.parse(readFileSync22(breadcrumbPath(), "utf8"));
14231
+ const raw = JSON.parse(readFileSync23(breadcrumbPath(), "utf8"));
14130
14232
  if (typeof raw !== "object" || raw === null)
14131
14233
  return null;
14132
14234
  const r = raw;
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAqEA,OAAO,EAGL,4BAA4B,EAC7B,MAAM,uBAAuB,CAAC;AAM/B,OAAO,EAAa,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAKjE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAEjD,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAmQD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,IAAI,CAwBpE;AAkMD,2BAA2B;AAC3B,OAAO,EAAE,4BAA4B,EAAE,CAAC"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAqEA,OAAO,EAGL,4BAA4B,EAC7B,MAAM,uBAAuB,CAAC;AAQ/B,OAAO,EAAa,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAKjE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAEjD,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAmQD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,IAAI,CAwBpE;AAkMD,2BAA2B;AAC3B,OAAO,EAAE,4BAA4B,EAAE,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { type ChannelUpdateResult } from "./channel-auth.js";
2
+ import { type InstalledSkillsState } from "../skills-bundle.js";
2
3
  import type { CliIo, Command, ExitCode } from "../dispatcher.js";
3
4
  /** Browser-based installer the platform channel directs users to for upgrades. */
4
5
  export declare const CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
@@ -96,6 +97,12 @@ export interface UpdateDeps {
96
97
  * failure). See {@link runSkillsSync}.
97
98
  */
98
99
  readonly syncSkills: () => Promise<SkillsSyncResult>;
100
+ /**
101
+ * Read the freshness of the on-disk Claude Code skill bundle. Injected so
102
+ * the already-up-to-date branch's drift check is unit-testable without
103
+ * touching the filesystem. See {@link reconcileSkillBundleWhenCurrent}.
104
+ */
105
+ readonly readInstalledSkills: () => InstalledSkillsState;
99
106
  }
100
107
  /** Outcome of the post-upgrade skill-bundle re-sync. */
101
108
  export interface SkillsSyncResult {
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAuDA,OAAO,EAA4B,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAWjE,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,oCAAoC,CAAC;AAYrE,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEpD,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,CAAC;AASnD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAGvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAEpB;AAED,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAIF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAMD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA4BnE;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAiBhE;AAID,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAI3D,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjE;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACtD;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AA+GD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,YAAY,CAAC,CAI5D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAQrD;AA0JD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,QAAQ,CAAC,CA2GnB"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { CliIo, ExitCode } from "../dispatcher.js";
2
+ import type { ApplyOptions } from "./apply.js";
3
+ /** The two-direction group diff the bot returns with a 409 `access_drift`. */
4
+ export interface AccessDriftBody {
5
+ readonly live_only?: unknown;
6
+ readonly manifest_only?: unknown;
7
+ }
8
+ export interface AccessDriftDeps {
9
+ /** True when we may prompt (stdin is a TTY). */
10
+ readonly interactive: boolean;
11
+ /** Ask a yes/no question; resolves the raw typed line. */
12
+ readonly prompt: (question: string) => Promise<string>;
13
+ /**
14
+ * Run the gated apply flow. Defaults to `runDeployApply` at the call site.
15
+ * The third arg carries an `onApplied` hook the flow fires only when a
16
+ * terraform apply actually completes — so we never claim success when the
17
+ * operator reviews the plan and aborts (both paths exit 0).
18
+ */
19
+ readonly runApply: (opts: ApplyOptions, io: CliIo, deps?: {
20
+ readonly onApplied?: () => void;
21
+ }) => Promise<ExitCode>;
22
+ }
23
+ /**
24
+ * The drift summary — printed in every case (interactive or not). Names both
25
+ * directions of the divergence so the operator can see exactly what would
26
+ * change before deciding.
27
+ */
28
+ export declare function renderAccessDriftSummary(slug: string, body: AccessDriftBody): string[];
29
+ /**
30
+ * The manual fallback guidance — printed when the session is non-interactive,
31
+ * or when the operator declines the inline offer.
32
+ */
33
+ export declare function renderManualApplyGuidance(): string[];
34
+ /**
35
+ * Surface an access-drift refusal and, when interactive, offer to reconcile
36
+ * inline via the gated apply flow. Returns the exit code the deploy command
37
+ * should return.
38
+ */
39
+ export declare function handleAccessDrift(params: {
40
+ readonly slug: string;
41
+ readonly manifestPath: string;
42
+ readonly body: AccessDriftBody;
43
+ readonly io: CliIo;
44
+ readonly deps: AccessDriftDeps;
45
+ }): Promise<ExitCode>;
46
+ //# sourceMappingURL=access-drift-offer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-drift-offer.d.ts","sourceRoot":"","sources":["../../src/deploy/access-drift-offer.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,KAAK,EACT,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KACvC,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxB;AAKD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,eAAe,GACpB,MAAM,EAAE,CAoBV;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAOpD;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8CpB"}
@@ -27,6 +27,14 @@ export interface ApplyDeps {
27
27
  readonly fetcher?: typeof fetch;
28
28
  /** Polling interval seconds (defaults to 10s; tests use 0). */
29
29
  readonly pollIntervalSeconds?: number;
30
+ /**
31
+ * Fired exactly once, when a terraform apply actually COMPLETES in this
32
+ * invocation (not when the operator aborts at the confirmation prompt, and
33
+ * not on a non-applied terminal state). Lets a caller — e.g. the PS-2071
34
+ * inline access-drift offer — distinguish a real reconcile from an abort,
35
+ * both of which exit 0.
36
+ */
37
+ readonly onApplied?: () => void;
30
38
  }
31
39
  export declare function runDeployApply(opts: ApplyOptions, io: CliIo, deps?: ApplyDeps): Promise<ExitCode>;
32
40
  //# sourceMappingURL=apply.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/deploy/apply.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAU1D,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;iCAC6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;wDACoD;IACpD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;oEAGgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAwCD,wBAAsB,cAAc,CAClC,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,KAAK,EACT,IAAI,GAAE,SAAc,GACnB,OAAO,CAAC,QAAQ,CAAC,CAoLnB"}
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/deploy/apply.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAU1D,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;iCAC6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;wDACoD;IACpD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;oEAGgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACjC;AAwCD,wBAAsB,cAAc,CAClC,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,KAAK,EACT,IAAI,GAAE,SAAc,GACnB,OAAO,CAAC,QAAQ,CAAC,CAsLnB"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * True when we can meaningfully prompt the operator — stdin is an interactive
3
+ * TTY. In a pipe, a CI job, or a redirected stdin this is false, and callers
4
+ * must fall back to a non-interactive path rather than block on a read that
5
+ * will never receive input.
6
+ */
7
+ export declare function isInteractive(): boolean;
8
+ /**
9
+ * Ask a question on stdin/stdout and resolve the raw typed line (untrimmed).
10
+ * The readline interface is always closed, even if the question rejects.
11
+ */
12
+ export declare function defaultPrompt(question: string): Promise<string>;
13
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/io/prompt.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQrE"}
@@ -0,0 +1,20 @@
1
+ /** Prefix every bundled launchpad skill directory shares. */
2
+ export declare const SKILL_PREFIX = "launchpad-";
3
+ /** Freshness of the on-disk Claude Code skill bundle vs the running CLI. */
4
+ export interface InstalledSkillsState {
5
+ /** True if any `launchpad-*` skill is installed under the skills dir. */
6
+ readonly present: boolean;
7
+ /** The bundle's `version:` frontmatter, or null if unreadable. */
8
+ readonly version: string | null;
9
+ }
10
+ /** Where `launchpad skills install` writes the bundle (mirrors skills.ts). */
11
+ export declare function skillsTargetDir(): string;
12
+ /**
13
+ * Synchronously read the installed skill-bundle freshness. Hot-path safe:
14
+ * one `readdir` + at most one small `readFile`, no network. A missing or
15
+ * unreadable skills dir is reported as `{ present: false }` (not an error).
16
+ */
17
+ export declare function readInstalledSkills(): InstalledSkillsState;
18
+ /** Parse the `version:` frontmatter from a SKILL.md (inside the `---` fence). */
19
+ export declare function readSkillVersion(path: string): string | null;
20
+ //# sourceMappingURL=skills-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills-bundle.d.ts","sourceRoot":"","sources":["../src/skills-bundle.ts"],"names":[],"mappings":"AAiBA,6DAA6D;AAC7D,eAAO,MAAM,YAAY,eAAe,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,IAAI,MAAM,CAKxC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,oBAAoB,CAc1D;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW5D"}
@@ -1,5 +1,8 @@
1
1
  import type { CliIo, Command } from "./dispatcher.js";
2
2
  import { resolveLatestVersion, type InstallChannel } from "./commands/update.js";
3
+ import { readInstalledSkills, type InstalledSkillsState } from "./skills-bundle.js";
4
+ export { readInstalledSkills };
5
+ export type { InstalledSkillsState };
3
6
  /** Internal verb the detached refresh process runs. Hidden from help. */
4
7
  export declare const INTERNAL_REFRESH_VERB = "__refresh-update-cache";
5
8
  /** Re-check the registry at most once per this window (24h). */
@@ -15,19 +18,6 @@ export interface CacheState {
15
18
  export declare function readCache(): CacheState | null;
16
19
  /** Write the cache file (0600), creating ~/.launchpad if needed. Best-effort. */
17
20
  export declare function writeCache(state: CacheState): void;
18
- /** Freshness of the on-disk Claude Code skill bundle vs the running CLI. */
19
- export interface InstalledSkillsState {
20
- /** True if any `launchpad-*` skill is installed under the skills dir. */
21
- readonly present: boolean;
22
- /** The bundle's `version:` frontmatter, or null if unreadable. */
23
- readonly version: string | null;
24
- }
25
- /**
26
- * Synchronously read the installed skill-bundle freshness. Hot-path safe:
27
- * one `readdir` + at most one small `readFile`, no network. A missing or
28
- * unreadable skills dir is reported as `{ present: false }` (not an error).
29
- */
30
- export declare function readInstalledSkills(): InstalledSkillsState;
31
21
  /** Has the one-time "skills not installed" hint already been shown? */
32
22
  export declare function absentHintShown(): boolean;
33
23
  /** Record that the one-time absent hint was shown. Best-effort. */
@@ -1 +1 @@
1
- {"version":3,"file":"update-notifier.d.ts","sourceRoot":"","sources":["../src/update-notifier.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"update-notifier.d.ts","sourceRoot":"","sources":["../src/update-notifier.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAIL,oBAAoB,EACpB,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EACL,mBAAmB,EACnB,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAC/B,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAE9D,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,QAAsB,CAAC;AAQrD,6CAA6C;AAC7C,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wEAAwE;AACxE,wBAAgB,SAAS,IAAI,UAAU,GAAG,IAAI,CAkB7C;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAOlD;AAQD,uEAAuE;AACvE,wBAAgB,eAAe,IAAI,OAAO,CAMzC;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAID,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,cAAc,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,UAAU,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;IACzD,uEAAuE;IACvE,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC;IACxC,sDAAsD;IACtD,QAAQ,CAAC,mBAAmB,EAAE,MAAM,IAAI,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAQxD;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,cAAc,GACtB,MAAM,CAMR;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,YAAY,GACjB,IAAI,CAgCN;AAwBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,KAAK,EACT,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,MAAM,GAAG,SAA2B,GAC5C,IAAI,CAuBN;AAID,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,oBAAoB,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,UAAU,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AASD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,GAAE,WAA6B,GAClC,OAAO,CAAC,IAAI,CAAC,CASf;AAED,0EAA0E;AAC1E,eAAO,MAAM,yBAAyB,EAAE,OAQvC,CAAC"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.46.1";
1
+ export declare const CLI_VERSION = "0.47.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.46.1",
4
- "description": "Launchpad CLI \u2014 clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
3
+ "version": "0.47.0",
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": {
7
7
  "launchpad": "./dist/cli.js"
@@ -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.46.1
4
+ version: 0.47.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -193,11 +193,15 @@ group passes (no need to use `deploy --dry-run` as a workaround).
193
193
  > `access.allowed_entra_groups` and running a plain `launchpad deploy`
194
194
  > ships content only — it does **not** reconcile the gateway's access
195
195
  > policy, so the change won't take effect (and is refused once the
196
- > access-drift gate is enforced). To change a live app's groups, the
197
- > user self-serves the **gated Terraform route**: `launchpad deploy
198
- > --apply` (preview with `--dry-run`, confirm with `launchpad status`).
199
- > Do **not** tell the user to raise a request with the platform team —
200
- > `--apply` IS the self-serve route. See `/launchpad-status` and the
196
+ > access-drift gate is enforced). When that refusal happens in an
197
+ > **interactive terminal**, the CLI now **offers to reconcile inline**
198
+ > answering `y` routes straight into the gated apply flow (which shows
199
+ > the plan and confirms again before opening the PR). To change a live
200
+ > app's groups the user self-serves the **gated Terraform route**:
201
+ > `launchpad deploy --apply` (preview with `--dry-run`, confirm with
202
+ > `launchpad status`) — or just accept the inline offer. Do **not** tell
203
+ > the user to raise a request with the platform team — `--apply` IS the
204
+ > self-serve route. See `/launchpad-status` and the
201
205
  > [auth & groups](https://get.launchpad.m-kopa.us/docs/concepts/auth-groups#changing-access-on-an-existing-app)
202
206
  > doc.
203
207
 
@@ -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.46.1
4
+ version: 0.47.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -235,10 +235,15 @@ Use whichever fits; `--dry-run` additionally previews the Terraform.
235
235
 
236
236
  This `--group` flow sets the group at **first deploy**. Changing the
237
237
  group on an **existing** app is a different path — a plain `launchpad
238
- deploy` ships content only and won't move the gateway policy. The
239
- self-serve fix is `launchpad deploy --apply` (the gated Terraform
240
- route); see `/launchpad-content-pr`. Never route the user to the
241
- platform team for a group change.
238
+ deploy` ships content only and won't move the gateway policy. When a
239
+ plain deploy detects access drift in an **interactive terminal** it now
240
+ **offers to reconcile inline** (answer `y` to route straight into the
241
+ gated apply flow, which shows the plan and confirms again before it
242
+ opens the PR); in a non-interactive session it prints the manual
243
+ `launchpad deploy --apply` route and exits non-zero. Either way the fix
244
+ is the gated Terraform apply — see `/launchpad-content-pr`. Never route
245
+ the user to the platform team for a group change, and never tell them
246
+ access drift is unfixable: the self-serve reconcile is one keystroke.
242
247
 
243
248
  If `launchpad groups list` fails with:
244
249
 
@@ -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.46.1
4
+ version: 0.47.0
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.46.1
4
+ version: 0.47.0
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.46.1
4
+ version: 0.47.0
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.46.1
4
+ version: 0.47.0
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.46.1
4
+ version: 0.47.0
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: Show whether a Launchpad app's local launchpad.yaml matches what's deployed, and read the deployed manifest. Wraps `launchpad pull` (fetch deployed YAML) and `launchpad status` (drift report). Use when someone says "is my app in sync", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or after `launchpad deploy` to verify the change landed.
4
- version: 0.46.1
4
+ version: 0.47.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->