@launchsecure/launch-kit 0.0.31 → 0.0.33

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 (95) hide show
  1. package/dist/beacon/beacon.mjs +550 -521
  2. package/dist/beacon/beacon.mjs.map +1 -1
  3. package/dist/beacon/beacon.umd.js +9 -9
  4. package/dist/beacon/beacon.umd.js.map +1 -1
  5. package/dist/beacon/types/internal/pick-mode-overlay.d.ts.map +1 -1
  6. package/dist/beacon/types/internal/picker.d.ts.map +1 -1
  7. package/dist/beacon/types/internal/pin-popover.d.ts.map +1 -1
  8. package/dist/beacon/types/internal/selector.d.ts.map +1 -1
  9. package/dist/chart-client/assets/{index-B__ARB8k.js → index-DFu2xIrM.js} +2 -2
  10. package/dist/chart-client/assets/index-DpKO9p0s.css +1 -0
  11. package/dist/chart-client/index.html +2 -2
  12. package/dist/client/assets/{index-h8kMzVtG.js → index-Cbw6bVdx.js} +2 -2
  13. package/dist/client/assets/index-Dv6dD2zY.css +32 -0
  14. package/dist/client/index.html +2 -2
  15. package/dist/council-client/assets/index-AqQ9Sei6.css +1 -0
  16. package/dist/council-client/assets/{index-CWaDcsFR.js → index-CAsmGTzg.js} +2 -2
  17. package/dist/council-client/index.html +2 -2
  18. package/dist/deck-client/assets/{_baseUniq-DdHaBFYO.js → _baseUniq-BiVx0WO_.js} +1 -1
  19. package/dist/deck-client/assets/{arc-D98e_18X.js → arc-DGMkiEzS.js} +1 -1
  20. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-DNFZzh-4.js → architectureDiagram-Q4EWVU46-Y2WRmHtk.js} +1 -1
  21. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-DeQvGUdX.js → blockDiagram-DXYQGD6D-_Lbfu5BQ.js} +1 -1
  22. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-B6ekZf1n.js → c4Diagram-AHTNJAMY-CTqpYTBX.js} +1 -1
  23. package/dist/deck-client/assets/channel-DB6LxW_l.js +1 -0
  24. package/dist/deck-client/assets/{chunk-4BX2VUAB-9aDWymq2.js → chunk-4BX2VUAB-liEIbPHs.js} +1 -1
  25. package/dist/deck-client/assets/{chunk-4TB4RGXK-DtKQqaI7.js → chunk-4TB4RGXK-CCc6lYvL.js} +1 -1
  26. package/dist/deck-client/assets/{chunk-55IACEB6-COy9hEae.js → chunk-55IACEB6-D02jJUR2.js} +1 -1
  27. package/dist/deck-client/assets/{chunk-EDXVE4YY-D_f861An.js → chunk-EDXVE4YY-BFmGMbLD.js} +1 -1
  28. package/dist/deck-client/assets/{chunk-FMBD7UC4-CmuA5UKn.js → chunk-FMBD7UC4-6wFLOVcJ.js} +1 -1
  29. package/dist/deck-client/assets/{chunk-OYMX7WX6-vT8z8D-0.js → chunk-OYMX7WX6-Bnr8RiBf.js} +1 -1
  30. package/dist/deck-client/assets/{chunk-QZHKN3VN-CTlwwg-R.js → chunk-QZHKN3VN-Ct82MksJ.js} +1 -1
  31. package/dist/deck-client/assets/{chunk-YZCP3GAM-C44yr620.js → chunk-YZCP3GAM-BXmN1diQ.js} +1 -1
  32. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +1 -0
  33. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +1 -0
  34. package/dist/deck-client/assets/clone-DiIRH1pI.js +1 -0
  35. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-DBB2J2nL.js → cose-bilkent-S5V4N54A-CmQCT-mH.js} +1 -1
  36. package/dist/deck-client/assets/{dagre-KV5264BT-DxDTYbKl.js → dagre-KV5264BT-DDdSa9EX.js} +1 -1
  37. package/dist/deck-client/assets/{diagram-5BDNPKRD-DByWrWd1.js → diagram-5BDNPKRD-Bccks2xJ.js} +1 -1
  38. package/dist/deck-client/assets/{diagram-G4DWMVQ6-B8B6ddMq.js → diagram-G4DWMVQ6-CPPNgxmQ.js} +1 -1
  39. package/dist/deck-client/assets/{diagram-MMDJMWI5-BMUZ2PWK.js → diagram-MMDJMWI5-KrD300pS.js} +1 -1
  40. package/dist/deck-client/assets/{diagram-TYMM5635-Bk9e8BB-.js → diagram-TYMM5635-DefnLuQf.js} +1 -1
  41. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DcOSwSol.js → erDiagram-SMLLAGMA-DI9FfnFP.js} +1 -1
  42. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-DI-4BR0F.js → flowDiagram-DWJPFMVM-twKyd3Fx.js} +1 -1
  43. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-BeZuXBoU.js → ganttDiagram-T4ZO3ILL-Wau3jhBr.js} +1 -1
  44. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-Bcki__f-.js → gitGraphDiagram-UUTBAWPF-D9GgYXwb.js} +1 -1
  45. package/dist/deck-client/assets/{graph-CifKx6G1.js → graph-BhNLzyXS.js} +1 -1
  46. package/dist/deck-client/assets/index-B-YQq5b5.css +1 -0
  47. package/dist/deck-client/assets/{index-CB-qlwRT.js → index-BtQBaQ7s.js} +76 -76
  48. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-CReN1nFN.js → infoDiagram-42DDH7IO-TylGlSG-.js} +1 -1
  49. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-CDF_VLN_.js → ishikawaDiagram-UXIWVN3A-DAT8icpg.js} +1 -1
  50. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DwgGrNVB.js → journeyDiagram-VCZTEJTY-D3v_XL72.js} +1 -1
  51. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DB_zohh5.js → kanban-definition-6JOO6SKY-DNUOBiNr.js} +1 -1
  52. package/dist/deck-client/assets/{layout-DFfX1O3z.js → layout-COfodgwF.js} +1 -1
  53. package/dist/deck-client/assets/{linear-CtKb4EXj.js → linear-DmTsuIvK.js} +1 -1
  54. package/dist/deck-client/assets/{min-DCRRwUZv.js → min-BW1F7i1D.js} +1 -1
  55. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-D0QBOiFe.js → mindmap-definition-QFDTVHPH-CErFzKWl.js} +1 -1
  56. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-CD-EV5WB.js → pieDiagram-DEJITSTG-DW5F757o.js} +1 -1
  57. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B-JXZ8xI.js → quadrantDiagram-34T5L4WZ-B1S2-TfI.js} +1 -1
  58. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-D2_OK5Dp.js → requirementDiagram-MS252O5E-BY5BAR-5.js} +1 -1
  59. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BbBJqVSC.js → sankeyDiagram-XADWPNL6-CE1Cp9HS.js} +1 -1
  60. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-Db8A-Rkk.js → sequenceDiagram-FGHM5R23-IaHnbKye.js} +1 -1
  61. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-DGJnanjS.js → stateDiagram-FHFEXIEX-CwPJm9hU.js} +1 -1
  62. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +1 -0
  63. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-BRkr6T4w.js → timeline-definition-GMOUNBTQ-DVFGGSgN.js} +1 -1
  64. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-d0rsTqFo.js → vennDiagram-DHZGUBPP-C1194MJi.js} +1 -1
  65. package/dist/deck-client/assets/{wardley-RL74JXVD-2t7cMqdS.js → wardley-RL74JXVD-CHZiUbBa.js} +1 -1
  66. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-DzboAsHh.js → wardleyDiagram-NUSXRM2D-hpwdFfGj.js} +1 -1
  67. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CgTP9u2V.js → xychartDiagram-5P7HB3ND-DYkotwy8.js} +1 -1
  68. package/dist/deck-client/index.html +2 -2
  69. package/dist/server/cli.js +91 -13
  70. package/dist/server/council-entry.js +0 -0
  71. package/dist/server/deck-mcp-entry.js +83 -40
  72. package/dist/server/deck-serve.js +54 -20
  73. package/dist/server/fb-wizard.js +0 -0
  74. package/dist/server/init-entry.js +952 -290
  75. package/dist/server/radar-docker-init-entry.js +239 -0
  76. package/dist/server/radar-entrypoint-entry.js +99 -0
  77. package/dist/server/radar-teardown-entry.js +477 -0
  78. package/dist/server/recall-entry.js +4 -1
  79. package/package.json +22 -23
  80. package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
  81. package/scaffolds/ls-marketplace/plugins/kit/skills/diagram/SKILL.md +27 -5
  82. package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +274 -0
  83. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  84. package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
  85. package/scaffolds/statusline/statusline-mcp.sh +82 -2
  86. package/scaffolds/statusline/statusline-wrapper.sh +8 -1
  87. package/dist/chart-client/assets/index-CDIhdgWg.css +0 -1
  88. package/dist/client/assets/index-CfW4n40I.css +0 -32
  89. package/dist/council-client/assets/index-CZim6x1u.css +0 -1
  90. package/dist/deck-client/assets/channel-DmR7Tyyt.js +0 -1
  91. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bl4ozQWs.js +0 -1
  92. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bl4ozQWs.js +0 -1
  93. package/dist/deck-client/assets/clone-BAy58j24.js +0 -1
  94. package/dist/deck-client/assets/index-BlTlhxFW.css +0 -1
  95. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CR7riiab.js +0 -1
@@ -30,6 +30,217 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  mod
31
31
  ));
32
32
 
