@launchsecure/launch-kit 0.0.28 → 0.0.29

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.
Files changed (23) hide show
  1. package/dist/server/chart-serve.js +3 -1
  2. package/dist/server/cli.js +231 -221
  3. package/dist/server/council-entry.js +0 -0
  4. package/dist/server/course-entry.js +3 -3
  5. package/dist/server/fb-wizard.js +0 -0
  6. package/dist/server/graph-mcp-entry.js +35 -72
  7. package/dist/server/init-entry.js +794 -195
  8. package/package.json +22 -21
  9. package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +4 -4
  10. package/scaffolds/ls-marketplace/plugins/{ls → kit}/.claude-plugin/plugin.json +1 -10
  11. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/activate-beacon.md +2 -2
  12. package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +46 -0
  13. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-array.md +3 -3
  14. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-clear.md +2 -2
  15. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-pulse.md +7 -7
  16. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-scan.md +7 -7
  17. package/scaffolds/ls-marketplace/plugins/kit/commands/deactivate-statusline.md +34 -0
  18. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/show-mcp-status.md +6 -6
  19. package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/standup.md +52 -38
  20. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  21. package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
  22. package/scaffolds/statusline/statusline-mcp.sh +192 -0
  23. package/scaffolds/statusline/statusline-wrapper.sh +50 -0
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,12 +30,119 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
33
+ // src/server/statusline-install.ts
34
+ var statusline_install_exports = {};
35
+ __export(statusline_install_exports, {
36
+ activateStatusline: () => activateStatusline,
37
+ deactivateStatusline: () => deactivateStatusline
38
+ });
39
+ function readSettings() {
40
+ if (!fs3.existsSync(SETTINGS_PATH)) return null;
41
+ try {
42
+ return JSON.parse(fs3.readFileSync(SETTINGS_PATH, "utf-8"));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function writeSettings(s) {
48
+ fs3.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
49
+ fs3.writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
50
+ }
51
+ function readScaffold(name) {
52
+ const p = path3.resolve(__dirname, "..", "..", "scaffolds", "statusline", name);
53
+ return fs3.readFileSync(p, "utf-8");
54
+ }
55
+ function writeScripts() {
56
+ fs3.mkdirSync(LK_DIR, { recursive: true });
57
+ fs3.writeFileSync(WRAPPER_PATH, readScaffold("statusline-wrapper.sh"), { mode: 493 });
58
+ fs3.writeFileSync(CHIP_PATH, readScaffold("statusline-mcp.sh"), { mode: 493 });
59
+ }
60
+ function wrapperCommand(opts) {
61
+ const env = [];
62
+ if (opts.show) env.push(`LK_STATUSLINE_SHOW=${opts.show}`);
63
+ if (opts.compact) env.push(`LK_STATUSLINE_COMPACT=1`);
64
+ const prefix = env.length > 0 ? env.join(" ") + " " : "";
65
+ return `${prefix}bash ${WRAPPER_PATH}`;
66
+ }
67
+ function activateStatusline(opts = {}) {
68
+ const settings = readSettings();
69
+ if (!settings) {
70
+ return { ok: false, outcome: "no-settings", message: `no ~/.claude/settings.json \u2014 nothing to wrap` };
71
+ }
72
+ const currentCmd = settings.statusLine?.command;
73
+ const alreadyWrapped = typeof currentCmd === "string" && currentCmd.includes(WRAPPER_PATH);
74
+ if (alreadyWrapped) {
75
+ writeScripts();
76
+ if (opts.show !== void 0 || opts.compact !== void 0) {
77
+ const updated = {
78
+ ...settings,
79
+ statusLine: { type: "command", command: wrapperCommand(opts) }
80
+ };
81
+ writeSettings(updated);
82
+ const parts = [];
83
+ if (opts.show) parts.push(`--show=${opts.show}`);
84
+ if (opts.compact) parts.push("--compact");
85
+ const desc = parts.length > 0 ? parts.join(" ") : "default (all chips, verbose)";
86
+ return { ok: true, outcome: "refreshed", message: `refreshed chip scripts and updated mode: ${desc}` };
87
+ }
88
+ return { ok: true, outcome: "refreshed", message: "statusline already wrapped \u2014 refreshed chip scripts only" };
89
+ }
90
+ if (!currentCmd) {
91
+ return { ok: false, outcome: "no-statusline", message: "no statusLine.command in ~/.claude/settings.json \u2014 launch-kit only extends an existing statusline" };
92
+ }
93
+ writeScripts();
94
+ const wrapped = {
95
+ ...settings,
96
+ statusLine: { type: "command", command: wrapperCommand(opts) },
97
+ [ORIGINAL_KEY]: settings.statusLine
98
+ };
99
+ writeSettings(wrapped);
100
+ const modeParts = [];
101
+ if (opts.show) modeParts.push(`chips: ${opts.show}`);
102
+ if (opts.compact) modeParts.push("compact mode");
103
+ const modeDesc = modeParts.length > 0 ? ` (${modeParts.join(", ")})` : "";
104
+ return { ok: true, outcome: "activated", message: `wrapped statusLine.command${modeDesc}; original stashed under ${ORIGINAL_KEY}` };
105
+ }
106
+ function deactivateStatusline() {
107
+ const settings = readSettings();
108
+ if (!settings) return { ok: false, outcome: "no-settings", message: "no ~/.claude/settings.json" };
109
+ const original = settings[ORIGINAL_KEY];
110
+ if (!original) {
111
+ return { ok: false, outcome: "not-active", message: `no ${ORIGINAL_KEY} in settings.json \u2014 statusline isn't wrapped by launch-kit` };
112
+ }
113
+ const restored = { ...settings, statusLine: original };
114
+ delete restored[ORIGINAL_KEY];
115
+ writeSettings(restored);
116
+ for (const p of [WRAPPER_PATH, CHIP_PATH]) {
117
+ try {
118
+ fs3.unlinkSync(p);
119
+ } catch {
120
+ }
121
+ }
122
+ return { ok: true, outcome: "deactivated", message: "restored original statusLine.command" };
123
+ }
124
+ var fs3, path3, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
125
+ var init_statusline_install = __esm({
126
+ "src/server/statusline-install.ts"() {
127
+ "use strict";
128
+ fs3 = __toESM(require("node:fs"));
129
+ path3 = __toESM(require("node:path"));
130
+ import_node_os = require("node:os");
131
+ LK_DIR = path3.join((0, import_node_os.homedir)(), ".launchsecure");
132
+ WRAPPER_PATH = path3.join(LK_DIR, "statusline-wrapper.sh");
133
+ CHIP_PATH = path3.join(LK_DIR, "statusline-mcp.sh");
134
+ SETTINGS_PATH = path3.join((0, import_node_os.homedir)(), ".claude", "settings.json");
135
+ ORIGINAL_KEY = "_launchKitStatuslineOriginal";
136
+ }
137
+ });
138
+
26
139
  // src/server/init-entry.ts
27
140
  var import_node_child_process = require("node:child_process");
28
- var fs2 = __toESM(require("node:fs"));
141
+ var crypto = __toESM(require("node:crypto"));
142
+ var fs4 = __toESM(require("node:fs"));
29
143
  var import_node_http = require("node:http");
30
144
  var import_node_https = require("node:https");
31
- var path2 = __toESM(require("node:path"));
145
+ var path4 = __toESM(require("node:path"));
32
146
  var readline = __toESM(require("node:readline"));
33
147
  var import_node_url = require("node:url");
34
148
 
@@ -95,12 +209,94 @@ function writeJsonAtomic(absPath, value, mode) {
95
209
  fs.renameSync(tmp, absPath);
96
210
  }
97
211
 
212
+ // src/server/cred-recovery.ts
213
+ var fs2 = __toESM(require("node:fs"));
214
+ var path2 = __toESM(require("node:path"));
215
+ var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
216
+ function migrateLegacyCredFile(targetDir, opts) {
217
+ const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
218
+ const dest = path2.join(targetDir, CONFIG_FILENAME);
219
+ if (!fs2.existsSync(legacy) || fs2.existsSync(dest)) return;
220
+ let parsed;
221
+ try {
222
+ parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
223
+ } catch {
224
+ return;
225
+ }
226
+ const pat = parsed?.pat;
227
+ if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
228
+ if (opts.dryRun) {
229
+ opts.log.dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
230
+ return;
231
+ }
232
+ fs2.renameSync(legacy, dest);
233
+ removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME, opts);
234
+ opts.log.ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
235
+ }
236
+ function removeGitignoreLine(targetDir, line, opts) {
237
+ const p = path2.join(targetDir, ".gitignore");
238
+ if (!fs2.existsSync(p)) return;
239
+ const before = fs2.readFileSync(p, "utf-8");
240
+ const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
241
+ if (after === before) return;
242
+ if (opts.dryRun) {
243
+ opts.log.dryNote(`would remove "${line}" from .gitignore`);
244
+ return;
245
+ }
246
+ fs2.writeFileSync(p, after, "utf-8");
247
+ opts.log.ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
248
+ }
249
+ function recoverCredFromMcp(targetDir) {
250
+ const p = path2.join(targetDir, ".mcp.json");
251
+ if (!fs2.existsSync(p)) return null;
252
+ let mcp;
253
+ try {
254
+ mcp = JSON.parse(fs2.readFileSync(p, "utf-8"));
255
+ } catch {
256
+ return null;
257
+ }
258
+ const entry = mcp.mcpServers?.["launch-secure"];
259
+ if (!entry?.headers || !entry.url) return null;
260
+ const auth = entry.headers["Authorization"];
261
+ const pat = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice("Bearer ".length).trim() : null;
262
+ const orgSlug = entry.headers["X-Org-Slug"];
263
+ const projectSlug = entry.headers["X-Project-Slug"];
264
+ if (!pat || !pat.startsWith("ls_pat_") || !orgSlug || !projectSlug) return null;
265
+ const serverUrl = entry.url.replace(/\/api\/mcp\/project\/?$/, "").replace(/\/+$/, "");
266
+ return { pat, orgSlug, projectSlug, serverUrl };
267
+ }
268
+ function recoverCred(targetDir, opts) {
269
+ migrateLegacyCredFile(targetDir, opts);
270
+ let cred = null;
271
+ try {
272
+ cred = readCredFile(targetDir);
273
+ } catch {
274
+ }
275
+ if (cred) return { cred, source: "cred-file" };
276
+ if (opts.dryRun) {
277
+ const legacyPath = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
278
+ if (fs2.existsSync(legacyPath)) {
279
+ try {
280
+ const parsed = JSON.parse(fs2.readFileSync(legacyPath, "utf-8"));
281
+ if (typeof parsed?.pat === "string" && parsed.pat.startsWith("ls_pat_")) {
282
+ opts.log.info(`(dry-run) using legacy ${LEGACY_CONFIG_FILENAME} for preview \u2014 a real run would migrate it to ${CONFIG_FILENAME} first`);
283
+ return { cred: parsed, source: "legacy-dry-run" };
284
+ }
285
+ } catch {
286
+ }
287
+ }
288
+ }
289
+ const recovered = recoverCredFromMcp(targetDir);
290
+ if (recovered) return { cred: recovered, source: "mcp" };
291
+ return { cred: null, source: null };
292
+ }
293
+
98
294
  // src/server/init-entry.ts
295
+ init_statusline_install();
99
296
  var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
100
- var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
101
297
  var ONBOARD_SCRIPT_NAME = "onboard";
102
298
  var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
103
- var LAUNCH_KIT_TOOLS_GUIDE = `
299
+ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
104
300
  Wired in Claude Code (.mcp.json):
105
301
  launch-secure \u2014 LS API: work items, comms, secrets, members, board
106
302
  launch-chart \u2014 code search + project graph (use instead of grep/glob)
@@ -115,41 +311,56 @@ Other tools (run on demand via npx):
115
311
  in-browser monitor. Paste the printed URL into
116
312
  the beacon debug panel; events stream to
117
313
  .launchsecure/beacon-<token>.ndjson for the
118
- /ls:beacon-* commands below to read.
119
-
120
- LS slash commands (run inside Claude Code in this project):
121
- /ls:activate-beacon \u2014 wire the launch-kit-beacon in-app feedback
122
- widget into this app (mounts the <launch-kit-
123
- beacon> Web Component + scaffolds /api/feedback
124
- forwarding to LaunchSecure Comm Hub)
125
- /ls:standup \u2014 draft a daily standup from work since the last
126
- push (chart-grouped themes, work-item linkage,
127
- release detection) and post to LS Comm Hub as
128
- a daily_update after you confirm
129
- /ls:show-mcp-status \u2014 show recall watcher health + last snapshot.
130
- Add 'full' for expanded report (PID, shadow
131
- repo size, recent snaps)
132
- /ls:beacon-scan \u2014 scan recent events from the active
133
- launch-beacon monitor session. Pass a kind
134
- (error/click/fetch/route/dialog/probe) and/or
135
- a limit to filter.
136
- /ls:beacon-pulse \u2014 most recent error + the N events that preceded
137
- it. "What was happening just before it broke."
138
- /ls:beacon-array \u2014 list monitor sessions in .launchsecure/ with
139
- event counts, last activity, liveness glyph.
140
- Add 'full' for a per-session expanded report.
141
- /ls:beacon-clear \u2014 wipe the latest monitor session NDJSON (or
142
- 'all'). Confirms before deleting.
143
-
314
+ /kit:beacon-* commands below to read.
315
+ `;
316
+ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
144
317
  Open this repo in Claude Code; on first open you'll be prompted to install
145
- the "launchsecure" marketplace \u2014 accept to enable the commands above.
318
+ the "launch-secure" marketplace \u2014 accept to enable the commands above.
319
+ `;
320
+ function renderLsCommandsSection() {
321
+ const dir = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit", "commands");
322
+ if (!fs4.existsSync(dir)) {
323
+ return "\nLS slash commands: (scaffold dir not bundled \u2014 this is a packaging bug)\n";
324
+ }
325
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
326
+ if (files.length === 0) return "\nLS slash commands: (none defined)\n";
327
+ const lines = files.map((file) => {
328
+ const name = file.replace(/\.md$/, "");
329
+ const text = fs4.readFileSync(path4.join(dir, file), "utf-8");
330
+ const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
331
+ const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
332
+ const padded = `/${PLUGIN_ID}:${name}`.padEnd(26);
333
+ return ` ${padded} \u2014 ${desc}`;
334
+ });
335
+ return `
336
+ LS slash commands (run inside Claude Code in this project):
337
+ ${lines.join("\n")}
146
338
  `;
339
+ }
340
+ function getLaunchKitToolsGuide() {
341
+ return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
342
+ }
147
343
  var PACKAGE_MANAGERS = [
148
344
  { name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
149
345
  { name: "yarn", binary: "yarn", lockfiles: ["yarn.lock"], installArgs: ["install"] },
150
346
  { name: "bun", binary: "bun", lockfiles: ["bun.lockb", "bun.lock"], installArgs: ["install"] },
151
347
  { name: "npm", binary: "npm", lockfiles: ["package-lock.json"], installArgs: ["install"] }
152
348
  ];
349
+ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
350
+ "--help",
351
+ "-h",
352
+ "--no-install",
353
+ "--no-onboard",
354
+ "--no-recall",
355
+ "--no-migrate-safety",
356
+ "--no-ls-marketplace",
357
+ "--no-recall-hook",
358
+ "--refresh-scaffolds",
359
+ "--quiet",
360
+ "--force",
361
+ "--dry-run"
362
+ ]);
363
+ var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
153
364
  function parseArgs(argv) {
154
365
  const args = {
155
366
  token: process.env.LS_PAT ?? null,
@@ -159,14 +370,21 @@ function parseArgs(argv) {
159
370
  targetDir: null,
160
371
  course: null,
161
372
  noInstall: false,
373
+ noOnboard: false,
162
374
  noRecall: false,
163
375
  noMigrateSafety: false,
164
376
  noLsMarketplace: false,
165
377
  noRecallHook: false,
378
+ refreshScaffolds: false,
379
+ quiet: false,
380
+ force: false,
166
381
  dryRun: false,
167
382
  help: false
168
383
  };
169
- for (const raw of argv) {
384
+ const unknown = [];
385
+ for (let i = 0; i < argv.length; i++) {
386
+ const raw = argv[i];
387
+ if (i === 0 && !raw.startsWith("--") && !raw.startsWith("-")) continue;
170
388
  if (raw === "--help" || raw === "-h") {
171
389
  args.help = true;
172
390
  continue;
@@ -175,6 +393,10 @@ function parseArgs(argv) {
175
393
  args.noInstall = true;
176
394
  continue;
177
395
  }
396
+ if (raw === "--no-onboard") {
397
+ args.noOnboard = true;
398
+ continue;
399
+ }
178
400
  if (raw === "--no-recall") {
179
401
  args.noRecall = true;
180
402
  continue;
@@ -191,28 +413,116 @@ function parseArgs(argv) {
191
413
  args.noRecallHook = true;
192
414
  continue;
193
415
  }
416
+ if (raw === "--refresh-scaffolds") {
417
+ args.refreshScaffolds = true;
418
+ continue;
419
+ }
420
+ if (raw === "--quiet") {
421
+ args.quiet = true;
422
+ continue;
423
+ }
424
+ if (raw === "--force") {
425
+ args.force = true;
426
+ continue;
427
+ }
194
428
  if (raw === "--dry-run") {
195
429
  args.dryRun = true;
196
430
  continue;
197
431
  }
198
432
  const eq = raw.indexOf("=");
199
- if (!raw.startsWith("--") || eq < 0) continue;
200
- const key = raw.slice(2, eq);
201
- const val = raw.slice(eq + 1);
202
- if (key === "token") args.token = val;
203
- else if (key === "org") args.orgSlug = val;
204
- else if (key === "project") args.projectSlug = val;
205
- else if (key === "url") args.serverUrl = val.replace(/\/+$/, "");
206
- else if (key === "dir") args.targetDir = val;
207
- else if (key === "course") args.course = val;
433
+ if (raw.startsWith("--") && eq > 0) {
434
+ const key = raw.slice(2, eq);
435
+ const val = raw.slice(eq + 1);
436
+ if (key === "token") {
437
+ args.token = val;
438
+ continue;
439
+ }
440
+ if (key === "org") {
441
+ args.orgSlug = val;
442
+ continue;
443
+ }
444
+ if (key === "project") {
445
+ args.projectSlug = val;
446
+ continue;
447
+ }
448
+ if (key === "url") {
449
+ args.serverUrl = val.replace(/\/+$/, "");
450
+ continue;
451
+ }
452
+ if (key === "dir") {
453
+ args.targetDir = val;
454
+ continue;
455
+ }
456
+ if (key === "course") {
457
+ args.course = val;
458
+ continue;
459
+ }
460
+ unknown.push(raw);
461
+ continue;
462
+ }
463
+ if (raw.startsWith("-")) {
464
+ unknown.push(raw);
465
+ continue;
466
+ }
467
+ unknown.push(raw);
468
+ }
469
+ if (unknown.length > 0) {
470
+ const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
471
+ const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
472
+ fail(`Unknown argument(s): ${unknown.join(" ")}
473
+ Known boolean flags: ${knownBool}
474
+ Known key=value flags: ${knownKv}`);
208
475
  }
209
476
  return args;
210
477
  }
478
+ function printRefreshHelp() {
479
+ console.log(`launch-kit refresh \u2014 re-apply launch-kit scaffolds in an already-initialized project
480
+
481
+ Usage:
482
+ npx @launchsecure/launch-kit@latest refresh [--dir=<path>] [options]
483
+
484
+ What it does:
485
+ Re-merges .mcp.json with the latest launch-kit entries (preserves your other
486
+ entries), refreshes the launch-secure marketplace tree at .claude/marketplace/
487
+ (picks up any new /kit:* commands), refreshes scripts/ensure-recall.sh and
488
+ the SessionStart hook, and refreshes the migrate-safety scaffold.
489
+
490
+ Reads org/project/serverUrl/pat from the existing .launch-secure.cred.config.
491
+ Does NOT clone, re-install deps, re-prompt for PAT, or re-run the onboard
492
+ script. Use \`init\` for those.
493
+
494
+ Options:
495
+ --dir=<path> Target directory (default: cwd). Must contain a
496
+ valid .launch-secure.cred.config.
497
+ --no-migrate-safety Skip refreshing the migrate-safety scaffold.
498
+ --no-ls-marketplace Skip refreshing the launch-secure marketplace.
499
+ --no-recall-hook Skip refreshing the recall-hook scaffold.
500
+ --refresh-scaffolds Force-overwrite migrate-safety files (default is to
501
+ preserve user edits). Use this to pull updates
502
+ published to @launchsecure/launch-kit.
503
+ --quiet Suppress the post-run tools guide.
504
+ --dry-run Preview every file write without making changes.
505
+ --help Show this help.
506
+
507
+ Tip: prefix the command with @latest (\`launch-kit@latest refresh\`) to force
508
+ npx to pull the newest published version instead of using a cached older one.
509
+ `);
510
+ }
211
511
  function printHelp() {
212
- console.log(`launch-kit init \u2014 bootstrap a LaunchSecure project on this machine
512
+ console.log(`launch-kit \u2014 bootstrap and refresh a LaunchSecure project on this machine
513
+
514
+ Subcommands:
515
+ init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
516
+ refresh Re-apply scaffolds + MCP entries in an already-initialized project
517
+ (no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
518
+ statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
519
+ chips (recall, chart, deck, council) get appended. Refuses to
520
+ create one if none exists \u2014 additive only.
521
+ statusline deactivate Restore the original statusLine.command and remove kit scripts.
213
522
 
214
523
  Usage:
215
- npx launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
524
+ npx @launchsecure/launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
525
+ npx @launchsecure/launch-kit@latest refresh [--dir=<path>] [options]
216
526
 
217
527
  Required:
218
528
  --token=<pat> LaunchSecure PAT (ls_pat_...). Or set LS_PAT env var.
@@ -229,19 +539,34 @@ Options:
229
539
  becomes active; re-run with a different --course
230
540
  and --url to add another (e.g. local + staging).
231
541
  Use \`launch-course set <name>\` to switch later.
232
- --no-install Skip dependency install step.
542
+ --no-install Skip dependency install step (also skips the onboard
543
+ script \u2014 install is its prerequisite).
544
+ --no-onboard Skip the onboard script even when install runs.
233
545
  --no-recall Skip launch-recall (shadow git backup) scaffold.
234
546
  --no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
235
547
  wrapper + GitHub Action + runbook).
236
- --no-ls-marketplace Skip the Claude Code "launchsecure" marketplace
548
+ --no-ls-marketplace Skip the Claude Code "launch-secure" marketplace
237
549
  scaffold (.claude/marketplace/ + .claude/settings.json
238
- wiring \u2014 exposes /ls:activate-beacon and future
550
+ wiring \u2014 exposes /kit:activate-beacon and future
239
551
  ls-namespaced slash commands).
240
552
  --no-recall-hook Skip the SessionStart hook scaffold (Claude Code
241
553
  hook + scripts/ensure-recall.sh that auto-restarts
242
554
  the launch-recall watcher if it died between
243
555
  sessions). The hook is the surfacing layer for
244
556
  watcher-died-silently scenarios.
557
+ --refresh-scaffolds Force-overwrite migrate-safety scaffold files even
558
+ when they already exist. Default behavior preserves
559
+ user edits; use this to pull updates published to
560
+ @launchsecure/launch-kit (e.g., a newer
561
+ migrate-with-backup.sh).
562
+ --quiet Suppress the post-run tools guide (the long block
563
+ listing /kit:* commands and wired MCPs). Useful for
564
+ idempotent re-runs in CI or scripts.
565
+ --force Skip the auto-delegate-to-refresh check. By default
566
+ init detects an existing bootstrap (cred file +
567
+ launch-secure MCP entry) and runs refresh instead.
568
+ Pass --force to re-init from scratch even when the
569
+ target dir is already set up.
245
570
  --dry-run Preview every file write, merge, clone, and install
246
571
  command without making any changes. Useful before
247
572
  re-running init against a customized project. The
@@ -265,10 +590,10 @@ What it does:
265
590
  8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
266
591
  9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
267
592
  runbook + .backups/ gitignore line). Skip with --no-migrate-safety.
268
- 10. Scaffolds the Claude Code "launchsecure" marketplace at
593
+ 10. Scaffolds the Claude Code "launch-secure" marketplace at
269
594
  .claude/marketplace/ and wires .claude/settings.json so Claude Code
270
- auto-discovers it and enables the "ls" plugin (exposes
271
- /ls:activate-beacon for wiring the launch-kit-beacon in-app feedback
595
+ auto-discovers it and enables the "kit" plugin (exposes
596
+ /kit:activate-beacon for wiring the launch-kit-beacon in-app feedback
272
597
  widget). Skip with --no-ls-marketplace.
273
598
  11. Scaffolds scripts/ensure-recall.sh and appends a SessionStart hook to
274
599
  .claude/settings.json that respawns the launch-recall watcher if it
@@ -278,9 +603,9 @@ What it does:
278
603
  }
279
604
  async function prompt(question) {
280
605
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
281
- return new Promise((resolve2) => rl.question(question, (answer) => {
606
+ return new Promise((resolve3) => rl.question(question, (answer) => {
282
607
  rl.close();
283
- resolve2(answer.trim());
608
+ resolve3(answer.trim());
284
609
  }));
285
610
  }
286
611
  function fail(msg) {
@@ -310,8 +635,17 @@ function preflight() {
310
635
  ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
311
636
  return { hasGh };
312
637
  }
313
- function callProjectInfo(args) {
314
- return new Promise((resolve2, reject) => {
638
+ var PROJECT_INFO_TIMEOUT_MS = 3e4;
639
+ var PROJECT_INFO_MAX_ATTEMPTS = 3;
640
+ var ProjectInfoHttpError = class extends Error {
641
+ constructor(status, retryable, message) {
642
+ super(message);
643
+ this.status = status;
644
+ this.retryable = retryable;
645
+ }
646
+ };
647
+ function attemptProjectInfo(args) {
648
+ return new Promise((resolve3, reject) => {
315
649
  const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
316
650
  const body = JSON.stringify({
317
651
  jsonrpc: "2.0",
@@ -343,12 +677,24 @@ function callProjectInfo(args) {
343
677
  res.on("data", (c) => chunks.push(c));
344
678
  res.on("end", () => {
345
679
  const text = Buffer.concat(chunks).toString("utf-8");
346
- if (res.statusCode === 401 || res.statusCode === 403) {
347
- reject(new Error(`PAT rejected (${res.statusCode}). Check token + that it has access to ${args.orgSlug}/${args.projectSlug}.`));
680
+ if (res.statusCode === 401) {
681
+ reject(new ProjectInfoHttpError(401, false, `PAT rejected (401). Token is invalid or expired. Generate a new PAT at ${args.serverUrl}/settings/tokens and retry.`));
682
+ return;
683
+ }
684
+ if (res.statusCode === 403) {
685
+ reject(new ProjectInfoHttpError(403, false, `Access denied (403). Token is valid but lacks access to ${args.orgSlug}/${args.projectSlug}. Check membership/permissions on the project.`));
686
+ return;
687
+ }
688
+ if (res.statusCode === 404) {
689
+ reject(new ProjectInfoHttpError(404, false, `Project not found (404). Verify ${args.orgSlug}/${args.projectSlug} exists at ${args.serverUrl}.`));
690
+ return;
691
+ }
692
+ if (res.statusCode && res.statusCode >= 500) {
693
+ reject(new ProjectInfoHttpError(res.statusCode, true, `LaunchSecure server error ${res.statusCode}: ${text.slice(0, 200)}`));
348
694
  return;
349
695
  }
350
696
  if (res.statusCode && res.statusCode >= 400) {
351
- reject(new Error(`LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
697
+ reject(new ProjectInfoHttpError(res.statusCode, false, `LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
352
698
  return;
353
699
  }
354
700
  let json = text;
@@ -377,7 +723,7 @@ function callProjectInfo(args) {
377
723
  return;
378
724
  }
379
725
  const payload = JSON.parse(inner);
380
- resolve2({
726
+ resolve3({
381
727
  orgSlug: payload.org.slug,
382
728
  projectSlug: payload.project.slug,
383
729
  projectName: payload.project.name,
@@ -389,11 +735,34 @@ function callProjectInfo(args) {
389
735
  });
390
736
  }
391
737
  );
392
- req.on("error", reject);
738
+ req.setTimeout(PROJECT_INFO_TIMEOUT_MS, () => {
739
+ req.destroy(new Error(`project_info timed out after ${PROJECT_INFO_TIMEOUT_MS / 1e3}s`));
740
+ });
741
+ req.on("error", (err) => {
742
+ const code = err.code;
743
+ const retryable = code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EAI_AGAIN" || /timed out/.test(err.message);
744
+ reject(retryable ? new ProjectInfoHttpError(0, true, err.message) : err);
745
+ });
393
746
  req.write(body);
394
747
  req.end();
395
748
  });
396
749
  }
750
+ async function callProjectInfo(args) {
751
+ let lastErr;
752
+ for (let attempt = 1; attempt <= PROJECT_INFO_MAX_ATTEMPTS; attempt++) {
753
+ try {
754
+ return await attemptProjectInfo(args);
755
+ } catch (err) {
756
+ lastErr = err;
757
+ const retryable = err instanceof ProjectInfoHttpError && err.retryable;
758
+ if (!retryable || attempt === PROJECT_INFO_MAX_ATTEMPTS) break;
759
+ const delayMs = 1e3 * 2 ** (attempt - 1);
760
+ info(`project_info attempt ${attempt}/${PROJECT_INFO_MAX_ATTEMPTS} failed (${err instanceof Error ? err.message : String(err)}) \u2014 retrying in ${delayMs}ms`);
761
+ await new Promise((r) => setTimeout(r, delayMs));
762
+ }
763
+ }
764
+ throw lastErr;
765
+ }
397
766
  function gitRemoteUrl(dir) {
398
767
  const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
399
768
  if (res.status !== 0) return null;
@@ -411,11 +780,11 @@ function normalizeRepoUrl(url) {
411
780
  }
412
781
  }
413
782
  function isGitRepo(dir) {
414
- return fs2.existsSync(path2.join(dir, ".git"));
783
+ return fs4.existsSync(path4.join(dir, ".git"));
415
784
  }
416
785
  function dirIsEmpty(dir) {
417
- if (!fs2.existsSync(dir)) return true;
418
- return fs2.readdirSync(dir).length === 0;
786
+ if (!fs4.existsSync(dir)) return true;
787
+ return fs4.readdirSync(dir).length === 0;
419
788
  }
420
789
  function cloneRepo(repoUrl, targetDir, hasGh) {
421
790
  const isGithub = /github\.com/i.test(repoUrl);
@@ -440,11 +809,14 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
440
809
  `Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\` or an SSH key on your GitHub account.`
441
810
  );
442
811
  }
812
+ if (!fs4.existsSync(path4.join(targetDir, ".git"))) {
813
+ fail(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
814
+ }
443
815
  ok(`cloned to ${targetDir}`);
444
816
  }
445
817
  function writeConfigFile(targetDir, cfg, courseName) {
446
- migrateLegacyCredFile(targetDir);
447
- const p = path2.join(targetDir, CONFIG_FILENAME);
818
+ recoverCred(targetDir, getRecoveryOptions());
819
+ const p = path4.join(targetDir, CONFIG_FILENAME);
448
820
  const existing = readCredFile(targetDir);
449
821
  const isNew = existing === null;
450
822
  const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
@@ -458,38 +830,26 @@ function writeConfigFile(targetDir, cfg, courseName) {
458
830
  const action = isNew ? "wrote" : isUpdate ? `updated course "${courseName}" in` : `added course "${courseName}" to`;
459
831
  ok(`${action} ${CONFIG_FILENAME} (active: ${courseName})`);
460
832
  }
461
- function migrateLegacyCredFile(targetDir) {
462
- const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
463
- const dest = path2.join(targetDir, CONFIG_FILENAME);
464
- if (!fs2.existsSync(legacy) || fs2.existsSync(dest)) return;
465
- let parsed;
466
- try {
467
- parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
468
- } catch {
469
- return;
833
+ var recoveryLog = { info, ok, dryNote };
834
+ function getRecoveryOptions() {
835
+ return { dryRun: DRY_RUN, log: recoveryLog };
836
+ }
837
+ function detectExistingBootstrap(targetDir) {
838
+ if (!fs4.existsSync(path4.join(targetDir, CONFIG_FILENAME))) {
839
+ return { bootstrapped: false };
470
840
  }
471
- const pat = parsed?.pat;
472
- if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
473
- if (DRY_RUN) {
474
- dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
475
- return;
841
+ const mcpPath = path4.join(targetDir, ".mcp.json");
842
+ if (!fs4.existsSync(mcpPath)) {
843
+ return { bootstrapped: false };
476
844
  }
477
- fs2.renameSync(legacy, dest);
478
- removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME);
479
- ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
480
- }
481
- function removeGitignoreLine(targetDir, line) {
482
- const p = path2.join(targetDir, ".gitignore");
483
- if (!fs2.existsSync(p)) return;
484
- const before = fs2.readFileSync(p, "utf-8");
485
- const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
486
- if (after === before) return;
487
- if (DRY_RUN) {
488
- dryNote(`would remove "${line}" from .gitignore`);
489
- return;
845
+ try {
846
+ const mcp = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
847
+ if (mcp.mcpServers?.["launch-secure"]) {
848
+ return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
849
+ }
850
+ } catch {
490
851
  }
491
- fs2.writeFileSync(p, after, "utf-8");
492
- ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
852
+ return { bootstrapped: false };
493
853
  }
494
854
  var LAUNCH_SECURE_HEADERS_HELPER = `node -e 'const j=JSON.parse(require("fs").readFileSync(".launch-secure.cred.config","utf-8"));const c=j.profiles&&j.active?j.profiles[j.active]:j;process.stdout.write(JSON.stringify({Authorization:"Bearer "+c.pat,"X-Org-Slug":c.orgSlug,"X-Project-Slug":c.projectSlug}))'`;
495
855
  function buildLaunchKitMcpEntries(cfg) {
@@ -502,6 +862,11 @@ function buildLaunchKitMcpEntries(cfg) {
502
862
  "launch-chart": {
503
863
  command: "npx",
504
864
  args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
865
+ // Tells launch-chart to also start its HTTP UI alongside the MCP, so
866
+ // users can open the chart viewer at localhost:<port> while queries
867
+ // hit the MCP. Without this, the MCP runs passively (queries work,
868
+ // no UI). I4 deep-merge preserves user-added env keys; this default
869
+ // ensures the auto-serve UX ships out of the box.
505
870
  env: { LAUNCH_CHART_AUTOSERVE: "1" }
506
871
  },
507
872
  "launch-deck": {
@@ -518,13 +883,29 @@ function buildLaunchKitMcpEntries(cfg) {
518
883
  }
519
884
  };
520
885
  }
886
+ function mergeMcpEntry(existing, ours) {
887
+ const merged = { ...existing, ...ours };
888
+ if (existing.headers || ours.headers) {
889
+ const authKeys = /* @__PURE__ */ new Set(["Authorization", "X-Org-Slug", "X-Project-Slug"]);
890
+ const baseHeaders = ours.headersHelper ? Object.fromEntries(Object.entries(existing.headers ?? {}).filter(([k]) => !authKeys.has(k))) : { ...existing.headers ?? {} };
891
+ const combinedHeaders = { ...baseHeaders, ...ours.headers ?? {} };
892
+ if (Object.keys(combinedHeaders).length > 0) merged.headers = combinedHeaders;
893
+ else delete merged.headers;
894
+ }
895
+ if (existing.env || ours.env) {
896
+ const combinedEnv = { ...existing.env ?? {}, ...ours.env ?? {} };
897
+ if (Object.keys(combinedEnv).length > 0) merged.env = combinedEnv;
898
+ else delete merged.env;
899
+ }
900
+ return merged;
901
+ }
521
902
  function mergeMcpFile(targetDir, launchKitEntries) {
522
- const p = path2.join(targetDir, ".mcp.json");
523
- const hadExisting = fs2.existsSync(p);
903
+ const p = path4.join(targetDir, ".mcp.json");
904
+ const hadExisting = fs4.existsSync(p);
524
905
  let existing = {};
525
906
  if (hadExisting) {
526
907
  try {
527
- existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
908
+ existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
528
909
  } catch (err) {
529
910
  fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
530
911
  }
@@ -534,24 +915,29 @@ function mergeMcpFile(targetDir, launchKitEntries) {
534
915
  const overwrites = [];
535
916
  const additions = [];
536
917
  for (const [name, entry] of Object.entries(launchKitEntries)) {
537
- if (merged.mcpServers[name]) overwrites.push(name);
538
- else additions.push(name);
539
- merged.mcpServers[name] = entry;
918
+ const existingEntry = merged.mcpServers[name];
919
+ if (existingEntry) {
920
+ overwrites.push(name);
921
+ merged.mcpServers[name] = mergeMcpEntry(existingEntry, entry);
922
+ } else {
923
+ additions.push(name);
924
+ merged.mcpServers[name] = entry;
925
+ }
540
926
  }
541
927
  if (DRY_RUN) {
542
928
  const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
543
929
  dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
544
930
  return;
545
931
  }
546
- fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
932
+ fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
547
933
  const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
548
934
  ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
549
935
  }
550
936
  function detectPackageManager(repoDir) {
551
- const pkgPath = path2.join(repoDir, "package.json");
552
- if (!fs2.existsSync(pkgPath)) return null;
937
+ const pkgPath = path4.join(repoDir, "package.json");
938
+ if (!fs4.existsSync(pkgPath)) return null;
553
939
  try {
554
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
940
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
555
941
  if (typeof pkg.packageManager === "string") {
556
942
  const name = pkg.packageManager.split("@")[0];
557
943
  const match = PACKAGE_MANAGERS.find((p) => p.name === name);
@@ -560,7 +946,7 @@ function detectPackageManager(repoDir) {
560
946
  }
561
947
  } catch {
562
948
  }
563
- const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs2.existsSync(path2.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
949
+ const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs4.existsSync(path4.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
564
950
  if (matches.length === 1) {
565
951
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
566
952
  }
@@ -569,8 +955,8 @@ function detectPackageManager(repoDir) {
569
955
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
570
956
  }
571
957
  for (const pm of PACKAGE_MANAGERS) {
572
- if (pm.workspaceFiles?.some((wf) => fs2.existsSync(path2.join(repoDir, wf)))) {
573
- return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs2.existsSync(path2.join(repoDir, wf)))})` };
958
+ if (pm.workspaceFiles?.some((wf) => fs4.existsSync(path4.join(repoDir, wf)))) {
959
+ return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs4.existsSync(path4.join(repoDir, wf)))})` };
574
960
  }
575
961
  }
576
962
  const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
@@ -580,7 +966,7 @@ function runInstall(repoDir, detected) {
580
966
  const { pm } = detected;
581
967
  if (!which(pm.binary)) {
582
968
  fail(
583
- `${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
969
+ `${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
584
970
  );
585
971
  }
586
972
  info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
@@ -591,16 +977,32 @@ function runInstall(repoDir, detected) {
591
977
  const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
592
978
  if (res.status !== 0) {
593
979
  fail(
594
- `${pm.name} install failed (exit ${res.status}). Configs and clone are intact \u2014 fix the underlying error and retry: cd ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
980
+ `${pm.name} install failed (exit ${res.status}).
981
+
982
+ Half-init state \u2014 install didn't complete, but these files ARE on disk:
983
+ - ${path4.join(repoDir, CONFIG_FILENAME)} (cred file)
984
+ - ${path4.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
985
+ - ${path4.join(repoDir, ".gitignore")} (cred line appended)
986
+ - clone at ${repoDir}
987
+
988
+ Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
989
+
990
+ To retry install only:
991
+ cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
992
+
993
+ To re-run init after fixing the install error:
994
+ cd ${path4.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
995
+
996
+ To fully reset: delete the files listed above and the clone, then re-init.`
595
997
  );
596
998
  }
597
999
  ok(`${pm.name} install complete`);
598
1000
  }
599
1001
  function hasOnboardScript(repoDir) {
600
- const pkgPath = path2.join(repoDir, "package.json");
601
- if (!fs2.existsSync(pkgPath)) return false;
1002
+ const pkgPath = path4.join(repoDir, "package.json");
1003
+ if (!fs4.existsSync(pkgPath)) return false;
602
1004
  try {
603
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
1005
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
604
1006
  return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
605
1007
  } catch {
606
1008
  return false;
@@ -608,8 +1010,8 @@ function hasOnboardScript(repoDir) {
608
1010
  }
609
1011
  function runRecallInit(repoDir) {
610
1012
  info(`scaffolding launch-recall (shadow git backup) \u2026`);
611
- const recallEntry = path2.resolve(__dirname, "recall-entry.js");
612
- const useSibling = fs2.existsSync(recallEntry);
1013
+ const recallEntry = path4.resolve(__dirname, "recall-entry.js");
1014
+ const useSibling = fs4.existsSync(recallEntry);
613
1015
  const cmd = useSibling ? process.execPath : "npx";
614
1016
  const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
615
1017
  if (DRY_RUN) {
@@ -618,7 +1020,7 @@ function runRecallInit(repoDir) {
618
1020
  }
619
1021
  const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
620
1022
  if (res.status !== 0) {
621
- info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path2.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
1023
+ info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path4.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
622
1024
  return;
623
1025
  }
624
1026
  ok(`launch-recall ready (shadow git initialized)`);
@@ -632,14 +1034,14 @@ function runOnboard(repoDir, pm) {
632
1034
  const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
633
1035
  if (res.status !== 0) {
634
1036
  fail(
635
- `${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path2.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
1037
+ `${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path4.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
636
1038
  );
637
1039
  }
638
1040
  ok(`${ONBOARD_SCRIPT_NAME} script complete`);
639
1041
  }
640
1042
  function ensureGitignoreLine(targetDir, line) {
641
- const p = path2.join(targetDir, ".gitignore");
642
- let content = fs2.existsSync(p) ? fs2.readFileSync(p, "utf-8") : "";
1043
+ const p = path4.join(targetDir, ".gitignore");
1044
+ let content = fs4.existsSync(p) ? fs4.readFileSync(p, "utf-8") : "";
643
1045
  const lines = content.split(/\r?\n/);
644
1046
  if (lines.some((l) => l.trim() === line)) return;
645
1047
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -649,131 +1051,191 @@ function ensureGitignoreLine(targetDir, line) {
649
1051
  dryNote(`would append "${line}" to .gitignore`);
650
1052
  return;
651
1053
  }
652
- fs2.writeFileSync(p, content, "utf-8");
1054
+ fs4.writeFileSync(p, content, "utf-8");
653
1055
  ok(`appended ${line} to .gitignore`);
654
1056
  }
655
- function copyScaffoldIfMissing(srcPath, destPath, label) {
656
- if (!fs2.existsSync(srcPath)) return "missing-src";
657
- if (fs2.existsSync(destPath)) {
658
- info(`${label} already present \u2014 leaving alone`);
659
- return "existed";
660
- }
661
- if (DRY_RUN) {
662
- dryNote(`would write ${label}`);
663
- return "wrote";
664
- }
665
- fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
666
- fs2.copyFileSync(srcPath, destPath);
667
- try {
668
- const srcMode = fs2.statSync(srcPath).mode;
669
- fs2.chmodSync(destPath, srcMode);
670
- } catch {
671
- }
672
- ok(`wrote ${label}`);
673
- return "wrote";
674
- }
675
1057
  function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
676
- if (!fs2.existsSync(srcDir)) return;
677
- for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
678
- const srcPath = path2.join(srcDir, entry.name);
679
- const destPath = path2.join(destDir, entry.name);
1058
+ if (!fs4.existsSync(srcDir)) return;
1059
+ for (const entry of fs4.readdirSync(srcDir, { withFileTypes: true })) {
1060
+ const srcPath = path4.join(srcDir, entry.name);
1061
+ const destPath = path4.join(destDir, entry.name);
680
1062
  const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
681
1063
  if (entry.isDirectory()) {
682
1064
  copyScaffoldDirAlways(srcPath, destPath, label);
683
1065
  } else if (entry.isFile()) {
684
- const existed = fs2.existsSync(destPath);
1066
+ const existed = fs4.existsSync(destPath);
685
1067
  if (DRY_RUN) {
686
1068
  dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
687
1069
  continue;
688
1070
  }
689
- fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
690
- fs2.copyFileSync(srcPath, destPath);
1071
+ fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1072
+ fs4.copyFileSync(srcPath, destPath);
691
1073
  try {
692
- const srcMode = fs2.statSync(srcPath).mode;
693
- fs2.chmodSync(destPath, srcMode);
1074
+ const srcMode = fs4.statSync(srcPath).mode;
1075
+ fs4.chmodSync(destPath, srcMode);
694
1076
  } catch {
695
1077
  }
696
1078
  ok(`${existed ? "refreshed" : "wrote"} ${label}`);
697
1079
  }
698
1080
  }
699
1081
  }
700
- function scaffoldMigrateSafety(targetDir) {
701
- const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
702
- if (!fs2.existsSync(scaffoldsRoot)) {
1082
+ function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
1083
+ const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
1084
+ if (!fs4.existsSync(scaffoldsRoot)) {
703
1085
  info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
704
1086
  return;
705
1087
  }
706
1088
  const files = [
707
1089
  {
708
- src: path2.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
709
- dest: path2.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
1090
+ src: path4.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
1091
+ dest: path4.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
710
1092
  label: ".github/workflows/backup-on-migration.yml"
711
1093
  },
712
1094
  {
713
- src: path2.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
714
- dest: path2.join(targetDir, "scripts", "migrate-with-backup.sh"),
1095
+ src: path4.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
1096
+ dest: path4.join(targetDir, "scripts", "migrate-with-backup.sh"),
715
1097
  label: "scripts/migrate-with-backup.sh"
716
1098
  },
717
1099
  {
718
- src: path2.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
719
- dest: path2.join(targetDir, "docs", "migrations-runbook.md"),
1100
+ src: path4.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
1101
+ dest: path4.join(targetDir, "docs", "migrations-runbook.md"),
720
1102
  label: "docs/migrations-runbook.md"
721
1103
  }
722
1104
  ];
723
- info("scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook) \u2026");
724
- for (const f of files) copyScaffoldIfMissing(f.src, f.dest, f.label);
1105
+ info(`scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook)${refreshScaffolds ? " \u2014 --refresh-scaffolds active" : ""} \u2026`);
1106
+ for (const f of files) copyScaffoldDriftAware(f.src, f.dest, f.label, refreshScaffolds);
725
1107
  ensureGitignoreLine(targetDir, ".backups/");
726
1108
  ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
727
1109
  }
728
- var MARKETPLACE_ID = "launchsecure";
729
- var LS_PLUGIN_ID = "ls";
1110
+ function hashFile(p) {
1111
+ try {
1112
+ return crypto.createHash("sha256").update(fs4.readFileSync(p)).digest("hex");
1113
+ } catch {
1114
+ return null;
1115
+ }
1116
+ }
1117
+ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1118
+ if (!fs4.existsSync(srcPath)) {
1119
+ info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
1120
+ return "missing-src";
1121
+ }
1122
+ if (!fs4.existsSync(destPath)) {
1123
+ if (DRY_RUN) {
1124
+ dryNote(`would write ${label}`);
1125
+ return "wrote";
1126
+ }
1127
+ fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1128
+ fs4.copyFileSync(srcPath, destPath);
1129
+ try {
1130
+ fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
1131
+ } catch {
1132
+ }
1133
+ ok(`wrote ${label}`);
1134
+ return "wrote";
1135
+ }
1136
+ const srcHash = hashFile(srcPath);
1137
+ const destHash = hashFile(destPath);
1138
+ if (srcHash && destHash && srcHash === destHash) {
1139
+ if (DRY_RUN) dryNote(`${label} in sync with shipped scaffold \u2014 no change`);
1140
+ else info(`${label} in sync with shipped scaffold \u2014 no change`);
1141
+ return "in-sync";
1142
+ }
1143
+ if (refreshScaffolds) {
1144
+ if (DRY_RUN) {
1145
+ dryNote(`would refresh ${label} (overrides local edits)`);
1146
+ return "drifted-refreshed";
1147
+ }
1148
+ fs4.copyFileSync(srcPath, destPath);
1149
+ try {
1150
+ fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
1151
+ } catch {
1152
+ }
1153
+ ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
1154
+ return "drifted-refreshed";
1155
+ }
1156
+ info(`${label} differs from shipped scaffold (customized or older version) \u2014 preserving. Pass --refresh-scaffolds to overwrite.`);
1157
+ return "drifted-preserved";
1158
+ }
1159
+ var MARKETPLACE_ID = "launch-secure";
1160
+ var PLUGIN_ID = "kit";
1161
+ function isDogfoodMarketplace(targetDir) {
1162
+ const p = path4.join(targetDir, ".claude", "settings.json");
1163
+ if (!fs4.existsSync(p)) return { isDogfood: false };
1164
+ try {
1165
+ const settings = JSON.parse(fs4.readFileSync(p, "utf-8"));
1166
+ const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
1167
+ if (existingPath && existingPath !== "./.claude/marketplace") {
1168
+ return { isDogfood: true, existingPath };
1169
+ }
1170
+ return { isDogfood: false };
1171
+ } catch {
1172
+ return { isDogfood: false };
1173
+ }
1174
+ }
730
1175
  function scaffoldLsMarketplace(targetDir) {
731
- const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
732
- if (!fs2.existsSync(scaffoldsRoot)) {
1176
+ const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
1177
+ if (!fs4.existsSync(scaffoldsRoot)) {
733
1178
  info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
734
1179
  return;
735
1180
  }
736
- const marketplaceRoot = path2.join(targetDir, ".claude", "marketplace");
737
- info("scaffolding ls marketplace (Claude Code /ls: namespace \u2014 refreshes every /ls:* command found in the scaffold) \u2026");
1181
+ const dogfood = isDogfoodMarketplace(targetDir);
1182
+ if (dogfood.isDogfood) {
1183
+ info(`dogfood marketplace pointer detected (${dogfood.existingPath}) \u2014 skipping copy, only refreshing enabledPlugins`);
1184
+ wireLsSettings(targetDir);
1185
+ ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
1186
+ return;
1187
+ }
1188
+ const marketplaceRoot = path4.join(targetDir, ".claude", "marketplace");
1189
+ info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
738
1190
  copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
739
1191
  wireLsSettings(targetDir);
740
- ok(`ls marketplace ready \u2014 open this repo in Claude Code, approve the "${MARKETPLACE_ID}" marketplace prompt, then try /ls:activate-beacon, /ls:standup, or /ls:show-mcp-status`);
1192
+ ok(`launch-secure marketplace ready \u2014 open this repo in Claude Code, approve the "${MARKETPLACE_ID}" marketplace prompt, then try /kit:activate-beacon, /kit:standup, or /kit:show-mcp-status`);
741
1193
  }
742
1194
  function wireLsSettings(targetDir) {
743
- const p = path2.join(targetDir, ".claude", "settings.json");
744
- const hadExisting = fs2.existsSync(p);
1195
+ const p = path4.join(targetDir, ".claude", "settings.json");
1196
+ const hadExisting = fs4.existsSync(p);
745
1197
  let existing = {};
746
1198
  if (hadExisting) {
747
1199
  try {
748
- existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
1200
+ existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
749
1201
  } catch (err) {
750
1202
  fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
751
1203
  }
752
1204
  }
753
1205
  const merged = { ...existing };
1206
+ const existingMarketplacePath = existing.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
1207
+ const targetPath = existingMarketplacePath ?? "./.claude/marketplace";
1208
+ if (existingMarketplacePath && existingMarketplacePath !== "./.claude/marketplace") {
1209
+ info(`preserving existing marketplace path: ${existingMarketplacePath} (likely dogfood \u2014 leaving alone)`);
1210
+ }
754
1211
  merged.extraKnownMarketplaces = {
755
1212
  ...existing.extraKnownMarketplaces ?? {},
756
1213
  [MARKETPLACE_ID]: {
757
- source: { source: "directory", path: "./.claude/marketplace" }
1214
+ source: { source: "directory", path: targetPath }
758
1215
  }
759
1216
  };
1217
+ const pluginKey = `${PLUGIN_ID}@${MARKETPLACE_ID}`;
1218
+ const existingEnabledPlugins = existing.enabledPlugins ?? {};
760
1219
  merged.enabledPlugins = {
761
- ...existing.enabledPlugins ?? {},
762
- [`${LS_PLUGIN_ID}@${MARKETPLACE_ID}`]: true
1220
+ ...existingEnabledPlugins,
1221
+ ...pluginKey in existingEnabledPlugins ? {} : { [pluginKey]: true }
763
1222
  };
1223
+ if (pluginKey in existingEnabledPlugins && existingEnabledPlugins[pluginKey] === false) {
1224
+ info(`enabledPlugins["${pluginKey}"] is explicitly false \u2014 leaving disabled (re-enable manually to use /kit:* commands)`);
1225
+ }
764
1226
  if (DRY_RUN) {
765
- dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
1227
+ dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
766
1228
  return;
767
1229
  }
768
- fs2.mkdirSync(path2.dirname(p), { recursive: true });
769
- fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
770
- ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID})`);
1230
+ fs4.mkdirSync(path4.dirname(p), { recursive: true });
1231
+ fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1232
+ ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
771
1233
  }
772
1234
  var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
773
1235
  var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
774
1236
  function scaffoldRecallHook(targetDir) {
775
- const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
776
- if (!fs2.existsSync(scaffoldsRoot)) {
1237
+ const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
1238
+ if (!fs4.existsSync(scaffoldsRoot)) {
777
1239
  info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
778
1240
  return;
779
1241
  }
@@ -783,12 +1245,12 @@ function scaffoldRecallHook(targetDir) {
783
1245
  ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
784
1246
  }
785
1247
  function wireRecallHook(targetDir) {
786
- const p = path2.join(targetDir, ".claude", "settings.json");
787
- const hadExisting = fs2.existsSync(p);
1248
+ const p = path4.join(targetDir, ".claude", "settings.json");
1249
+ const hadExisting = fs4.existsSync(p);
788
1250
  let existing = {};
789
1251
  if (hadExisting) {
790
1252
  try {
791
- existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
1253
+ existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
792
1254
  } catch (err) {
793
1255
  fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
794
1256
  }
@@ -816,19 +1278,62 @@ function wireRecallHook(targetDir) {
816
1278
  dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
817
1279
  return;
818
1280
  }
819
- fs2.mkdirSync(path2.dirname(p), { recursive: true });
820
- fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1281
+ fs4.mkdirSync(path4.dirname(p), { recursive: true });
1282
+ fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
821
1283
  ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
822
1284
  }
1285
+ function tryActivateStatusline() {
1286
+ if (DRY_RUN) {
1287
+ dryNote(`would wrap ~/.claude/settings.json statusLine.command with launch-kit's MCP chip wrapper (skips silently if no statusline configured)`);
1288
+ return;
1289
+ }
1290
+ const res = activateStatusline();
1291
+ if (res.ok && res.outcome === "activated") {
1292
+ ok(`statusline wrapped \u2014 MCP chips will render alongside your existing statusline next session`);
1293
+ } else if (res.ok && res.outcome === "refreshed") {
1294
+ info(`statusline already wrapped \u2014 refreshed chip scripts`);
1295
+ }
1296
+ }
823
1297
  async function main() {
1298
+ const subcommand = process.argv[2];
1299
+ if (subcommand === "statusline") {
1300
+ const action = process.argv[3];
1301
+ if (!action || action === "--help" || action === "-h") {
1302
+ console.log("usage: launch-kit statusline activate [--show=recall,chart,deck,council] [--compact]");
1303
+ console.log(" launch-kit statusline deactivate");
1304
+ console.log("");
1305
+ console.log(" --show comma-separated subset of chips. Default: all four.");
1306
+ console.log(" --compact collapse to `mcp N/total` (green if all up, red otherwise).");
1307
+ console.log(" Re-run activate with different flags to change in place.");
1308
+ return;
1309
+ }
1310
+ let showArg;
1311
+ let compactArg = false;
1312
+ for (const a of process.argv.slice(4)) {
1313
+ if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
1314
+ else if (a === "--compact") compactArg = true;
1315
+ else fail(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
1316
+ }
1317
+ const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
1318
+ let res;
1319
+ if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
1320
+ else if (action === "deactivate") res = deactivateStatusline2();
1321
+ else fail(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
1322
+ if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
1323
+ else info(`statusline ${res.outcome} \u2014 ${res.message}`);
1324
+ return;
1325
+ }
824
1326
  const args = parseArgs(process.argv.slice(2));
825
1327
  if (args.help) {
826
- printHelp();
1328
+ if (subcommand === "refresh") printRefreshHelp();
1329
+ else printHelp();
827
1330
  return;
828
1331
  }
829
- const subcommand = process.argv[2];
830
- if (subcommand && subcommand !== "init" && !subcommand.startsWith("--")) {
831
- fail(`Unknown subcommand "${subcommand}". Only "init" is supported. Run with --help for usage.`);
1332
+ if (!subcommand || subcommand.startsWith("--")) {
1333
+ fail(`missing subcommand. Usage: launch-kit <init|refresh|statusline> [options]. Run with --help.`);
1334
+ }
1335
+ if (subcommand !== "init" && subcommand !== "refresh") {
1336
+ fail(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline. Run with --help for usage.`);
832
1337
  }
833
1338
  DRY_RUN = args.dryRun;
834
1339
  if (DRY_RUN) {
@@ -837,6 +1342,99 @@ async function main() {
837
1342
  info("Lines tagged (dry-run) show what would happen.");
838
1343
  info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
839
1344
  }
1345
+ if (subcommand === "refresh") return mainRefresh(args);
1346
+ return mainInit(args);
1347
+ }
1348
+ async function mainRefresh(args) {
1349
+ const cwd = process.cwd();
1350
+ const targetDir = path4.resolve(args.targetDir ?? cwd);
1351
+ if (!fs4.existsSync(targetDir)) fail(`target dir does not exist: ${targetDir}`);
1352
+ let cred;
1353
+ let source;
1354
+ try {
1355
+ const recovery = recoverCred(targetDir, getRecoveryOptions());
1356
+ cred = recovery.cred;
1357
+ source = recovery.source;
1358
+ } catch (err) {
1359
+ fail(err instanceof Error ? err.message : String(err));
1360
+ }
1361
+ if (cred && source === "mcp") {
1362
+ info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
1363
+ const courseName = inferCourseName(cred.serverUrl);
1364
+ const nested2 = upsertProfile(null, courseName, cred);
1365
+ if (DRY_RUN) {
1366
+ dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
1367
+ } else {
1368
+ writeJsonAtomic(path4.join(targetDir, CONFIG_FILENAME), nested2, 384);
1369
+ ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
1370
+ }
1371
+ }
1372
+ if (!cred) {
1373
+ fail(
1374
+ `no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json. Refresh requires an existing cred or a hardcoded launch-secure MCP entry \u2014 run \`npx @launchsecure/launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path4.relative(cwd, targetDir) || "."}\` first.`
1375
+ );
1376
+ }
1377
+ const nested = toNested(cred);
1378
+ if (!nested) fail(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
1379
+ const active = nested.profiles[nested.active];
1380
+ if (!active) fail(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
1381
+ info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
1382
+ const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
1383
+ mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
1384
+ ensureGitignoreLine(targetDir, CONFIG_FILENAME);
1385
+ if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
1386
+ if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
1387
+ if (!args.noRecallHook) scaffoldRecallHook(targetDir);
1388
+ tryActivateStatusline();
1389
+ console.log("");
1390
+ if (DRY_RUN) {
1391
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1392
+ info(`DRY RUN COMPLETE \u2014 refresh would have applied the above; no files modified.`);
1393
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1394
+ return;
1395
+ }
1396
+ ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
1397
+ if (!args.quiet) {
1398
+ console.log(`
1399
+ Skipped (refresh never runs these): clone, dependency install, onboard script, launch-recall init. Use \`npx @launchsecure/launch-kit init\` for a full bootstrap.
1400
+ ${getLaunchKitToolsGuide()}`);
1401
+ }
1402
+ }
1403
+ async function mainInit(args) {
1404
+ const probeDir = path4.resolve(args.targetDir ?? process.cwd());
1405
+ if (!args.force && fs4.existsSync(probeDir)) {
1406
+ const detection = detectExistingBootstrap(probeDir);
1407
+ if (detection.bootstrapped) {
1408
+ info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
1409
+ info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
1410
+ return mainRefresh({ ...args, targetDir: probeDir });
1411
+ }
1412
+ }
1413
+ if (!args.token || !args.orgSlug || !args.projectSlug) {
1414
+ const recoveryDir = path4.resolve(args.targetDir ?? process.cwd());
1415
+ if (fs4.existsSync(recoveryDir)) {
1416
+ const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
1417
+ const nested = cred ? toNested(cred) : null;
1418
+ const recovered = nested ? nested.profiles[nested.active] : cred;
1419
+ if (recovered) {
1420
+ if (!args.token && recovered.pat) {
1421
+ args.token = recovered.pat;
1422
+ info(`recovered --token from existing config in ${recoveryDir}`);
1423
+ }
1424
+ if (!args.orgSlug && recovered.orgSlug) {
1425
+ args.orgSlug = recovered.orgSlug;
1426
+ info(`recovered --org=${recovered.orgSlug} from existing config`);
1427
+ }
1428
+ if (!args.projectSlug && recovered.projectSlug) {
1429
+ args.projectSlug = recovered.projectSlug;
1430
+ info(`recovered --project=${recovered.projectSlug} from existing config`);
1431
+ }
1432
+ if (args.serverUrl === DEFAULT_SERVER_URL && recovered.serverUrl) {
1433
+ args.serverUrl = recovered.serverUrl;
1434
+ }
1435
+ }
1436
+ }
1437
+ }
840
1438
  if (!args.token) {
841
1439
  const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
842
1440
  args.token = t || null;
@@ -861,10 +1459,10 @@ async function main() {
861
1459
  }
862
1460
  const repoUrl = resolved.repositoryUrl;
863
1461
  const cwd = process.cwd();
864
- const targetDir = path2.resolve(args.targetDir ?? path2.join(cwd, resolved.projectSlug));
1462
+ const targetDir = path4.resolve(args.targetDir ?? path4.join(cwd, resolved.projectSlug));
865
1463
  const normalizedRemote = normalizeRepoUrl(repoUrl);
866
1464
  let skipClone = false;
867
- if (fs2.existsSync(targetDir)) {
1465
+ if (fs4.existsSync(targetDir)) {
868
1466
  if (isGitRepo(targetDir)) {
869
1467
  const existingRemote = gitRemoteUrl(targetDir);
870
1468
  if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
@@ -897,14 +1495,15 @@ async function main() {
897
1495
  installSkippedReason = "no package.json found";
898
1496
  } else {
899
1497
  runInstall(targetDir, detected);
900
- if (hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
1498
+ if (!args.noOnboard && hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
901
1499
  }
902
1500
  const hasOnboard = hasOnboardScript(targetDir);
903
1501
  if (!args.noRecall) runRecallInit(targetDir);
904
- if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
1502
+ if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
905
1503
  if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
906
1504
  if (!args.noRecallHook) scaffoldRecallHook(targetDir);
907
- const relTarget = path2.relative(cwd, targetDir) || ".";
1505
+ tryActivateStatusline();
1506
+ const relTarget = path4.relative(cwd, targetDir) || ".";
908
1507
  console.log("");
909
1508
  if (DRY_RUN) {
910
1509
  info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -917,20 +1516,20 @@ async function main() {
917
1516
  ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
918
1517
  if (installSkippedReason) {
919
1518
  const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
920
- const onboardLine = hasOnboard && detected ? `
1519
+ const onboardLine = hasOnboard && detected && !args.noOnboard ? `
921
1520
  ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
922
1521
  console.log(`
923
1522
  Next steps (install skipped: ${installSkippedReason}):
924
1523
  cd ${relTarget}
925
1524
  ${installLine}${onboardLine}
926
- claude # launch Claude Code (5 MCPs wired)
927
- ${LAUNCH_KIT_TOOLS_GUIDE}`);
928
- } else {
1525
+ claude # launch Claude Code (5 MCPs wired)${args.quiet ? "" : `
1526
+ ${getLaunchKitToolsGuide()}`}`);
1527
+ } else if (!args.quiet) {
929
1528
  console.log(`
930
1529
  Next steps:
931
1530
  cd ${relTarget}
932
1531
  claude # launch Claude Code (5 MCPs wired)
933
- ${LAUNCH_KIT_TOOLS_GUIDE}`);
1532
+ ${getLaunchKitToolsGuide()}`);
934
1533
  }
935
1534
  }
936
1535
  main().catch((err) => {