33
+ // src/server/cred-shape.ts
34
+ function inferCourseName(serverUrl) {
35
+ try {
36
+ const host = new URL(serverUrl).hostname.toLowerCase();
37
+ if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
38
+ if (host.includes("staging")) return "staging";
39
+ if (host.endsWith(".vercel.app")) return "prod";
40
+ return host.split(".")[0] || "default";
41
+ } catch {
42
+ return "default";
43
+ }
44
+ }
45
+ function toNested(cred) {
46
+ if (cred.profiles && cred.active && cred.profiles[cred.active]) {
47
+ return { active: cred.active, profiles: cred.profiles };
48
+ }
49
+ if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
50
+ return null;
51
+ }
52
+ const name = inferCourseName(cred.serverUrl);
53
+ return {
54
+ active: name,
55
+ profiles: {
56
+ [name]: {
57
+ pat: cred.pat,
58
+ orgSlug: cred.orgSlug,
59
+ projectSlug: cred.projectSlug,
60
+ serverUrl: cred.serverUrl
61
+ }
62
+ }
63
+ };
64
+ }
65
+ function upsertProfile(existing, name, profile) {
66
+ const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
67
+ return {
68
+ active: name,
69
+ profiles: { ...base.profiles, [name]: profile }
70
+ };
71
+ }
72
+ function readCredFile(repoRoot) {
73
+ const p = path.join(repoRoot, CONFIG_FILENAME);
74
+ if (!fs.existsSync(p)) return null;
75
+ try {
76
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
77
+ } catch (err) {
78
+ throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
79
+ }
80
+ }
81
+ function writeJsonAtomic(absPath, value, mode) {
82
+ const tmp = `${absPath}.tmp`;
83
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
84
+ if (mode !== void 0) {
85
+ try {
86
+ fs.chmodSync(tmp, mode);
87
+ } catch {
88
+ }
89
+ }
90
+ fs.renameSync(tmp, absPath);
91
+ }
92
+ var fs, path, CONFIG_FILENAME;
93
+ var init_cred_shape = __esm({
94
+ "src/server/cred-shape.ts"() {
95
+ "use strict";
96
+ fs = __toESM(require("node:fs"));
97
+ path = __toESM(require("node:path"));
98
+ CONFIG_FILENAME = ".launch-secure.cred.config";
99
+ }
100
+ });
101
+
102
+ // src/server/secrets-pull.ts
103
+ var secrets_pull_exports = {};
104
+ __export(secrets_pull_exports, {
105
+ runSecretsPull: () => runSecretsPull
106
+ });
107
+ async function runSecretsPull(opts) {
108
+ const cred = readCredFile(opts.targetDir);
109
+ if (!cred) {
110
+ throw new Error(
111
+ `No ${CONFIG_FILENAME} found in ${opts.targetDir}. Run \`launch-kit init\` first.`
112
+ );
113
+ }
114
+ const nested = toNested(cred);
115
+ if (!nested) {
116
+ throw new Error(`${CONFIG_FILENAME} is missing required fields (pat, orgSlug, projectSlug, serverUrl).`);
117
+ }
118
+ const profile = nested.profiles[nested.active];
119
+ if (!profile) {
120
+ throw new Error(`Active profile "${nested.active}" not found in ${CONFIG_FILENAME}.`);
121
+ }
122
+ const envsRaw = await callMcp(profile, "environments_list", {});
123
+ const envsResult = Array.isArray(envsRaw) ? { environments: envsRaw, defaultPullEnvSlug: null } : envsRaw;
124
+ const envSlug = resolveEnvSlug(opts.envOverride, envsResult);
125
+ if (!envSlug) {
126
+ const list = envsResult.environments.map((e) => e.slug).join(", ") || "(none)";
127
+ throw new Error(
128
+ `No env specified and no default set. Available: ${list}. Pass --env=<slug>, set $LS_ENV, or set a project-level default at ${profile.serverUrl}/${profile.orgSlug}/projects/${profile.projectSlug}/settings.`
129
+ );
130
+ }
131
+ const pullResult = await callMcp(profile, "secrets_pull_env", { env_slug: envSlug });
132
+ const filePath = path3.join(opts.targetDir, opts.fileName);
133
+ writeEnvFile(filePath, pullResult.vars);
134
+ console.log(
135
+ `\u2714 wrote ${pullResult.vars.length} secrets from "${pullResult.env.slug}" \u2192 ${path3.relative(opts.targetDir, filePath) || opts.fileName}`
136
+ );
137
+ }
138
+ function resolveEnvSlug(override, envs) {
139
+ if (override) return override;
140
+ if (process.env.LS_ENV) return process.env.LS_ENV;
141
+ if (envs.defaultPullEnvSlug) return envs.defaultPullEnvSlug;
142
+ if (envs.environments.length === 1) return envs.environments[0].slug;
143
+ return null;
144
+ }
145
+ function writeEnvFile(filePath, vars) {
146
+ const lines = vars.map((v) => `${v.key}=${formatValue(v.value)}`);
147
+ fs3.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
148
+ try {
149
+ fs3.chmodSync(filePath, 384);
150
+ } catch {
151
+ }
152
+ }
153
+ function formatValue(v) {
154
+ if (v === "") return "";
155
+ if (/[\s"'\\$`#=]/.test(v)) {
156
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
157
+ }
158
+ return v;
159
+ }
160
+ async function callMcp(profile, toolName, args) {
161
+ return new Promise((resolve3, reject) => {
162
+ const mcpUrl = new import_node_url.URL("/api/mcp/project", profile.serverUrl);
163
+ const body = JSON.stringify({
164
+ jsonrpc: "2.0",
165
+ id: 1,
166
+ method: "tools/call",
167
+ params: { name: toolName, arguments: args }
168
+ });
169
+ const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
170
+ const req = requester(
171
+ {
172
+ host: mcpUrl.hostname,
173
+ port: mcpUrl.port || (mcpUrl.protocol === "https:" ? 443 : 80),
174
+ path: mcpUrl.pathname,
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ "Accept": "application/json, text/event-stream",
179
+ "Content-Length": String(Buffer.byteLength(body)),
180
+ "Authorization": `Bearer ${profile.pat}`,
181
+ "X-Org-Slug": profile.orgSlug,
182
+ "X-Project-Slug": profile.projectSlug
183
+ }
184
+ },
185
+ (res) => {
186
+ const chunks = [];
187
+ res.on("data", (c2) => chunks.push(c2));
188
+ res.on("end", () => {
189
+ const text = Buffer.concat(chunks).toString("utf-8");
190
+ if (res.statusCode === 401) {
191
+ return reject(new Error(`PAT rejected (401). Regenerate at ${profile.serverUrl}/settings/tokens.`));
192
+ }
193
+ if (res.statusCode === 403) {
194
+ return reject(new Error(`Access denied (403). PAT lacks the scope required for ${toolName}.`));
195
+ }
196
+ if (res.statusCode && res.statusCode >= 400) {
197
+ return reject(new Error(`LaunchSecure ${res.statusCode}: ${text.slice(0, 300)}`));
198
+ }
199
+ let json = text;
200
+ if (text.startsWith("event:") || text.includes("\ndata: ")) {
201
+ for (const line of text.split("\n")) {
202
+ if (line.startsWith("data: ")) {
203
+ json = line.slice(6);
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ try {
209
+ const parsed = JSON.parse(json);
210
+ if (parsed.error) return reject(new Error(`${toolName}: ${parsed.error.message ?? "unknown"}`));
211
+ const inner = parsed.result?.content?.[0]?.text;
212
+ if (!inner) return reject(new Error(`${toolName} returned no content`));
213
+ if (inner.startsWith("\u2500\u2500 Error")) {
214
+ const firstLine = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("\u2500\u2500"))[0];
215
+ return reject(new Error(`${toolName}: ${firstLine ?? inner}`));
216
+ }
217
+ resolve3(JSON.parse(inner));
218
+ } catch (err) {
219
+ reject(new Error(`Could not parse ${toolName} response: ${err instanceof Error ? err.message : String(err)}`));
220
+ }
221
+ });
222
+ }
223
+ );
224
+ req.setTimeout(TIMEOUT_MS, () => req.destroy(new Error(`${toolName} timed out after ${TIMEOUT_MS / 1e3}s`)));
225
+ req.on("error", reject);
226
+ req.write(body);
227
+ req.end();
228
+ });
229
+ }
230
+ var fs3, path3, import_node_http, import_node_https, import_node_url, TIMEOUT_MS;
231
+ var init_secrets_pull = __esm({
232
+ "src/server/secrets-pull.ts"() {
233
+ "use strict";
234
+ fs3 = __toESM(require("node:fs"));
235
+ path3 = __toESM(require("node:path"));
236
+ import_node_http = require("node:http");
237
+ import_node_https = require("node:https");
238
+ import_node_url = require("node:url");
239
+ init_cred_shape();
240
+ TIMEOUT_MS = 15e3;
241
+ }
242
+ });
243
+
33
244
  // src/server/statusline-install.ts
34
245
  var statusline_install_exports = {};
35
246
  __export(statusline_install_exports, {
@@ -37,25 +248,25 @@ __export(statusline_install_exports, {
37
248
  deactivateStatusline: () => deactivateStatusline
38
249
  });
39
250
  function readSettings() {
40
- if (!fs3.existsSync(SETTINGS_PATH)) return null;
251
+ if (!fs4.existsSync(SETTINGS_PATH)) return null;
41
252
  try {
42
- return JSON.parse(fs3.readFileSync(SETTINGS_PATH, "utf-8"));
253
+ return JSON.parse(fs4.readFileSync(SETTINGS_PATH, "utf-8"));
43
254
  } catch {
44
255
  return null;
45
256
  }
46
257
  }
47
258
  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");
259
+ fs4.mkdirSync(path4.dirname(SETTINGS_PATH), { recursive: true });
260
+ fs4.writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
50
261
  }
51
262
  function readScaffold(name) {
52
- const p = path3.resolve(__dirname, "..", "..", "scaffolds", "statusline", name);
53
- return fs3.readFileSync(p, "utf-8");
263
+ const p = path4.resolve(__dirname, "..", "..", "scaffolds", "statusline", name);
264
+ return fs4.readFileSync(p, "utf-8");
54
265
  }
55
266
  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 });
267
+ fs4.mkdirSync(LK_DIR, { recursive: true });
268
+ fs4.writeFileSync(WRAPPER_PATH, readScaffold("statusline-wrapper.sh"), { mode: 493 });
269
+ fs4.writeFileSync(CHIP_PATH, readScaffold("statusline-mcp.sh"), { mode: 493 });
59
270
  }
60
271
  function wrapperCommand(opts) {
61
272
  const env = [];
@@ -115,103 +326,313 @@ function deactivateStatusline() {
115
326
  writeSettings(restored);
116
327
  for (const p of [WRAPPER_PATH, CHIP_PATH]) {
117
328
  try {
118
- fs3.unlinkSync(p);
329
+ fs4.unlinkSync(p);
119
330
  } catch {
120
331
  }
121
332
  }
122
333
  return { ok: true, outcome: "deactivated", message: "restored original statusLine.command" };
123
334
  }
124
- var fs3, path3, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
335
+ var fs4, path4, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
125
336
  var init_statusline_install = __esm({
126
337
  "src/server/statusline-install.ts"() {
127
338
  "use strict";
128
- fs3 = __toESM(require("node:fs"));
129
- path3 = __toESM(require("node:path"));
339
+ fs4 = __toESM(require("node:fs"));
340
+ path4 = __toESM(require("node:path"));
130
341
  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");
342
+ LK_DIR = path4.join((0, import_node_os.homedir)(), ".launchsecure");
343
+ WRAPPER_PATH = path4.join(LK_DIR, "statusline-wrapper.sh");
344
+ CHIP_PATH = path4.join(LK_DIR, "statusline-mcp.sh");
345
+ SETTINGS_PATH = path4.join((0, import_node_os.homedir)(), ".claude", "settings.json");
135
346
  ORIGINAL_KEY = "_launchKitStatuslineOriginal";
136
347
  }
137
348
  });
138
349
 
139
- // src/server/init-entry.ts
140
- var import_node_child_process = require("node:child_process");
141
- var crypto = __toESM(require("node:crypto"));
142
- var fs4 = __toESM(require("node:fs"));
143
- var import_node_http = require("node:http");
144
- var import_node_https = require("node:https");
145
- var path4 = __toESM(require("node:path"));
146
- var readline = __toESM(require("node:readline"));
147
- var import_node_url = require("node:url");
148
-
149
- // src/server/cred-shape.ts
150
- var fs = __toESM(require("node:fs"));
151
- var path = __toESM(require("node:path"));
152
- var CONFIG_FILENAME = ".launch-secure.cred.config";
153
- function inferCourseName(serverUrl) {
350
+ // src/server/radar/mcp.ts
351
+ function parseBody(text) {
352
+ if (!text) return {};
353
+ if (text.startsWith("event:") || text.includes("\ndata: ")) {
354
+ for (const line of text.split("\n")) {
355
+ if (line.startsWith("data: ")) {
356
+ try {
357
+ return JSON.parse(line.slice(6));
358
+ } catch {
359
+ }
360
+ }
361
+ }
362
+ }
154
363
  try {
155
- const host = new URL(serverUrl).hostname.toLowerCase();
156
- if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
157
- if (host.includes("staging")) return "staging";
158
- if (host.endsWith(".vercel.app")) return "prod";
159
- return host.split(".")[0] || "default";
364
+ return JSON.parse(text);
160
365
  } catch {
161
- return "default";
366
+ return {};
162
367
  }
163
368
  }
164
- function toNested(cred) {
165
- if (cred.profiles && cred.active && cred.profiles[cred.active]) {
166
- return { active: cred.active, profiles: cred.profiles };
167
- }
168
- if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
169
- return null;
170
- }
171
- const name = inferCourseName(cred.serverUrl);
172
- return {
173
- active: name,
174
- profiles: {
175
- [name]: {
176
- pat: cred.pat,
177
- orgSlug: cred.orgSlug,
178
- projectSlug: cred.projectSlug,
179
- serverUrl: cred.serverUrl
369
+ var import_node_https2, import_node_http2, ProjectMcpClient;
370
+ var init_mcp = __esm({
371
+ "src/server/radar/mcp.ts"() {
372
+ "use strict";
373
+ import_node_https2 = require("node:https");
374
+ import_node_http2 = require("node:http");
375
+ ProjectMcpClient = class {
376
+ constructor(opts) {
377
+ this.initialized = false;
378
+ this.callId = 1;
379
+ this.mcpUrl = new URL("/api/mcp/project", opts.serverUrl);
380
+ this.headers = {
381
+ "Content-Type": "application/json",
382
+ "Accept": "application/json, text/event-stream",
383
+ "Authorization": `Bearer ${opts.pat}`
384
+ };
385
+ if (opts.orgSlug) this.headers["X-Org-Slug"] = opts.orgSlug;
386
+ if (opts.projectSlug) this.headers["X-Project-Slug"] = opts.projectSlug;
180
387
  }
181
- }
182
- };
388
+ async call(toolName, args) {
389
+ await this.ensureInitialized();
390
+ const body = JSON.stringify({
391
+ jsonrpc: "2.0",
392
+ id: this.callId++,
393
+ method: "tools/call",
394
+ params: { name: toolName, arguments: args }
395
+ });
396
+ const resp = await this.send(body);
397
+ const parsed = parseBody(resp.body);
398
+ if (parsed.error) {
399
+ throw new Error(`MCP ${toolName} failed: ${parsed.error.message ?? JSON.stringify(parsed.error)}`);
400
+ }
401
+ const text = parsed.result?.content?.[0]?.text;
402
+ if (!text) return parsed.result;
403
+ if (text.startsWith("\u2500\u2500 Error")) {
404
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
405
+ const reason = lines.find((l) => !l.startsWith("\u2500\u2500")) ?? text;
406
+ throw new Error(`MCP ${toolName} failed: ${reason}`);
407
+ }
408
+ try {
409
+ return JSON.parse(text);
410
+ } catch {
411
+ return text;
412
+ }
413
+ }
414
+ async ensureInitialized() {
415
+ if (this.initialized) return;
416
+ const initBody = JSON.stringify({
417
+ jsonrpc: "2.0",
418
+ id: this.callId++,
419
+ method: "initialize",
420
+ params: {
421
+ protocolVersion: "2025-03-26",
422
+ capabilities: {},
423
+ clientInfo: { name: "launchpod-feedback-agent", version: "0.0.1" }
424
+ }
425
+ });
426
+ const initResp = await this.send(initBody);
427
+ if (initResp.sessionId) this.sessionId = initResp.sessionId;
428
+ const notifBody = JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" });
429
+ await this.send(notifBody);
430
+ this.initialized = true;
431
+ }
432
+ send(body) {
433
+ return new Promise((resolve3, reject) => {
434
+ const headers = {
435
+ ...this.headers,
436
+ "Content-Length": String(Buffer.byteLength(body))
437
+ };
438
+ if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
439
+ const requester = this.mcpUrl.protocol === "https:" ? import_node_https2.request : import_node_http2.request;
440
+ const req = requester(
441
+ {
442
+ host: this.mcpUrl.hostname,
443
+ port: this.mcpUrl.port || (this.mcpUrl.protocol === "https:" ? 443 : 80),
444
+ path: this.mcpUrl.pathname + this.mcpUrl.search,
445
+ method: "POST",
446
+ headers
447
+ },
448
+ (res) => {
449
+ const chunks = [];
450
+ res.on("data", (c2) => chunks.push(c2));
451
+ res.on("end", () => {
452
+ const text = Buffer.concat(chunks).toString("utf-8");
453
+ if (res.statusCode && res.statusCode >= 400) {
454
+ reject(new Error(`MCP HTTP ${res.statusCode}: ${text}`));
455
+ return;
456
+ }
457
+ const sid = res.headers["mcp-session-id"];
458
+ resolve3({ body: text, sessionId: typeof sid === "string" ? sid : void 0 });
459
+ });
460
+ }
461
+ );
462
+ req.on("error", reject);
463
+ req.write(body);
464
+ req.end();
465
+ });
466
+ }
467
+ };
468
+ }
469
+ });
470
+
471
+ // src/server/radar-docker-init-entry.ts
472
+ var radar_docker_init_entry_exports = {};
473
+ function fail(message) {
474
+ console.error(message);
475
+ process.exit(1);
183
476
  }
184
- function upsertProfile(existing, name, profile) {
185
- const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
186
- return {
187
- active: name,
188
- profiles: { ...base.profiles, [name]: profile }
189
- };
477
+ function requireEnv(name) {
478
+ const v = process.env[name];
479
+ if (!v) fail(`ERROR: ${name} is required but not set`);
480
+ return v;
190
481
  }
191
- function readCredFile(repoRoot) {
192
- const p = path.join(repoRoot, CONFIG_FILENAME);
193
- if (!fs.existsSync(p)) return null;
482
+ function run2(cmd, args, stdio = "inherit") {
483
+ const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
484
+ return r.status ?? 1;
485
+ }
486
+ async function setupFromCloud() {
487
+ const pat = requireEnv("LS_PAT");
488
+ const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
489
+ const orgSlug = process.env.LS_ORG_SLUG;
490
+ const projectSlug = process.env.LS_PROJECT_SLUG;
491
+ const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
492
+ let bundle;
194
493
  try {
195
- return JSON.parse(fs.readFileSync(p, "utf-8"));
494
+ bundle = await mcp.call("radar_bootstrap_get", {});
196
495
  } catch (err) {
197
- throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
496
+ fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and is scoped to the right org/project.`);
198
497
  }
498
+ if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
499
+ if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
500
+ if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
501
+ if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
502
+ if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
503
+ if (!process.env.GH_TOKEN) {
504
+ fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
505
+ }
506
+ console.log(`[entrypoint] bundle from cloud: org=${process.env.LS_ORG_SLUG} project=${process.env.LS_PROJECT_SLUG} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()}`);
199
507
  }
200
- function writeJsonAtomic(absPath, value, mode) {
201
- const tmp = `${absPath}.tmp`;
202
- fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
203
- if (mode !== void 0) {
508
+ function setupClaudeCredentials() {
509
+ const home = process.env.HOME ?? "/home/launchpod";
510
+ const claudeDir = (0, import_node_path.join)(home, ".claude");
511
+ (0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
512
+ const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
513
+ const credsPath = (0, import_node_path.join)(claudeDir, ".credentials.json");
514
+ (0, import_node_fs.writeFileSync)(credsPath, decoded);
515
+ (0, import_node_fs.chmodSync)(credsPath, 384);
516
+ const configPath = (0, import_node_path.join)(home, ".claude.json");
517
+ let cfg = {};
518
+ if ((0, import_node_fs.existsSync)(configPath)) {
204
519
  try {
205
- fs.chmodSync(tmp, mode);
520
+ cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
206
521
  } catch {
522
+ cfg = {};
207
523
  }
208
524
  }
209
- fs.renameSync(tmp, absPath);
525
+ cfg.hasCompletedOnboarding = true;
526
+ cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
527
+ cfg.numStartups = (cfg.numStartups ?? 0) + 1;
528
+ cfg.installMethod = cfg.installMethod ?? "global";
529
+ (0, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
530
+ (0, import_node_fs.chmodSync)(configPath, 384);
531
+ }
532
+ function setupGitAndGh() {
533
+ const name = process.env.GIT_USER_NAME ?? "Radar Bot";
534
+ const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
535
+ const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
536
+ if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
537
+ }
538
+ function initWorkspaceIfEmpty() {
539
+ process.chdir("/workspace");
540
+ if ((0, import_node_fs.existsSync)(".git")) {
541
+ console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
542
+ return;
543
+ }
544
+ console.log("[entrypoint] /workspace is empty \u2014 running launch-kit init");
545
+ const status = run2("launch-kit", [
546
+ "init",
547
+ `--token=${requireEnv("LS_PAT")}`,
548
+ `--org=${requireEnv("LS_ORG_SLUG")}`,
549
+ `--project=${requireEnv("LS_PROJECT_SLUG")}`,
550
+ `--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
551
+ `--dir=/workspace`
552
+ ]);
553
+ if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
554
+ }
555
+ function execLaunchPodRadar() {
556
+ console.log("[entrypoint] starting launch-pod radar");
557
+ const child = (0, import_node_child_process2.spawn)("launch-pod", ["radar"], { stdio: "inherit" });
558
+ const forward = (sig) => () => {
559
+ try {
560
+ child.kill(sig);
561
+ } catch {
562
+ }
563
+ };
564
+ process.on("SIGTERM", forward("SIGTERM"));
565
+ process.on("SIGINT", forward("SIGINT"));
566
+ process.on("SIGHUP", forward("SIGHUP"));
567
+ child.on("exit", (code, signal) => {
568
+ if (signal) process.kill(process.pid, signal);
569
+ else process.exit(code ?? 0);
570
+ });
571
+ }
572
+ async function main() {
573
+ for (const k of REQUIRED_ENV) requireEnv(k);
574
+ await setupFromCloud();
575
+ setupClaudeCredentials();
576
+ setupGitAndGh();
577
+ initWorkspaceIfEmpty();
578
+ execLaunchPodRadar();
579
+ }
580
+ var import_node_child_process2, import_node_fs, import_node_path, REQUIRED_ENV;
581
+ var init_radar_docker_init_entry = __esm({
582
+ "src/server/radar-docker-init-entry.ts"() {
583
+ "use strict";
584
+ import_node_child_process2 = require("node:child_process");
585
+ import_node_fs = require("node:fs");
586
+ import_node_path = require("node:path");
587
+ init_mcp();
588
+ REQUIRED_ENV = [
589
+ "CLAUDE_CREDENTIALS_B64",
590
+ "LS_PAT"
591
+ ];
592
+ main().catch((err) => {
593
+ console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
594
+ process.exit(1);
595
+ });
596
+ }
597
+ });
598
+
599
+ // src/server/init-entry.ts
600
+ var import_node_child_process3 = require("node:child_process");
601
+ var crypto = __toESM(require("node:crypto"));
602
+ var fs5 = __toESM(require("node:fs"));
603
+ var import_node_http3 = require("node:http");
604
+ var import_node_https3 = require("node:https");
605
+ var path5 = __toESM(require("node:path"));
606
+ var readline = __toESM(require("node:readline"));
607
+ var import_node_url2 = require("node:url");
608
+ init_cred_shape();
609
+
610
+ // src/server/git-bot-config.ts
611
+ var import_node_child_process = require("node:child_process");
612
+ function run(cmd, args, stdio = "inherit") {
613
+ return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
614
+ }
615
+ function configureGitForBot(identity) {
616
+ if (process.env.GH_TOKEN) {
617
+ run("gh", ["auth", "setup-git"]);
618
+ }
619
+ run("git", ["config", "--global", "user.name", identity.name]);
620
+ run("git", ["config", "--global", "user.email", identity.email]);
621
+ run("git", ["config", "--global", "init.defaultBranch", "main"]);
622
+ run("git", ["config", "--global", "pull.rebase", "false"]);
623
+ }
624
+ function parseGitIdentityFlag(value, flagName = "--git-identity") {
625
+ const m = value.match(/^\s*(.+?)\s*<\s*([^>]+?)\s*>\s*$/);
626
+ if (!m) {
627
+ throw new Error(`${flagName} must be in the form "Name <email>" (got: ${value})`);
628
+ }
629
+ return { name: m[1], email: m[2] };
210
630
  }
211
631
 
212
632
  // src/server/cred-recovery.ts
213
633
  var fs2 = __toESM(require("node:fs"));
214
634
  var path2 = __toESM(require("node:path"));
635
+ init_cred_shape();
215
636
  var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
216
637
  function migrateLegacyCredFile(targetDir, opts) {
217
638
  const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
@@ -292,9 +713,54 @@ function recoverCred(targetDir, opts) {
292
713
  }
293
714
 
294
715
  // src/server/init-entry.ts
716
+ init_secrets_pull();
295
717
  init_statusline_install();
296
718
  var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
297
719
  var ONBOARD_SCRIPT_NAME = "onboard";
720
+ var TTY = process.stdout.isTTY === true && process.env.NO_COLOR !== "1";
721
+ var c = {
722
+ dim: (s) => TTY ? `\x1B[2m${s}\x1B[0m` : s,
723
+ bold: (s) => TTY ? `\x1B[1m${s}\x1B[0m` : s,
724
+ cyan: (s) => TTY ? `\x1B[36m${s}\x1B[0m` : s,
725
+ green: (s) => TTY ? `\x1B[32m${s}\x1B[0m` : s,
726
+ yellow: (s) => TTY ? `\x1B[33m${s}\x1B[0m` : s,
727
+ red: (s) => TTY ? `\x1B[31m${s}\x1B[0m` : s
728
+ };
729
+ var PHASE_NAME_COL = 16;
730
+ var HEADER_WIDTH = 64;
731
+ function header(title, kv) {
732
+ if (VERBOSE) return;
733
+ const dashes = "\u2500".repeat(Math.max(3, HEADER_WIDTH - title.length - 5));
734
+ console.log("");
735
+ console.log(c.cyan(`\u2500\u2500\u2500 ${c.bold(title)} ${dashes}`));
736
+ if (kv.length > 0) {
737
+ const labelWidth = Math.max(...kv.map(([k]) => k.length));
738
+ for (const [k, v] of kv) console.log(` ${c.dim(k.padEnd(labelWidth))} ${v}`);
739
+ }
740
+ console.log("");
741
+ }
742
+ function phase(name, result) {
743
+ if (VERBOSE) return;
744
+ const mark = result.status === "ok" ? c.green("\u2713") : result.status === "in-sync" ? c.dim("\xB7") : result.status === "skipped" ? c.dim("\u2212") : c.yellow("\u26A0");
745
+ const summary = result.status === "in-sync" || result.status === "skipped" ? c.dim(result.summary) : result.summary;
746
+ const label = name.padEnd(PHASE_NAME_COL);
747
+ console.log(` ${result.status === "warn" ? c.yellow(label) : label} ${mark} ${summary}`);
748
+ }
749
+ function section(title) {
750
+ if (VERBOSE) return;
751
+ console.log("");
752
+ console.log(` ${c.dim("\u2500 " + title + " \u2500")}`);
753
+ }
754
+ function footer(msg, hints = []) {
755
+ if (VERBOSE) return;
756
+ console.log("");
757
+ console.log(` ${c.bold(msg)}`);
758
+ for (const h of hints) console.log(` ${c.dim(h)}`);
759
+ console.log("");
760
+ }
761
+ function warn(msg) {
762
+ console.log(` ${c.yellow("\u26A0")} ${msg}`);
763
+ }
298
764
  var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
299
765
  var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
300
766
  Wired in Claude Code (.mcp.json):
@@ -319,12 +785,12 @@ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
319
785
  `;
320
786
  function listEntries(dir, kind) {
321
787
  if (kind === "commands") {
322
- return fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path4.join(dir, f) }));
788
+ return fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path5.join(dir, f) }));
323
789
  }
324
- return fs4.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs4.existsSync(path4.join(dir, e.name, "SKILL.md"))).map((e) => e.name).sort().map((name) => ({ name, file: path4.join(dir, name, "SKILL.md") }));
790
+ return fs5.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs5.existsSync(path5.join(dir, e.name, "SKILL.md"))).map((e) => e.name).sort().map((name) => ({ name, file: path5.join(dir, name, "SKILL.md") }));
325
791
  }
326
792
  function renderEntries(dir, kind) {
327
- if (!fs4.existsSync(dir)) return `
793
+ if (!fs5.existsSync(dir)) return `
328
794
  LS slash ${kind}: (scaffold dir not bundled \u2014 this is a packaging bug)
329
795
  `;
330
796
  const entries = listEntries(dir, kind);
@@ -334,7 +800,7 @@ LS slash ${kind}: (none defined)
334
800
  const names = entries.map((e) => `/${PLUGIN_ID}:${e.name}`);
335
801
  const colWidth = Math.max(26, ...names.map((n) => n.length + 2));
336
802
  const lines = entries.map((entry, i) => {
337
- const text = fs4.readFileSync(entry.file, "utf-8");
803
+ const text = fs5.readFileSync(entry.file, "utf-8");
338
804
  const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
339
805
  const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
340
806
  return ` ${names[i].padEnd(colWidth)} \u2014 ${desc}`;
@@ -345,8 +811,8 @@ ${lines.join("\n")}
345
811
  `;
346
812
  }
347
813
  function renderLsCommandsSection() {
348
- const base = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
349
- return renderEntries(path4.join(base, "commands"), "commands") + renderEntries(path4.join(base, "skills"), "skills");
814
+ const base = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
815
+ return renderEntries(path5.join(base, "commands"), "commands") + renderEntries(path5.join(base, "skills"), "skills");
350
816
  }
351
817
  function getLaunchKitToolsGuide() {
352
818
  return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
@@ -369,9 +835,12 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
369
835
  "--refresh-scaffolds",
370
836
  "--quiet",
371
837
  "--force",
372
- "--dry-run"
838
+ "--dry-run",
839
+ "--verbose",
840
+ "--guide",
841
+ "--no-guide"
373
842
  ]);
374
- var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
843
+ var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
375
844
  function parseArgs(argv) {
376
845
  const args = {
377
846
  token: process.env.LS_PAT ?? null,
@@ -379,6 +848,7 @@ function parseArgs(argv) {
379
848
  projectSlug: null,
380
849
  serverUrl: DEFAULT_SERVER_URL,
381
850
  targetDir: null,
851
+ gitIdentity: null,
382
852
  course: null,
383
853
  noInstall: false,
384
854
  noOnboard: false,
@@ -390,6 +860,8 @@ function parseArgs(argv) {
390
860
  quiet: false,
391
861
  force: false,
392
862
  dryRun: false,
863
+ verbose: false,
864
+ guide: null,
393
865
  help: false
394
866
  };
395
867
  const unknown = [];
@@ -440,6 +912,18 @@ function parseArgs(argv) {
440
912
  args.dryRun = true;
441
913
  continue;
442
914
  }
915
+ if (raw === "--verbose") {
916
+ args.verbose = true;
917
+ continue;
918
+ }
919
+ if (raw === "--guide") {
920
+ args.guide = true;
921
+ continue;
922
+ }
923
+ if (raw === "--no-guide") {
924
+ args.guide = false;
925
+ continue;
926
+ }
443
927
  const eq = raw.indexOf("=");
444
928
  if (raw.startsWith("--") && eq > 0) {
445
929
  const key = raw.slice(2, eq);
@@ -468,6 +952,14 @@ function parseArgs(argv) {
468
952
  args.course = val;
469
953
  continue;
470
954
  }
955
+ if (key === "git-identity") {
956
+ try {
957
+ args.gitIdentity = parseGitIdentityFlag(val);
958
+ } catch (err) {
959
+ fail2(err instanceof Error ? err.message : String(err));
960
+ }
961
+ continue;
962
+ }
471
963
  unknown.push(raw);
472
964
  continue;
473
965
  }
@@ -480,7 +972,7 @@ function parseArgs(argv) {
480
972
  if (unknown.length > 0) {
481
973
  const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
482
974
  const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
483
- fail(`Unknown argument(s): ${unknown.join(" ")}
975
+ fail2(`Unknown argument(s): ${unknown.join(" ")}
484
976
  Known boolean flags: ${knownBool}
485
977
  Known key=value flags: ${knownKv}`);
486
978
  }
@@ -511,8 +1003,15 @@ Options:
511
1003
  --refresh-scaffolds Force-overwrite migrate-safety files (default is to
512
1004
  preserve user edits). Use this to pull updates
513
1005
  published to @launchsecure/launch-kit.
514
- --quiet Suppress the post-run tools guide.
1006
+ --verbose Print the legacy per-line debug log instead of the
1007
+ default compact phase summary. Useful when refresh
1008
+ reports a warn-status phase and you want detail.
1009
+ --guide Print the trailing tools guide (MCP table + every
1010
+ /kit:* command). Off by default on refresh.
1011
+ --quiet Suppress the tools guide and the next-steps hint.
1012
+ Overrides --guide.
515
1013
  --dry-run Preview every file write without making changes.
1014
+ Implies --verbose.
516
1015
  --help Show this help.
517
1016
 
518
1017
  Tip: prefix the command with @latest (\`launch-kit@latest refresh\`) to force
@@ -526,6 +1025,12 @@ Subcommands:
526
1025
  init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
527
1026
  refresh Re-apply scaffolds + MCP entries in an already-initialized project
528
1027
  (no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
1028
+ setup-git Configure git identity + gh credential helper in one
1029
+ shot. Use in containers / CI where init isn't needed.
1030
+ \`launch-kit setup-git --identity="Name <email>"\`.
1031
+ secrets pull Fetch decrypted secrets from cloud LS and write a .env file.
1032
+ Env resolved from --env \u2192 $LS_ENV \u2192 server-side project default \u2192
1033
+ single-env auto-pick. See \`launch-kit secrets --help\`.
529
1034
  statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
530
1035
  chips (recall, chart, deck, council) get appended. Refuses to
531
1036
  create one if none exists \u2014 additive only.
@@ -550,6 +1055,12 @@ Options:
550
1055
  becomes active; re-run with a different --course
551
1056
  and --url to add another (e.g. local + staging).
552
1057
  Use \`launch-course set <name>\` to switch later.
1058
+ --git-identity="N <e>" Non-interactive git identity for service-account /
1059
+ CI / Docker runs. Configures git user.name, user.email,
1060
+ init.defaultBranch=main, pull.rebase=false; also
1061
+ wires GH_TOKEN into git's credential helper via
1062
+ \`gh auth setup-git\` when GH_TOKEN is set. Example:
1063
+ --git-identity="Radar Bot <radar@launchpod.local>".
553
1064
  --no-install Skip dependency install step (also skips the onboard
554
1065
  script \u2014 install is its prerequisite).
555
1066
  --no-onboard Skip the onboard script even when install runs.
@@ -570,9 +1081,17 @@ Options:
570
1081
  user edits; use this to pull updates published to
571
1082
  @launchsecure/launch-kit (e.g., a newer
572
1083
  migrate-with-backup.sh).
573
- --quiet Suppress the post-run tools guide (the long block
574
- listing /kit:* commands and wired MCPs). Useful for
575
- idempotent re-runs in CI or scripts.
1084
+ --verbose Print the legacy per-line debug log instead of the
1085
+ default compact phase summary. Useful when a phase
1086
+ reports a warn status and you want per-file detail.
1087
+ --guide Print the trailing tools guide (MCP table + every
1088
+ /kit:* command). On by default for init; pair with
1089
+ --no-guide to suppress.
1090
+ --no-guide Suppress the trailing tools guide (handy after the
1091
+ first init, when you don't need the catalog again).
1092
+ --quiet Suppress the post-run tools guide AND the next-steps
1093
+ hint. Useful for idempotent re-runs in CI or scripts.
1094
+ Overrides --guide.
576
1095
  --force Skip the auto-delegate-to-refresh check. By default
577
1096
  init detects an existing bootstrap (cred file +
578
1097
  launch-secure MCP entry) and runs refresh instead.
@@ -619,29 +1138,30 @@ async function prompt(question) {
619
1138
  resolve3(answer.trim());
620
1139
  }));
621
1140
  }
622
- function fail(msg) {
1141
+ function fail2(msg) {
623
1142
  console.error(`[launch-kit] \u2717 ${msg}`);
624
1143
  process.exit(1);
625
1144
  }
1145
+ var VERBOSE = false;
626
1146
  function info(msg) {
627
- console.log(`[launch-kit] ${msg}`);
1147
+ if (VERBOSE) console.log(`[launch-kit] ${msg}`);
628
1148
  }
629
1149
  function ok(msg) {
630
- console.log(`[launch-kit] \u2713 ${msg}`);
1150
+ if (VERBOSE) console.log(`[launch-kit] \u2713 ${msg}`);
631
1151
  }
632
1152
  var DRY_RUN = false;
633
1153
  function dryNote(msg) {
634
1154
  console.log(`[launch-kit] (dry-run) ${msg}`);
635
1155
  }
636
1156
  function which(bin) {
637
- const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
1157
+ const res = (0, import_node_child_process3.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
638
1158
  if (res.status !== 0) return null;
639
1159
  return res.stdout.split(/\r?\n/)[0]?.trim() || null;
640
1160
  }
641
1161
  function preflight() {
642
1162
  const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
643
- if (nodeMajor < 18) fail(`Node.js >= 18 required (current: ${process.versions.node}).`);
644
- if (!which("git")) fail("git not found in PATH. Install git: https://git-scm.com/downloads");
1163
+ if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
1164
+ if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
645
1165
  const hasGh = which("gh") !== null;
646
1166
  ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
647
1167
  return { hasGh };
@@ -657,7 +1177,7 @@ var ProjectInfoHttpError = class extends Error {
657
1177
  };
658
1178
  function attemptProjectInfo(args) {
659
1179
  return new Promise((resolve3, reject) => {
660
- const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
1180
+ const mcpUrl = new import_node_url2.URL("/api/mcp/project", args.serverUrl);
661
1181
  const body = JSON.stringify({
662
1182
  jsonrpc: "2.0",
663
1183
  id: 1,
@@ -667,7 +1187,7 @@ function attemptProjectInfo(args) {
667
1187
  arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
668
1188
  }
669
1189
  });
670
- const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
1190
+ const requester = mcpUrl.protocol === "https:" ? import_node_https3.request : import_node_http3.request;
671
1191
  const req = requester(
672
1192
  {
673
1193
  host: mcpUrl.hostname,
@@ -685,7 +1205,7 @@ function attemptProjectInfo(args) {
685
1205
  },
686
1206
  (res) => {
687
1207
  const chunks = [];
688
- res.on("data", (c) => chunks.push(c));
1208
+ res.on("data", (c2) => chunks.push(c2));
689
1209
  res.on("end", () => {
690
1210
  const text = Buffer.concat(chunks).toString("utf-8");
691
1211
  if (res.statusCode === 401) {
@@ -775,7 +1295,7 @@ async function callProjectInfo(args) {
775
1295
  throw lastErr;
776
1296
  }
777
1297
  function gitRemoteUrl(dir) {
778
- const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
1298
+ const res = (0, import_node_child_process3.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
779
1299
  if (res.status !== 0) return null;
780
1300
  return res.stdout.trim() || null;
781
1301
  }
@@ -784,18 +1304,18 @@ function normalizeRepoUrl(url) {
784
1304
  const sshMatch = u.match(/^git@([^:]+):(.+)$/);
785
1305
  if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
786
1306
  try {
787
- const parsed = new import_node_url.URL(u);
1307
+ const parsed = new import_node_url2.URL(u);
788
1308
  return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
789
1309
  } catch {
790
1310
  return u;
791
1311
  }
792
1312
  }
793
1313
  function isGitRepo(dir) {
794
- return fs4.existsSync(path4.join(dir, ".git"));
1314
+ return fs5.existsSync(path5.join(dir, ".git"));
795
1315
  }
796
1316
  function dirIsEmpty(dir) {
797
- if (!fs4.existsSync(dir)) return true;
798
- return fs4.readdirSync(dir).length === 0;
1317
+ if (!fs5.existsSync(dir)) return true;
1318
+ return fs5.readdirSync(dir).length === 0;
799
1319
  }
800
1320
  function cloneRepo(repoUrl, targetDir, hasGh) {
801
1321
  const isGithub = /github\.com/i.test(repoUrl);
@@ -814,20 +1334,20 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
814
1334
  dryNote(`would run: ${cmd} ${args.join(" ")}`);
815
1335
  return;
816
1336
  }
817
- const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
1337
+ const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
818
1338
  if (res.status !== 0) {
819
- fail(
1339
+ fail2(
820
1340
  `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.`
821
1341
  );
822
1342
  }
823
- if (!fs4.existsSync(path4.join(targetDir, ".git"))) {
824
- fail(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
1343
+ if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
1344
+ fail2(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
825
1345
  }
826
1346
  ok(`cloned to ${targetDir}`);
827
1347
  }
828
1348
  function writeConfigFile(targetDir, cfg, courseName) {
829
1349
  recoverCred(targetDir, getRecoveryOptions());
830
- const p = path4.join(targetDir, CONFIG_FILENAME);
1350
+ const p = path5.join(targetDir, CONFIG_FILENAME);
831
1351
  const existing = readCredFile(targetDir);
832
1352
  const isNew = existing === null;
833
1353
  const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
@@ -846,15 +1366,15 @@ function getRecoveryOptions() {
846
1366
  return { dryRun: DRY_RUN, log: recoveryLog };
847
1367
  }
848
1368
  function detectExistingBootstrap(targetDir) {
849
- if (!fs4.existsSync(path4.join(targetDir, CONFIG_FILENAME))) {
1369
+ if (!fs5.existsSync(path5.join(targetDir, CONFIG_FILENAME))) {
850
1370
  return { bootstrapped: false };
851
1371
  }
852
- const mcpPath = path4.join(targetDir, ".mcp.json");
853
- if (!fs4.existsSync(mcpPath)) {
1372
+ const mcpPath = path5.join(targetDir, ".mcp.json");
1373
+ if (!fs5.existsSync(mcpPath)) {
854
1374
  return { bootstrapped: false };
855
1375
  }
856
1376
  try {
857
- const mcp = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
1377
+ const mcp = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
858
1378
  if (mcp.mcpServers?.["launch-secure"]) {
859
1379
  return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
860
1380
  }
@@ -872,13 +1392,7 @@ function buildLaunchKitMcpEntries(cfg) {
872
1392
  },
873
1393
  "launch-chart": {
874
1394
  command: "npx",
875
- args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
876
- // Tells launch-chart to also start its HTTP UI alongside the MCP, so
877
- // users can open the chart viewer at localhost:<port> while queries
878
- // hit the MCP. Without this, the MCP runs passively (queries work,
879
- // no UI). I4 deep-merge preserves user-added env keys; this default
880
- // ensures the auto-serve UX ships out of the box.
881
- env: { LAUNCH_CHART_AUTOSERVE: "1" }
1395
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
882
1396
  },
883
1397
  "launch-deck": {
884
1398
  command: "npx",
@@ -911,14 +1425,14 @@ function mergeMcpEntry(existing, ours) {
911
1425
  return merged;
912
1426
  }
913
1427
  function mergeMcpFile(targetDir, launchKitEntries) {
914
- const p = path4.join(targetDir, ".mcp.json");
915
- const hadExisting = fs4.existsSync(p);
1428
+ const p = path5.join(targetDir, ".mcp.json");
1429
+ const hadExisting = fs5.existsSync(p);
916
1430
  let existing = {};
917
1431
  if (hadExisting) {
918
1432
  try {
919
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
1433
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
920
1434
  } catch (err) {
921
- fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
1435
+ fail2(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
922
1436
  }
923
1437
  }
924
1438
  const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
@@ -936,19 +1450,21 @@ function mergeMcpFile(targetDir, launchKitEntries) {
936
1450
  }
937
1451
  }
938
1452
  if (DRY_RUN) {
939
- const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
940
- dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
941
- return;
942
- }
943
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
944
- const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
945
- ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
1453
+ const action = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
1454
+ dryNote(`${action} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
1455
+ return { status: "skipped", summary: "(dry-run)" };
1456
+ }
1457
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1458
+ const verb = hadExisting && existingServerCount > 0 ? "merged" : "wrote";
1459
+ ok(`${verb === "merged" ? "merged into" : "wrote"} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
1460
+ const total = Object.keys(launchKitEntries).length;
1461
+ return { status: "ok", summary: `${verb} ${total} entries` };
946
1462
  }
947
1463
  function detectPackageManager(repoDir) {
948
- const pkgPath = path4.join(repoDir, "package.json");
949
- if (!fs4.existsSync(pkgPath)) return null;
1464
+ const pkgPath = path5.join(repoDir, "package.json");
1465
+ if (!fs5.existsSync(pkgPath)) return null;
950
1466
  try {
951
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1467
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
952
1468
  if (typeof pkg.packageManager === "string") {
953
1469
  const name = pkg.packageManager.split("@")[0];
954
1470
  const match = PACKAGE_MANAGERS.find((p) => p.name === name);
@@ -957,7 +1473,7 @@ function detectPackageManager(repoDir) {
957
1473
  }
958
1474
  } catch {
959
1475
  }
960
- const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs4.existsSync(path4.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
1476
+ const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs5.existsSync(path5.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
961
1477
  if (matches.length === 1) {
962
1478
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
963
1479
  }
@@ -966,8 +1482,8 @@ function detectPackageManager(repoDir) {
966
1482
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
967
1483
  }
968
1484
  for (const pm of PACKAGE_MANAGERS) {
969
- if (pm.workspaceFiles?.some((wf) => fs4.existsSync(path4.join(repoDir, wf)))) {
970
- return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs4.existsSync(path4.join(repoDir, wf)))})` };
1485
+ if (pm.workspaceFiles?.some((wf) => fs5.existsSync(path5.join(repoDir, wf)))) {
1486
+ return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs5.existsSync(path5.join(repoDir, wf)))})` };
971
1487
  }
972
1488
  }
973
1489
  const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
@@ -976,8 +1492,8 @@ function detectPackageManager(repoDir) {
976
1492
  function runInstall(repoDir, detected) {
977
1493
  const { pm } = detected;
978
1494
  if (!which(pm.binary)) {
979
- fail(
980
- `${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(" ")}`
1495
+ fail2(
1496
+ `${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 ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
981
1497
  );
982
1498
  }
983
1499
  info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
@@ -985,24 +1501,24 @@ function runInstall(repoDir, detected) {
985
1501
  dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
986
1502
  return;
987
1503
  }
988
- const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
1504
+ const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
989
1505
  if (res.status !== 0) {
990
- fail(
1506
+ fail2(
991
1507
  `${pm.name} install failed (exit ${res.status}).
992
1508
 
993
1509
  Half-init state \u2014 install didn't complete, but these files ARE on disk:
994
- - ${path4.join(repoDir, CONFIG_FILENAME)} (cred file)
995
- - ${path4.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
996
- - ${path4.join(repoDir, ".gitignore")} (cred line appended)
1510
+ - ${path5.join(repoDir, CONFIG_FILENAME)} (cred file)
1511
+ - ${path5.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
1512
+ - ${path5.join(repoDir, ".gitignore")} (cred line appended)
997
1513
  - clone at ${repoDir}
998
1514
 
999
1515
  Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
1000
1516
 
1001
1517
  To retry install only:
1002
- cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
1518
+ cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
1003
1519
 
1004
1520
  To re-run init after fixing the install error:
1005
- cd ${path4.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
1521
+ cd ${path5.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
1006
1522
 
1007
1523
  To fully reset: delete the files listed above and the clone, then re-init.`
1008
1524
  );
@@ -1010,10 +1526,10 @@ To fully reset: delete the files listed above and the clone, then re-init.`
1010
1526
  ok(`${pm.name} install complete`);
1011
1527
  }
1012
1528
  function hasOnboardScript(repoDir) {
1013
- const pkgPath = path4.join(repoDir, "package.json");
1014
- if (!fs4.existsSync(pkgPath)) return false;
1529
+ const pkgPath = path5.join(repoDir, "package.json");
1530
+ if (!fs5.existsSync(pkgPath)) return false;
1015
1531
  try {
1016
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1532
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1017
1533
  return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
1018
1534
  } catch {
1019
1535
  return false;
@@ -1021,17 +1537,17 @@ function hasOnboardScript(repoDir) {
1021
1537
  }
1022
1538
  function runRecallInit(repoDir) {
1023
1539
  info(`scaffolding launch-recall (shadow git backup) \u2026`);
1024
- const recallEntry = path4.resolve(__dirname, "recall-entry.js");
1025
- const useSibling = fs4.existsSync(recallEntry);
1540
+ const recallEntry = path5.resolve(__dirname, "recall-entry.js");
1541
+ const useSibling = fs5.existsSync(recallEntry);
1026
1542
  const cmd = useSibling ? process.execPath : "npx";
1027
1543
  const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
1028
1544
  if (DRY_RUN) {
1029
1545
  dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
1030
1546
  return;
1031
1547
  }
1032
- const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
1548
+ const res = (0, import_node_child_process3.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
1033
1549
  if (res.status !== 0) {
1034
- 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`);
1550
+ info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path5.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
1035
1551
  return;
1036
1552
  }
1037
1553
  ok(`launch-recall ready (shadow git initialized)`);
@@ -1042,17 +1558,17 @@ function runOnboard(repoDir, pm) {
1042
1558
  dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
1043
1559
  return;
1044
1560
  }
1045
- const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
1561
+ const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
1046
1562
  if (res.status !== 0) {
1047
- fail(
1048
- `${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}`
1563
+ fail2(
1564
+ `${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path5.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
1049
1565
  );
1050
1566
  }
1051
1567
  ok(`${ONBOARD_SCRIPT_NAME} script complete`);
1052
1568
  }
1053
1569
  function ensureGitignoreLine(targetDir, line) {
1054
- const p = path4.join(targetDir, ".gitignore");
1055
- let content = fs4.existsSync(p) ? fs4.readFileSync(p, "utf-8") : "";
1570
+ const p = path5.join(targetDir, ".gitignore");
1571
+ let content = fs5.existsSync(p) ? fs5.readFileSync(p, "utf-8") : "";
1056
1572
  const lines = content.split(/\r?\n/);
1057
1573
  if (lines.some((l) => l.trim() === line)) return;
1058
1574
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -1062,28 +1578,28 @@ function ensureGitignoreLine(targetDir, line) {
1062
1578
  dryNote(`would append "${line}" to .gitignore`);
1063
1579
  return;
1064
1580
  }
1065
- fs4.writeFileSync(p, content, "utf-8");
1581
+ fs5.writeFileSync(p, content, "utf-8");
1066
1582
  ok(`appended ${line} to .gitignore`);
1067
1583
  }
1068
1584
  function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
1069
- if (!fs4.existsSync(srcDir)) return;
1070
- for (const entry of fs4.readdirSync(srcDir, { withFileTypes: true })) {
1071
- const srcPath = path4.join(srcDir, entry.name);
1072
- const destPath = path4.join(destDir, entry.name);
1585
+ if (!fs5.existsSync(srcDir)) return;
1586
+ for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) {
1587
+ const srcPath = path5.join(srcDir, entry.name);
1588
+ const destPath = path5.join(destDir, entry.name);
1073
1589
  const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
1074
1590
  if (entry.isDirectory()) {
1075
1591
  copyScaffoldDirAlways(srcPath, destPath, label);
1076
1592
  } else if (entry.isFile()) {
1077
- const existed = fs4.existsSync(destPath);
1593
+ const existed = fs5.existsSync(destPath);
1078
1594
  if (DRY_RUN) {
1079
1595
  dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
1080
1596
  continue;
1081
1597
  }
1082
- fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1083
- fs4.copyFileSync(srcPath, destPath);
1598
+ fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
1599
+ fs5.copyFileSync(srcPath, destPath);
1084
1600
  try {
1085
- const srcMode = fs4.statSync(srcPath).mode;
1086
- fs4.chmodSync(destPath, srcMode);
1601
+ const srcMode = fs5.statSync(srcPath).mode;
1602
+ fs5.chmodSync(destPath, srcMode);
1087
1603
  } catch {
1088
1604
  }
1089
1605
  ok(`${existed ? "refreshed" : "wrote"} ${label}`);
@@ -1091,54 +1607,78 @@ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
1091
1607
  }
1092
1608
  }
1093
1609
  function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
1094
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
1095
- if (!fs4.existsSync(scaffoldsRoot)) {
1096
- info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
1097
- return;
1610
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
1611
+ if (!fs5.existsSync(scaffoldsRoot)) {
1612
+ warn(`migrate-safety scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug; main onboarding unaffected`);
1613
+ return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1098
1614
  }
1099
1615
  const files = [
1100
1616
  {
1101
- src: path4.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
1102
- dest: path4.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
1617
+ src: path5.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
1618
+ dest: path5.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
1103
1619
  label: ".github/workflows/backup-on-migration.yml"
1104
1620
  },
1105
1621
  {
1106
- src: path4.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
1107
- dest: path4.join(targetDir, "scripts", "migrate-with-backup.sh"),
1622
+ src: path5.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
1623
+ dest: path5.join(targetDir, "scripts", "migrate-with-backup.sh"),
1108
1624
  label: "scripts/migrate-with-backup.sh"
1109
1625
  },
1110
1626
  {
1111
- src: path4.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
1112
- dest: path4.join(targetDir, "docs", "migrations-runbook.md"),
1627
+ src: path5.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
1628
+ dest: path5.join(targetDir, "docs", "migrations-runbook.md"),
1113
1629
  label: "docs/migrations-runbook.md"
1114
1630
  }
1115
1631
  ];
1116
1632
  info(`scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook)${refreshScaffolds ? " \u2014 --refresh-scaffolds active" : ""} \u2026`);
1117
- for (const f of files) copyScaffoldDriftAware(f.src, f.dest, f.label, refreshScaffolds);
1633
+ const counts = { wrote: 0, inSync: 0, drifted: 0, refreshed: 0, missing: 0 };
1634
+ for (const f of files) {
1635
+ const r = copyScaffoldDriftAware(f.src, f.dest, f.label, refreshScaffolds);
1636
+ if (r === "wrote") counts.wrote++;
1637
+ else if (r === "in-sync") counts.inSync++;
1638
+ else if (r === "drifted-preserved") counts.drifted++;
1639
+ else if (r === "drifted-refreshed") counts.refreshed++;
1640
+ else counts.missing++;
1641
+ }
1118
1642
  ensureGitignoreLine(targetDir, ".backups/");
1119
1643
  ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
1644
+ return summarizeFileCounts(counts);
1645
+ }
1646
+ function summarizeFileCounts(counts) {
1647
+ const total = counts.wrote + counts.inSync + counts.drifted + counts.refreshed + counts.missing;
1648
+ const parts = [];
1649
+ if (counts.wrote) parts.push(`${counts.wrote} written`);
1650
+ if (counts.refreshed) parts.push(`${counts.refreshed} refreshed`);
1651
+ if (counts.drifted) parts.push(`${counts.drifted} drifted (preserved \u2014 pass --refresh-scaffolds to update)`);
1652
+ if (counts.inSync && (counts.wrote || counts.refreshed || counts.drifted)) parts.push(`${counts.inSync} in sync`);
1653
+ if (counts.missing) parts.push(`${counts.missing} missing scaffold src`);
1654
+ if (parts.length === 0) parts.push(`in sync (${total} file${total === 1 ? "" : "s"})`);
1655
+ else if (!counts.wrote && !counts.refreshed && !counts.drifted && counts.inSync === total) {
1656
+ return { status: "in-sync", summary: `in sync (${total} file${total === 1 ? "" : "s"})` };
1657
+ }
1658
+ const status = counts.drifted || counts.missing ? "warn" : counts.wrote || counts.refreshed ? "ok" : "in-sync";
1659
+ return { status, summary: parts.join(", ") };
1120
1660
  }
1121
1661
  function hashFile(p) {
1122
1662
  try {
1123
- return crypto.createHash("sha256").update(fs4.readFileSync(p)).digest("hex");
1663
+ return crypto.createHash("sha256").update(fs5.readFileSync(p)).digest("hex");
1124
1664
  } catch {
1125
1665
  return null;
1126
1666
  }
1127
1667
  }
1128
1668
  function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1129
- if (!fs4.existsSync(srcPath)) {
1669
+ if (!fs5.existsSync(srcPath)) {
1130
1670
  info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
1131
1671
  return "missing-src";
1132
1672
  }
1133
- if (!fs4.existsSync(destPath)) {
1673
+ if (!fs5.existsSync(destPath)) {
1134
1674
  if (DRY_RUN) {
1135
1675
  dryNote(`would write ${label}`);
1136
1676
  return "wrote";
1137
1677
  }
1138
- fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1139
- fs4.copyFileSync(srcPath, destPath);
1678
+ fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
1679
+ fs5.copyFileSync(srcPath, destPath);
1140
1680
  try {
1141
- fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
1681
+ fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
1142
1682
  } catch {
1143
1683
  }
1144
1684
  ok(`wrote ${label}`);
@@ -1156,9 +1696,9 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1156
1696
  dryNote(`would refresh ${label} (overrides local edits)`);
1157
1697
  return "drifted-refreshed";
1158
1698
  }
1159
- fs4.copyFileSync(srcPath, destPath);
1699
+ fs5.copyFileSync(srcPath, destPath);
1160
1700
  try {
1161
- fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
1701
+ fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
1162
1702
  } catch {
1163
1703
  }
1164
1704
  ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
@@ -1170,10 +1710,10 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1170
1710
  var MARKETPLACE_ID = "launch-secure";
1171
1711
  var PLUGIN_ID = "kit";
1172
1712
  function isDogfoodMarketplace(targetDir) {
1173
- const p = path4.join(targetDir, ".claude", "settings.json");
1174
- if (!fs4.existsSync(p)) return { isDogfood: false };
1713
+ const p = path5.join(targetDir, ".claude", "settings.json");
1714
+ if (!fs5.existsSync(p)) return { isDogfood: false };
1175
1715
  try {
1176
- const settings = JSON.parse(fs4.readFileSync(p, "utf-8"));
1716
+ const settings = JSON.parse(fs5.readFileSync(p, "utf-8"));
1177
1717
  const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
1178
1718
  if (existingPath && existingPath !== "./.claude/marketplace") {
1179
1719
  return { isDogfood: true, existingPath };
@@ -1184,33 +1724,34 @@ function isDogfoodMarketplace(targetDir) {
1184
1724
  }
1185
1725
  }
1186
1726
  function scaffoldLsMarketplace(targetDir) {
1187
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
1188
- if (!fs4.existsSync(scaffoldsRoot)) {
1189
- info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
1190
- return;
1727
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
1728
+ if (!fs5.existsSync(scaffoldsRoot)) {
1729
+ warn(`ls-marketplace scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
1730
+ return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1191
1731
  }
1192
1732
  const dogfood = isDogfoodMarketplace(targetDir);
1193
1733
  if (dogfood.isDogfood) {
1194
1734
  info(`dogfood marketplace pointer detected (${dogfood.existingPath}) \u2014 skipping copy, only refreshing enabledPlugins`);
1195
1735
  wireLsSettings(targetDir);
1196
1736
  ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
1197
- return;
1737
+ return { status: "ok", summary: `dogfood pointer (${dogfood.existingPath})` };
1198
1738
  }
1199
- const marketplaceRoot = path4.join(targetDir, ".claude", "marketplace");
1739
+ const marketplaceRoot = path5.join(targetDir, ".claude", "marketplace");
1200
1740
  info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
1201
1741
  copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
1202
1742
  wireLsSettings(targetDir);
1203
1743
  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`);
1744
+ return { status: "ok", summary: "marketplace tree + settings refreshed" };
1204
1745
  }
1205
1746
  function wireLsSettings(targetDir) {
1206
- const p = path4.join(targetDir, ".claude", "settings.json");
1207
- const hadExisting = fs4.existsSync(p);
1747
+ const p = path5.join(targetDir, ".claude", "settings.json");
1748
+ const hadExisting = fs5.existsSync(p);
1208
1749
  let existing = {};
1209
1750
  if (hadExisting) {
1210
1751
  try {
1211
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
1752
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
1212
1753
  } catch (err) {
1213
- fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1754
+ fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1214
1755
  }
1215
1756
  }
1216
1757
  const merged = { ...existing };
@@ -1238,32 +1779,33 @@ function wireLsSettings(targetDir) {
1238
1779
  dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
1239
1780
  return;
1240
1781
  }
1241
- fs4.mkdirSync(path4.dirname(p), { recursive: true });
1242
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1782
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
1783
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1243
1784
  ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
1244
1785
  }
1245
1786
  var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
1246
1787
  var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
1247
1788
  function scaffoldRecallHook(targetDir) {
1248
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
1249
- if (!fs4.existsSync(scaffoldsRoot)) {
1250
- info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
1251
- return;
1789
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
1790
+ if (!fs5.existsSync(scaffoldsRoot)) {
1791
+ warn(`recall-hook scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
1792
+ return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1252
1793
  }
1253
1794
  info("scaffolding recall-hook (SessionStart watcher-respawn hook + ensure-recall.sh) \u2026");
1254
1795
  copyScaffoldDirAlways(scaffoldsRoot, targetDir, "");
1255
- wireRecallHook(targetDir);
1796
+ const wired = wireRecallHook(targetDir);
1256
1797
  ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
1798
+ return wired ? { status: "ok", summary: "ensure-recall.sh refreshed + SessionStart hook appended" } : { status: "ok", summary: "ensure-recall.sh refreshed (hook already wired)" };
1257
1799
  }
1258
1800
  function wireRecallHook(targetDir) {
1259
- const p = path4.join(targetDir, ".claude", "settings.json");
1260
- const hadExisting = fs4.existsSync(p);
1801
+ const p = path5.join(targetDir, ".claude", "settings.json");
1802
+ const hadExisting = fs5.existsSync(p);
1261
1803
  let existing = {};
1262
1804
  if (hadExisting) {
1263
1805
  try {
1264
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
1806
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
1265
1807
  } catch (err) {
1266
- fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1808
+ fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1267
1809
  }
1268
1810
  }
1269
1811
  const hooks = existing.hooks ?? {};
@@ -1273,7 +1815,7 @@ function wireRecallHook(targetDir) {
1273
1815
  );
1274
1816
  if (alreadyWired) {
1275
1817
  info(".claude/settings.json SessionStart hook already references ensure-recall.sh \u2014 leaving alone");
1276
- return;
1818
+ return false;
1277
1819
  }
1278
1820
  const newGroup = {
1279
1821
  hooks: [{ type: "command", command: RECALL_HOOK_COMMAND }]
@@ -1287,26 +1829,58 @@ function wireRecallHook(targetDir) {
1287
1829
  };
1288
1830
  if (DRY_RUN) {
1289
1831
  dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
1290
- return;
1832
+ return true;
1291
1833
  }
1292
- fs4.mkdirSync(path4.dirname(p), { recursive: true });
1293
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1834
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
1835
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1294
1836
  ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
1837
+ return true;
1295
1838
  }
1296
1839
  function tryActivateStatusline() {
1297
1840
  if (DRY_RUN) {
1298
1841
  dryNote(`would wrap ~/.claude/settings.json statusLine.command with launch-kit's MCP chip wrapper (skips silently if no statusline configured)`);
1299
- return;
1842
+ return { status: "skipped", summary: "(dry-run)" };
1300
1843
  }
1301
1844
  const res = activateStatusline();
1302
1845
  if (res.ok && res.outcome === "activated") {
1303
1846
  ok(`statusline wrapped \u2014 MCP chips will render alongside your existing statusline next session`);
1304
- } else if (res.ok && res.outcome === "refreshed") {
1847
+ return { status: "ok", summary: "wrapped \u2014 MCP chips render next session" };
1848
+ }
1849
+ if (res.ok && res.outcome === "refreshed") {
1305
1850
  info(`statusline already wrapped \u2014 refreshed chip scripts`);
1851
+ return { status: "ok", summary: "chip scripts refreshed" };
1306
1852
  }
1853
+ return null;
1307
1854
  }
1308
- async function main() {
1855
+ async function main2() {
1309
1856
  const subcommand = process.argv[2];
1857
+ if (subcommand === "radar-docker-init") {
1858
+ await Promise.resolve().then(() => (init_radar_docker_init_entry(), radar_docker_init_entry_exports));
1859
+ return;
1860
+ }
1861
+ if (subcommand === "setup-git") {
1862
+ if (process.argv[3] === "--help" || process.argv[3] === "-h") {
1863
+ console.log('usage: launch-kit setup-git --identity="Name <email>"');
1864
+ console.log("");
1865
+ console.log(" Runs `gh auth setup-git` when GH_TOKEN is set, then sets the");
1866
+ console.log(" global git user.name, user.email, init.defaultBranch=main, and");
1867
+ console.log(" pull.rebase=false. Idempotent; safe to re-run.");
1868
+ return;
1869
+ }
1870
+ let identityVal = null;
1871
+ for (const a of process.argv.slice(3)) {
1872
+ if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
1873
+ else fail2(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
1874
+ }
1875
+ if (!identityVal) fail2(`launch-kit setup-git requires --identity="Name <email>".`);
1876
+ try {
1877
+ const identity = parseGitIdentityFlag(identityVal, "--identity");
1878
+ configureGitForBot(identity);
1879
+ } catch (err) {
1880
+ fail2(err instanceof Error ? err.message : String(err));
1881
+ }
1882
+ return;
1883
+ }
1310
1884
  if (subcommand === "statusline") {
1311
1885
  const action = process.argv[3];
1312
1886
  if (!action || action === "--help" || action === "-h") {
@@ -1323,17 +1897,50 @@ async function main() {
1323
1897
  for (const a of process.argv.slice(4)) {
1324
1898
  if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
1325
1899
  else if (a === "--compact") compactArg = true;
1326
- else fail(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
1900
+ else fail2(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
1327
1901
  }
1328
1902
  const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
1329
1903
  let res;
1330
1904
  if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
1331
1905
  else if (action === "deactivate") res = deactivateStatusline2();
1332
- else fail(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
1906
+ else fail2(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
1333
1907
  if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
1334
1908
  else info(`statusline ${res.outcome} \u2014 ${res.message}`);
1335
1909
  return;
1336
1910
  }
1911
+ if (subcommand === "secrets") {
1912
+ const action = process.argv[3];
1913
+ if (!action || action === "--help" || action === "-h") {
1914
+ console.log("usage: launch-kit secrets pull [--env=<slug>] [--dir=<path>] [--file=<name>]");
1915
+ console.log("");
1916
+ console.log(" --env=<slug> env to pull from. Overrides $LS_ENV and the project-level default.");
1917
+ console.log(" --dir=<path> project root (default: cwd). Must contain a launch-kit cred file.");
1918
+ console.log(" --file=<name> output file relative to --dir (default: .env.local).");
1919
+ console.log("");
1920
+ console.log(" Resolution order: --env \u2192 $LS_ENV \u2192 server-side default \u2192 single-env auto-pick.");
1921
+ return;
1922
+ }
1923
+ if (action !== "pull") {
1924
+ fail2(`Unknown secrets action: "${action}". Supported: pull.`);
1925
+ }
1926
+ let envOverride = null;
1927
+ let dirArg = null;
1928
+ let fileArg = ".env.local";
1929
+ for (const a of process.argv.slice(4)) {
1930
+ if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
1931
+ else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
1932
+ else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
1933
+ else fail2(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
1934
+ }
1935
+ const targetDir = path5.resolve(dirArg ?? process.cwd());
1936
+ const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
1937
+ try {
1938
+ await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
1939
+ } catch (err) {
1940
+ fail2(err instanceof Error ? err.message : String(err));
1941
+ }
1942
+ return;
1943
+ }
1337
1944
  const args = parseArgs(process.argv.slice(2));
1338
1945
  if (args.help) {
1339
1946
  if (subcommand === "refresh") printRefreshHelp();
@@ -1341,25 +1948,26 @@ async function main() {
1341
1948
  return;
1342
1949
  }
1343
1950
  if (!subcommand || subcommand.startsWith("--")) {
1344
- fail(`missing subcommand. Usage: launch-kit <init|refresh|statusline> [options]. Run with --help.`);
1951
+ fail2(`missing subcommand. Usage: launch-kit <init|refresh|statusline|secrets> [options]. Run with --help.`);
1345
1952
  }
1346
1953
  if (subcommand !== "init" && subcommand !== "refresh") {
1347
- fail(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline. Run with --help for usage.`);
1954
+ fail2(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline, secrets. Run with --help for usage.`);
1348
1955
  }
1349
1956
  DRY_RUN = args.dryRun;
1957
+ VERBOSE = args.verbose || DRY_RUN;
1350
1958
  if (DRY_RUN) {
1351
- 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");
1352
- info("DRY RUN \u2014 no files will be written, no commands will run.");
1353
- info("Lines tagged (dry-run) show what would happen.");
1354
- 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");
1959
+ console.log(c.dim("\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"));
1960
+ console.log(c.bold("DRY RUN") + c.dim(" \u2014 no files will be written, no commands will run."));
1961
+ console.log(c.dim("Lines tagged (dry-run) show what would happen."));
1962
+ console.log(c.dim("\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"));
1355
1963
  }
1356
1964
  if (subcommand === "refresh") return mainRefresh(args);
1357
1965
  return mainInit(args);
1358
1966
  }
1359
1967
  async function mainRefresh(args) {
1360
1968
  const cwd = process.cwd();
1361
- const targetDir = path4.resolve(args.targetDir ?? cwd);
1362
- if (!fs4.existsSync(targetDir)) fail(`target dir does not exist: ${targetDir}`);
1969
+ const targetDir = path5.resolve(args.targetDir ?? cwd);
1970
+ if (!fs5.existsSync(targetDir)) fail2(`target dir does not exist: ${targetDir}`);
1363
1971
  let cred;
1364
1972
  let source;
1365
1973
  try {
@@ -1367,7 +1975,7 @@ async function mainRefresh(args) {
1367
1975
  cred = recovery.cred;
1368
1976
  source = recovery.source;
1369
1977
  } catch (err) {
1370
- fail(err instanceof Error ? err.message : String(err));
1978
+ fail2(err instanceof Error ? err.message : String(err));
1371
1979
  }
1372
1980
  if (cred && source === "mcp") {
1373
1981
  info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
@@ -1376,44 +1984,53 @@ async function mainRefresh(args) {
1376
1984
  if (DRY_RUN) {
1377
1985
  dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
1378
1986
  } else {
1379
- writeJsonAtomic(path4.join(targetDir, CONFIG_FILENAME), nested2, 384);
1987
+ writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
1380
1988
  ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
1381
1989
  }
1382
1990
  }
1383
1991
  if (!cred) {
1384
- fail(
1385
- `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.`
1992
+ fail2(
1993
+ `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=${path5.relative(cwd, targetDir) || "."}\` first.`
1386
1994
  );
1387
1995
  }
1388
1996
  const nested = toNested(cred);
1389
- if (!nested) fail(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
1997
+ if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
1390
1998
  const active = nested.profiles[nested.active];
1391
- if (!active) fail(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
1999
+ if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
1392
2000
  info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
2001
+ header("launch-kit refresh", [
2002
+ ["course", nested.active],
2003
+ ["project", `${active.orgSlug}/${active.projectSlug}`],
2004
+ ["dir", path5.relative(cwd, targetDir) || "."]
2005
+ ]);
1393
2006
  const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
1394
- mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
2007
+ phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
1395
2008
  ensureGitignoreLine(targetDir, CONFIG_FILENAME);
1396
- if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
1397
- if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
1398
- if (!args.noRecallHook) scaffoldRecallHook(targetDir);
1399
- tryActivateStatusline();
1400
- console.log("");
2009
+ if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
2010
+ if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
2011
+ if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
2012
+ const slR = tryActivateStatusline();
2013
+ if (slR) phase("statusline", slR);
1401
2014
  if (DRY_RUN) {
1402
- 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");
1403
- info(`DRY RUN COMPLETE \u2014 refresh would have applied the above; no files modified.`);
1404
- 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");
2015
+ console.log("");
2016
+ console.log(c.dim("\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"));
2017
+ console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
2018
+ console.log(c.dim("\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"));
1405
2019
  return;
1406
2020
  }
1407
2021
  ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
1408
- if (!args.quiet) {
1409
- console.log(`
1410
- Skipped (refresh never runs these): clone, dependency install, onboard script, launch-recall init. Use \`npx @launchsecure/launch-kit init\` for a full bootstrap.
1411
- ${getLaunchKitToolsGuide()}`);
2022
+ const showGuide = args.quiet ? false : args.guide === true;
2023
+ const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
2024
+ if (!showGuide && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
2025
+ footer("Refresh complete.", hints);
2026
+ if (showGuide) {
2027
+ console.log(c.dim(`(refresh never runs these: clone, dependency install, onboard script, launch-recall init \u2014 use \`launch-kit init\` for a full bootstrap)`));
2028
+ console.log(getLaunchKitToolsGuide());
1412
2029
  }
1413
2030
  }
1414
2031
  async function mainInit(args) {
1415
- const probeDir = path4.resolve(args.targetDir ?? process.cwd());
1416
- if (!args.force && fs4.existsSync(probeDir)) {
2032
+ const probeDir = path5.resolve(args.targetDir ?? process.cwd());
2033
+ if (!args.force && fs5.existsSync(probeDir)) {
1417
2034
  const detection = detectExistingBootstrap(probeDir);
1418
2035
  if (detection.bootstrapped) {
1419
2036
  info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
@@ -1422,8 +2039,8 @@ async function mainInit(args) {
1422
2039
  }
1423
2040
  }
1424
2041
  if (!args.token || !args.orgSlug || !args.projectSlug) {
1425
- const recoveryDir = path4.resolve(args.targetDir ?? process.cwd());
1426
- if (fs4.existsSync(recoveryDir)) {
2042
+ const recoveryDir = path5.resolve(args.targetDir ?? process.cwd());
2043
+ if (fs5.existsSync(recoveryDir)) {
1427
2044
  const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
1428
2045
  const nested = cred ? toNested(cred) : null;
1429
2046
  const recovered = nested ? nested.profiles[nested.active] : cred;
@@ -1450,43 +2067,61 @@ async function mainInit(args) {
1450
2067
  const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
1451
2068
  args.token = t || null;
1452
2069
  }
1453
- if (!args.token) fail("--token (or LS_PAT env) is required.");
1454
- if (!/^ls_pat_/.test(args.token)) fail("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
1455
- if (!args.orgSlug) fail("--org=<orgSlug> is required.");
1456
- if (!args.projectSlug) fail("--project=<projectSlug> is required.");
2070
+ if (!args.token) fail2("--token (or LS_PAT env) is required.");
2071
+ if (!/^ls_pat_/.test(args.token)) fail2("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
2072
+ if (!args.orgSlug) fail2("--org=<orgSlug> is required.");
2073
+ if (!args.projectSlug) fail2("--project=<projectSlug> is required.");
2074
+ header("launch-kit init", [
2075
+ ["org", args.orgSlug],
2076
+ ["project", args.projectSlug],
2077
+ ["server", args.serverUrl]
2078
+ ]);
1457
2079
  const { hasGh } = preflight();
2080
+ phase("preflight", { status: "ok", summary: `node ${process.versions.node}${hasGh ? " \xB7 git \xB7 gh" : " \xB7 git (gh not found \u2014 will use git for clone)"}` });
2081
+ if (args.gitIdentity) {
2082
+ configureGitForBot(args.gitIdentity);
2083
+ phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 gh credential helper wired" : ""}` });
2084
+ }
1458
2085
  info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
1459
2086
  let resolved;
1460
2087
  try {
1461
2088
  resolved = await callProjectInfo(args);
1462
2089
  } catch (err) {
1463
- fail(err instanceof Error ? err.message : String(err));
2090
+ fail2(err instanceof Error ? err.message : String(err));
1464
2091
  }
1465
2092
  ok(`resolved "${resolved.projectName}"`);
2093
+ phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
1466
2094
  if (!resolved.repositoryUrl) {
1467
- fail(
2095
+ fail2(
1468
2096
  `Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
1469
2097
  );
1470
2098
  }
1471
2099
  const repoUrl = resolved.repositoryUrl;
1472
2100
  const cwd = process.cwd();
1473
- const targetDir = path4.resolve(args.targetDir ?? path4.join(cwd, resolved.projectSlug));
2101
+ const targetDir = path5.resolve(args.targetDir ?? path5.join(cwd, resolved.projectSlug));
1474
2102
  const normalizedRemote = normalizeRepoUrl(repoUrl);
1475
2103
  let skipClone = false;
1476
- if (fs4.existsSync(targetDir)) {
2104
+ if (fs5.existsSync(targetDir)) {
1477
2105
  if (isGitRepo(targetDir)) {
1478
2106
  const existingRemote = gitRemoteUrl(targetDir);
1479
2107
  if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
1480
2108
  ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
1481
2109
  skipClone = true;
1482
2110
  } else {
1483
- fail(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
2111
+ fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
1484
2112
  }
1485
2113
  } else if (!dirIsEmpty(targetDir)) {
1486
- fail(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
2114
+ fail2(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
1487
2115
  }
1488
2116
  }
1489
- if (!skipClone) cloneRepo(repoUrl, targetDir, hasGh);
2117
+ const relTarget = path5.relative(cwd, targetDir) || ".";
2118
+ if (!skipClone) {
2119
+ section(`Cloning ${repoUrl}`);
2120
+ cloneRepo(repoUrl, targetDir, hasGh);
2121
+ phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
2122
+ } else {
2123
+ phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
2124
+ }
1490
2125
  const cfg = {
1491
2126
  pat: args.token,
1492
2127
  orgSlug: resolved.orgSlug,
@@ -1495,7 +2130,8 @@ async function mainInit(args) {
1495
2130
  };
1496
2131
  const courseName = args.course ?? inferCourseName(cfg.serverUrl);
1497
2132
  writeConfigFile(targetDir, cfg, courseName);
1498
- mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
2133
+ phase("cred file", { status: "ok", summary: `course=${courseName}, active` });
2134
+ phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
1499
2135
  ensureGitignoreLine(targetDir, CONFIG_FILENAME);
1500
2136
  let installSkippedReason = null;
1501
2137
  const detected = detectPackageManager(targetDir);
@@ -1505,45 +2141,71 @@ async function mainInit(args) {
1505
2141
  } else if (!detected) {
1506
2142
  installSkippedReason = "no package.json found";
1507
2143
  } else {
2144
+ section(`Installing dependencies (${detected.pm.name})`);
1508
2145
  runInstall(targetDir, detected);
1509
- if (!args.noOnboard && hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
2146
+ phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
2147
+ if (!args.noOnboard) {
2148
+ if (hasOnboardScript(targetDir)) {
2149
+ section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
2150
+ runOnboard(targetDir, detected.pm);
2151
+ phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
2152
+ } else {
2153
+ section("Pulling environment secrets");
2154
+ info("running launch-kit secrets pull \u2026");
2155
+ if (DRY_RUN) {
2156
+ dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
2157
+ phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
2158
+ } else {
2159
+ try {
2160
+ await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
2161
+ phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
2162
+ } catch (err) {
2163
+ const msg = err instanceof Error ? err.message : String(err);
2164
+ warn(`secrets pull skipped \u2014 ${msg}`);
2165
+ phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
2166
+ }
2167
+ }
2168
+ }
2169
+ }
1510
2170
  }
1511
2171
  const hasOnboard = hasOnboardScript(targetDir);
1512
- if (!args.noRecall) runRecallInit(targetDir);
1513
- if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
1514
- if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
1515
- if (!args.noRecallHook) scaffoldRecallHook(targetDir);
1516
- tryActivateStatusline();
1517
- const relTarget = path4.relative(cwd, targetDir) || ".";
1518
- console.log("");
2172
+ if (installSkippedReason) phase("install", { status: "skipped", summary: installSkippedReason });
2173
+ if (!args.noRecall) {
2174
+ section("Initializing launch-recall (shadow git backup)");
2175
+ runRecallInit(targetDir);
2176
+ phase("launch-recall", { status: "ok", summary: "shadow git ready" });
2177
+ }
2178
+ if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
2179
+ if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
2180
+ if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
2181
+ const slR = tryActivateStatusline();
2182
+ if (slR) phase("statusline", slR);
1519
2183
  if (DRY_RUN) {
1520
- 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");
1521
- info(`DRY RUN COMPLETE \u2014 no files were modified, no commands ran.`);
1522
- info(`Target: ${targetDir}`);
1523
- info(`Re-run without --dry-run to apply the changes shown above.`);
1524
- 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");
2184
+ console.log("");
2185
+ console.log(c.dim("\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"));
2186
+ console.log(c.bold("DRY RUN COMPLETE") + c.dim(` \u2014 no files were modified, no commands ran.`));
2187
+ console.log(c.dim(`Target: ${targetDir}`));
2188
+ console.log(c.dim(`Re-run without --dry-run to apply the changes shown above.`));
2189
+ console.log(c.dim("\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"));
1525
2190
  return;
1526
2191
  }
1527
2192
  ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
2193
+ const showGuide = args.quiet ? false : args.guide ?? true;
2194
+ const nextSteps = [`cd ${relTarget}`];
1528
2195
  if (installSkippedReason) {
1529
- const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
1530
- const onboardLine = hasOnboard && detected && !args.noOnboard ? `
1531
- ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
1532
- console.log(`
1533
- Next steps (install skipped: ${installSkippedReason}):
1534
- cd ${relTarget}
1535
- ${installLine}${onboardLine}
1536
- claude # launch Claude Code (5 MCPs wired)${args.quiet ? "" : `
1537
- ${getLaunchKitToolsGuide()}`}`);
1538
- } else if (!args.quiet) {
1539
- console.log(`
1540
- Next steps:
1541
- cd ${relTarget}
1542
- claude # launch Claude Code (5 MCPs wired)
1543
- ${getLaunchKitToolsGuide()}`);
1544
- }
1545
- }
1546
- main().catch((err) => {
2196
+ nextSteps.push(detected ? `${detected.pm.binary} ${detected.pm.installArgs.join(" ")} # install skipped: ${installSkippedReason}` : `npm install # install skipped: ${installSkippedReason}`);
2197
+ if (hasOnboard && detected && !args.noOnboard) {
2198
+ nextSteps.push(`${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook`);
2199
+ }
2200
+ }
2201
+ nextSteps.push("claude # launch Claude Code (5 MCPs wired)");
2202
+ footer(`${resolved.projectName} is ready at ${relTarget}.`, [
2203
+ "Next:",
2204
+ ...nextSteps.map((s) => " " + s)
2205
+ ]);
2206
+ if (showGuide) console.log(getLaunchKitToolsGuide());
2207
+ }
2208
+ main2().catch((err) => {
1547
2209
  console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
1548
2210
  process.exit(1);
1549
2211
  });