@launchsecure/launch-kit 0.0.32 → 0.0.34

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 (93) hide show
  1. package/dist/chart-client/assets/{index-B__ARB8k.js → index-DFu2xIrM.js} +2 -2
  2. package/dist/chart-client/assets/index-DpKO9p0s.css +1 -0
  3. package/dist/chart-client/index.html +2 -2
  4. package/dist/client/assets/{index-h8kMzVtG.js → index-Cbw6bVdx.js} +2 -2
  5. package/dist/client/assets/index-Dv6dD2zY.css +32 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/council-client/assets/index-AqQ9Sei6.css +1 -0
  8. package/dist/council-client/assets/{index-CWaDcsFR.js → index-CAsmGTzg.js} +2 -2
  9. package/dist/council-client/index.html +2 -2
  10. package/dist/deck-client/assets/{_baseUniq-C7GsHvgg.js → _baseUniq-BiVx0WO_.js} +1 -1
  11. package/dist/deck-client/assets/{arc-CSrZRINY.js → arc-DGMkiEzS.js} +1 -1
  12. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-zoB-G17J.js → architectureDiagram-Q4EWVU46-Y2WRmHtk.js} +1 -1
  13. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BRjjtYH6.js → blockDiagram-DXYQGD6D-_Lbfu5BQ.js} +1 -1
  14. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-C3D3sd2U.js → c4Diagram-AHTNJAMY-CTqpYTBX.js} +1 -1
  15. package/dist/deck-client/assets/channel-DB6LxW_l.js +1 -0
  16. package/dist/deck-client/assets/{chunk-4BX2VUAB-DhpDMOPO.js → chunk-4BX2VUAB-liEIbPHs.js} +1 -1
  17. package/dist/deck-client/assets/{chunk-4TB4RGXK-BIRgPXRl.js → chunk-4TB4RGXK-CCc6lYvL.js} +1 -1
  18. package/dist/deck-client/assets/{chunk-55IACEB6-BF24dwDZ.js → chunk-55IACEB6-D02jJUR2.js} +1 -1
  19. package/dist/deck-client/assets/{chunk-EDXVE4YY-CW75Y61B.js → chunk-EDXVE4YY-BFmGMbLD.js} +1 -1
  20. package/dist/deck-client/assets/{chunk-FMBD7UC4-B5-oyL79.js → chunk-FMBD7UC4-6wFLOVcJ.js} +1 -1
  21. package/dist/deck-client/assets/{chunk-OYMX7WX6-BB2bHe_Q.js → chunk-OYMX7WX6-Bnr8RiBf.js} +1 -1
  22. package/dist/deck-client/assets/{chunk-QZHKN3VN-D80eZO4B.js → chunk-QZHKN3VN-Ct82MksJ.js} +1 -1
  23. package/dist/deck-client/assets/{chunk-YZCP3GAM-Dz9787p_.js → chunk-YZCP3GAM-BXmN1diQ.js} +1 -1
  24. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +1 -0
  25. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +1 -0
  26. package/dist/deck-client/assets/clone-DiIRH1pI.js +1 -0
  27. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-MQjiZLcL.js → cose-bilkent-S5V4N54A-CmQCT-mH.js} +1 -1
  28. package/dist/deck-client/assets/{dagre-KV5264BT-DG4EcLpJ.js → dagre-KV5264BT-DDdSa9EX.js} +1 -1
  29. package/dist/deck-client/assets/{diagram-5BDNPKRD-1n7hM3Gc.js → diagram-5BDNPKRD-Bccks2xJ.js} +1 -1
  30. package/dist/deck-client/assets/{diagram-G4DWMVQ6-CYMarncV.js → diagram-G4DWMVQ6-CPPNgxmQ.js} +1 -1
  31. package/dist/deck-client/assets/{diagram-MMDJMWI5-DSisoipe.js → diagram-MMDJMWI5-KrD300pS.js} +1 -1
  32. package/dist/deck-client/assets/{diagram-TYMM5635-Btnq49OJ.js → diagram-TYMM5635-DefnLuQf.js} +1 -1
  33. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-Cu2Hb_Tz.js → erDiagram-SMLLAGMA-DI9FfnFP.js} +1 -1
  34. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-CGJzUzsO.js → flowDiagram-DWJPFMVM-twKyd3Fx.js} +1 -1
  35. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-D9sqGUBT.js → ganttDiagram-T4ZO3ILL-Wau3jhBr.js} +1 -1
  36. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-C0QwX2od.js → gitGraphDiagram-UUTBAWPF-D9GgYXwb.js} +1 -1
  37. package/dist/deck-client/assets/{graph-CcBjOQCl.js → graph-BhNLzyXS.js} +1 -1
  38. package/dist/deck-client/assets/index-B-YQq5b5.css +1 -0
  39. package/dist/deck-client/assets/{index-0arwoc0z.js → index-BtQBaQ7s.js} +3 -3
  40. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-DTimhhhS.js → infoDiagram-42DDH7IO-TylGlSG-.js} +1 -1
  41. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DxOxg_B4.js → ishikawaDiagram-UXIWVN3A-DAT8icpg.js} +1 -1
  42. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-Bpq0qa4j.js → journeyDiagram-VCZTEJTY-D3v_XL72.js} +1 -1
  43. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-aTIrpcVO.js → kanban-definition-6JOO6SKY-DNUOBiNr.js} +1 -1
  44. package/dist/deck-client/assets/{layout-DqglLR2E.js → layout-COfodgwF.js} +1 -1
  45. package/dist/deck-client/assets/{linear-D5GxehPc.js → linear-DmTsuIvK.js} +1 -1
  46. package/dist/deck-client/assets/{min-DXLfSREq.js → min-BW1F7i1D.js} +1 -1
  47. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-mO5Vys7I.js → mindmap-definition-QFDTVHPH-CErFzKWl.js} +1 -1
  48. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-Dm0gzdAr.js → pieDiagram-DEJITSTG-DW5F757o.js} +1 -1
  49. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-Daq7j3qD.js → quadrantDiagram-34T5L4WZ-B1S2-TfI.js} +1 -1
  50. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CmwV95um.js → requirementDiagram-MS252O5E-BY5BAR-5.js} +1 -1
  51. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BOYl3Nkf.js → sankeyDiagram-XADWPNL6-CE1Cp9HS.js} +1 -1
  52. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-BuUjhIcW.js → sequenceDiagram-FGHM5R23-IaHnbKye.js} +1 -1
  53. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-LUZ_uwio.js → stateDiagram-FHFEXIEX-CwPJm9hU.js} +1 -1
  54. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +1 -0
  55. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-CDUxCCAW.js → timeline-definition-GMOUNBTQ-DVFGGSgN.js} +1 -1
  56. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-BRb24Tf7.js → vennDiagram-DHZGUBPP-C1194MJi.js} +1 -1
  57. package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +162 -0
  58. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-BLGlYrQz.js → wardleyDiagram-NUSXRM2D-hpwdFfGj.js} +1 -1
  59. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-De31MSnk.js → xychartDiagram-5P7HB3ND-DYkotwy8.js} +1 -1
  60. package/dist/deck-client/index.html +2 -2
  61. package/dist/server/chart-serve.js +167 -2
  62. package/dist/server/cli.js +328 -42
  63. package/dist/server/course-entry.js +1 -1
  64. package/dist/server/graph-mcp-entry.js +180 -4
  65. package/dist/server/init-entry.js +1133 -219
  66. package/dist/server/launch-radar-entry.js +45 -0
  67. package/dist/server/parse-worker-entry.js +167 -2
  68. package/dist/server/radar-docker-init-entry.js +644 -0
  69. package/dist/server/radar-entrypoint-entry.js +99 -0
  70. package/dist/server/radar-teardown-entry.js +478 -0
  71. package/dist/server/recall-entry.js +4 -1
  72. package/dist/server/rover-entry.js +20122 -0
  73. package/package.json +7 -5
  74. package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
  75. package/scaffolds/ls-marketplace/plugins/kit/commands/standup.md +6 -6
  76. package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +6 -0
  77. package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +40 -48
  78. package/scaffolds/ls-marketplace/plugins/kit/skills/debug/SKILL.md +45 -20
  79. package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +76 -67
  80. package/scaffolds/ls-marketplace/plugins/kit/skills/handoff/SKILL.md +132 -0
  81. package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +290 -0
  82. package/scaffolds/statusline/statusline-mcp.sh +82 -2
  83. package/scaffolds/statusline/statusline-wrapper.sh +8 -1
  84. package/dist/chart-client/assets/index-CDIhdgWg.css +0 -1
  85. package/dist/client/assets/index-CfW4n40I.css +0 -32
  86. package/dist/council-client/assets/index-CZim6x1u.css +0 -1
  87. package/dist/deck-client/assets/channel-8ReQnQfH.js +0 -1
  88. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-cRxTeGkK.js +0 -1
  89. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-cRxTeGkK.js +0 -1
  90. package/dist/deck-client/assets/clone-LSHZ3K6R.js +0 -1
  91. package/dist/deck-client/assets/index-BlTlhxFW.css +0 -1
  92. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CnnRwE5D.js +0 -1
  93. package/dist/deck-client/assets/wardley-RL74JXVD-B0BYyVBY.js +0 -162
@@ -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,708 @@ 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 };
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;
387
+ }
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
+ };
167
468
  }
168
- if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
169
- return null;
469
+ });
470
+
471
+ // src/server/launch-kit-services.ts
472
+ function defaultServices() {
473
+ return [expandShorthand("radar")];
474
+ }
475
+ function expandShorthand(name) {
476
+ const def = SHORTHANDS[name];
477
+ if (!def) {
478
+ throw new Error(
479
+ `[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
480
+ );
481
+ }
482
+ return { name, port: def.port, bin: def.bin, args: [...def.args] };
483
+ }
484
+ function coerceEntry(raw, index) {
485
+ if (typeof raw === "string") {
486
+ return expandShorthand(raw);
487
+ }
488
+ if (typeof raw !== "object" || raw === null) {
489
+ throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
490
+ }
491
+ const r = raw;
492
+ if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
493
+ throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
494
+ }
495
+ if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
496
+ throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
170
497
  }
171
- const name = inferCourseName(cred.serverUrl);
172
498
  return {
173
- active: name,
174
- profiles: {
175
- [name]: {
176
- pat: cred.pat,
177
- orgSlug: cred.orgSlug,
178
- projectSlug: cred.projectSlug,
179
- serverUrl: cred.serverUrl
499
+ name: r.name,
500
+ port: r.port,
501
+ bin: r.bin,
502
+ args: r.args ?? []
503
+ };
504
+ }
505
+ function validate(services) {
506
+ if (services.length === 0) {
507
+ throw new Error(`[launch-kit-services] resolved an empty service list`);
508
+ }
509
+ const seenNames = /* @__PURE__ */ new Set();
510
+ const seenPorts = /* @__PURE__ */ new Set();
511
+ for (const s of services) {
512
+ if (!DNS_NAME_RE.test(s.name)) {
513
+ throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
514
+ }
515
+ if (seenNames.has(s.name)) {
516
+ throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
517
+ }
518
+ seenNames.add(s.name);
519
+ if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
520
+ throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
521
+ }
522
+ if (seenPorts.has(s.port)) {
523
+ throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
524
+ }
525
+ seenPorts.add(s.port);
526
+ }
527
+ return services;
528
+ }
529
+ function resolveServices(opts = {}) {
530
+ const env = opts.env ?? process.env;
531
+ const cwd = opts.cwd ?? process.cwd();
532
+ const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
533
+ if (rawEnv) {
534
+ let parsed;
535
+ try {
536
+ parsed = JSON.parse(rawEnv);
537
+ } catch (err) {
538
+ throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
539
+ }
540
+ if (!Array.isArray(parsed)) {
541
+ throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
542
+ }
543
+ return validate(parsed.map(coerceEntry));
544
+ }
545
+ const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
546
+ if ((0, import_node_fs.existsSync)(filePath)) {
547
+ let parsed;
548
+ try {
549
+ parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
550
+ } catch (err) {
551
+ throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
552
+ }
553
+ if (!Array.isArray(parsed)) {
554
+ throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
555
+ }
556
+ return validate(parsed.map(coerceEntry));
557
+ }
558
+ return validate(defaultServices());
559
+ }
560
+ var import_node_fs, import_node_path, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
561
+ var init_launch_kit_services = __esm({
562
+ "src/server/launch-kit-services.ts"() {
563
+ "use strict";
564
+ import_node_fs = require("node:fs");
565
+ import_node_path = require("node:path");
566
+ SHORTHANDS = {
567
+ radar: { port: 3517, bin: "launch-radar", args: [] },
568
+ sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
569
+ chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
570
+ deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
571
+ council: { port: 52839, bin: "launch-council", args: ["serve"] }
572
+ };
573
+ DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
574
+ SHORTHAND_NAMES = Object.keys(SHORTHANDS);
575
+ }
576
+ });
577
+
578
+ // src/server/cf-ingress.ts
579
+ async function cf(opts) {
580
+ const res = await fetch(`${CF_API_BASE}${opts.path}`, {
581
+ method: opts.method,
582
+ headers: {
583
+ Authorization: `Bearer ${opts.apiToken}`,
584
+ "Content-Type": "application/json",
585
+ Accept: "application/json",
586
+ "User-Agent": "launch-kit/cf-ingress"
587
+ },
588
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
589
+ signal: AbortSignal.timeout(15e3)
590
+ });
591
+ const text = await res.text();
592
+ let parsed;
593
+ try {
594
+ parsed = text ? JSON.parse(text) : { success: false };
595
+ } catch {
596
+ throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
597
+ }
598
+ return parsed;
599
+ }
600
+ function isNotFound(env) {
601
+ return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
602
+ }
603
+ function loadState(path6) {
604
+ if (!(0, import_node_fs2.existsSync)(path6)) return null;
605
+ try {
606
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path6, "utf8"));
607
+ if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
608
+ return parsed;
609
+ }
610
+ return null;
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+ function saveState(path6, state) {
616
+ const dir = (0, import_node_path2.dirname)(path6);
617
+ if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
618
+ (0, import_node_fs2.writeFileSync)(path6, JSON.stringify(state, null, 2));
619
+ }
620
+ async function ensureTunnel(input, knownTunnelId) {
621
+ if (knownTunnelId) {
622
+ const got = await cf({
623
+ apiToken: input.apiToken,
624
+ method: "GET",
625
+ path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
626
+ });
627
+ if (got.success && got.result && !got.result.deleted_at) {
628
+ return knownTunnelId;
629
+ }
630
+ if (!isNotFound(got) && !got.success) {
631
+ throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
632
+ }
633
+ }
634
+ const created = await cf({
635
+ apiToken: input.apiToken,
636
+ method: "POST",
637
+ path: `/accounts/${input.accountId}/cfd_tunnel`,
638
+ body: { name: input.tunnelName, config_src: "cloudflare" }
639
+ });
640
+ if (!created.success || !created.result) {
641
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
642
+ }
643
+ return created.result.id;
644
+ }
645
+ async function fetchConnectorToken(input, tunnelId) {
646
+ const res = await cf({
647
+ apiToken: input.apiToken,
648
+ method: "GET",
649
+ path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
650
+ });
651
+ if (!res.success || typeof res.result !== "string") {
652
+ throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
653
+ }
654
+ return res.result;
655
+ }
656
+ async function setIngressConfig(input, tunnelId) {
657
+ const ingress = input.services.map((s) => ({
658
+ hostname: `${s.name}.${input.zone.name}`,
659
+ service: `http://localhost:${s.port}`
660
+ }));
661
+ ingress.push({ service: "http_status:404" });
662
+ const res = await cf({
663
+ apiToken: input.apiToken,
664
+ method: "PUT",
665
+ path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
666
+ body: { config: { ingress } }
667
+ });
668
+ if (!res.success) {
669
+ throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
670
+ }
671
+ }
672
+ async function ensureDnsRecord(input, tunnelId, service) {
673
+ const fqdn = `${service.name}.${input.zone.name}`;
674
+ const target = `${tunnelId}.cfargotunnel.com`;
675
+ const existing = await cf({
676
+ apiToken: input.apiToken,
677
+ method: "GET",
678
+ path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
679
+ });
680
+ if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
681
+ const rec = existing.result[0];
682
+ if (rec.content === target) return;
683
+ const upd = await cf({
684
+ apiToken: input.apiToken,
685
+ method: "PUT",
686
+ path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
687
+ body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
688
+ });
689
+ if (!upd.success) {
690
+ throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
691
+ }
692
+ return;
693
+ }
694
+ const created = await cf({
695
+ apiToken: input.apiToken,
696
+ method: "POST",
697
+ path: `/zones/${input.zone.id}/dns_records`,
698
+ body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
699
+ });
700
+ if (created.success) return;
701
+ if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
702
+ throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
703
+ }
704
+ async function provisionIngress(input) {
705
+ const prior = loadState(input.stateFile);
706
+ const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
707
+ saveState(input.stateFile, {
708
+ tunnelId,
709
+ accountId: input.accountId,
710
+ tunnelName: input.tunnelName,
711
+ zoneId: input.zone.id
712
+ });
713
+ const connectorToken = await fetchConnectorToken(input, tunnelId);
714
+ await setIngressConfig(input, tunnelId);
715
+ await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
716
+ const hostnames = {};
717
+ for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
718
+ return { tunnelId, connectorToken, hostnames };
719
+ }
720
+ var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
721
+ var init_cf_ingress = __esm({
722
+ "src/server/cf-ingress.ts"() {
723
+ "use strict";
724
+ import_node_fs2 = require("node:fs");
725
+ import_node_path2 = require("node:path");
726
+ CF_API_BASE = "https://api.cloudflare.com/client/v4";
727
+ CF_ERR_DNS_RECORD_EXISTS = 81053;
728
+ }
729
+ });
730
+
731
+ // src/server/radar-docker-init-entry.ts
732
+ var radar_docker_init_entry_exports = {};
733
+ __export(radar_docker_init_entry_exports, {
734
+ maybeProvisionIngress: () => maybeProvisionIngress,
735
+ spawnServiceGroup: () => spawnServiceGroup
736
+ });
737
+ function fail(message) {
738
+ console.error(message);
739
+ process.exit(1);
740
+ }
741
+ function requireEnv(name) {
742
+ const v = process.env[name];
743
+ if (!v) fail(`ERROR: ${name} is required but not set`);
744
+ return v;
745
+ }
746
+ function run2(cmd, args, stdio = "inherit") {
747
+ const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
748
+ return r.status ?? 1;
749
+ }
750
+ async function setupFromCloud() {
751
+ const pat = requireEnv("LS_PAT");
752
+ const orgSlug = requireEnv("LS_ORG_SLUG");
753
+ const projectSlug = requireEnv("LS_PROJECT_SLUG");
754
+ const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
755
+ const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
756
+ let bundle;
757
+ try {
758
+ bundle = await mcp.call("radar_bootstrap_get", {});
759
+ } catch (err) {
760
+ fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
761
+ }
762
+ if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
763
+ if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
764
+ if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
765
+ if (!process.env.GH_TOKEN) {
766
+ 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.`);
767
+ }
768
+ const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
769
+ console.log(`[entrypoint] bundle from cloud: org=${orgSlug} project=${projectSlug} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()} ${cfNote}`);
770
+ return bundle;
771
+ }
772
+ function setupClaudeCredentials() {
773
+ const home = process.env.HOME ?? "/home/launchpod";
774
+ const claudeDir = (0, import_node_path3.join)(home, ".claude");
775
+ (0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
776
+ const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
777
+ const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
778
+ (0, import_node_fs3.writeFileSync)(credsPath, decoded);
779
+ (0, import_node_fs3.chmodSync)(credsPath, 384);
780
+ const configPath = (0, import_node_path3.join)(home, ".claude.json");
781
+ let cfg = {};
782
+ if ((0, import_node_fs3.existsSync)(configPath)) {
783
+ try {
784
+ cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
785
+ } catch {
786
+ cfg = {};
787
+ }
788
+ }
789
+ cfg.hasCompletedOnboarding = true;
790
+ cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
791
+ cfg.numStartups = (cfg.numStartups ?? 0) + 1;
792
+ cfg.installMethod = cfg.installMethod ?? "global";
793
+ (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
794
+ (0, import_node_fs3.chmodSync)(configPath, 384);
795
+ }
796
+ function setupGitAndGh() {
797
+ const name = process.env.GIT_USER_NAME ?? "Radar Bot";
798
+ const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
799
+ const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
800
+ if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
801
+ }
802
+ function initWorkspaceIfEmpty() {
803
+ process.chdir("/workspace");
804
+ if ((0, import_node_fs3.existsSync)(".git")) {
805
+ console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
806
+ return;
807
+ }
808
+ console.log("[entrypoint] /workspace is empty \u2014 running launch-kit init");
809
+ const status = run2("launch-kit", [
810
+ "init",
811
+ `--token=${requireEnv("LS_PAT")}`,
812
+ `--org=${requireEnv("LS_ORG_SLUG")}`,
813
+ `--project=${requireEnv("LS_PROJECT_SLUG")}`,
814
+ `--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
815
+ `--dir=/workspace`
816
+ ]);
817
+ if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
818
+ }
819
+ async function maybeProvisionIngress(bundle, services, projectSlug) {
820
+ const token = bundle.cloudflareToken ?? null;
821
+ const accountId = bundle.cloudflareAccountId ?? null;
822
+ const zones = bundle.cloudflareZones ?? [];
823
+ if (!token && !accountId && zones.length === 0) return null;
824
+ if (!token || !accountId) {
825
+ fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
826
+ }
827
+ const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
828
+ let chosen = null;
829
+ if (baseDomain) {
830
+ chosen = zones.find((z) => z.name === baseDomain) ?? null;
831
+ if (!chosen) {
832
+ fail(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
833
+ }
834
+ } else if (zones.length === 1) {
835
+ chosen = { id: zones[0].id, name: zones[0].name };
836
+ } else {
837
+ fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
838
+ }
839
+ const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
840
+ console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
841
+ const result = await provisionIngress({
842
+ apiToken: token,
843
+ accountId,
844
+ zone: chosen,
845
+ tunnelName: `launch-kit-${projectSlug}`,
846
+ services: services.map((s) => ({ name: s.name, port: s.port })),
847
+ stateFile
848
+ });
849
+ for (const [name, fqdn] of Object.entries(result.hostnames)) {
850
+ console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
851
+ }
852
+ return result;
853
+ }
854
+ function spawnServiceGroup(services) {
855
+ const children = [];
856
+ let shuttingDown = false;
857
+ const killAll = (signal = "SIGTERM") => {
858
+ if (shuttingDown) return;
859
+ shuttingDown = true;
860
+ for (const c2 of children) {
861
+ try {
862
+ c2.proc.kill(signal);
863
+ } catch {
180
864
  }
181
865
  }
182
866
  };
183
- }
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 }
867
+ const prefixStream = (name, stream, sink) => {
868
+ let buf = "";
869
+ stream.setEncoding("utf8");
870
+ stream.on("data", (chunk) => {
871
+ buf += chunk;
872
+ const lines = buf.split("\n");
873
+ buf = lines.pop() ?? "";
874
+ for (const line of lines) sink.write(`[${name}] ${line}
875
+ `);
876
+ });
877
+ stream.on("end", () => {
878
+ if (buf) sink.write(`[${name}] ${buf}
879
+ `);
880
+ });
881
+ };
882
+ const signalHandlers = [];
883
+ const installSignals = () => {
884
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
885
+ const fn = () => {
886
+ console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
887
+ killAll(sig);
888
+ };
889
+ process.on(sig, fn);
890
+ signalHandlers.push({ sig, fn });
891
+ }
892
+ };
893
+ const removeSignals = () => {
894
+ for (const h of signalHandlers) process.off(h.sig, h.fn);
895
+ signalHandlers.length = 0;
189
896
  };
897
+ return new Promise((resolve3, reject) => {
898
+ let exitedCount = 0;
899
+ let firstFailure = null;
900
+ for (const spec of services) {
901
+ const args = [...spec.args, "--port", String(spec.port)];
902
+ console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
903
+ const proc = (0, import_node_child_process2.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
904
+ children.push({ spec, proc });
905
+ if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
906
+ if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
907
+ proc.on("exit", (code, signal) => {
908
+ exitedCount += 1;
909
+ const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
910
+ if (!shuttingDown && code !== 0) {
911
+ console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
912
+ if (!firstFailure) firstFailure = { name: spec.name, code, signal };
913
+ killAll();
914
+ } else {
915
+ console.log(`[entrypoint] ${label}`);
916
+ }
917
+ if (exitedCount === children.length) {
918
+ if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
919
+ else resolve3();
920
+ }
921
+ });
922
+ proc.on("error", (err) => {
923
+ console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
924
+ if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
925
+ killAll();
926
+ });
927
+ }
928
+ installSignals();
929
+ }).finally(removeSignals);
190
930
  }
191
- function readCredFile(repoRoot) {
192
- const p = path.join(repoRoot, CONFIG_FILENAME);
193
- if (!fs.existsSync(p)) return null;
931
+ async function main() {
932
+ for (const k of REQUIRED_ENV) requireEnv(k);
933
+ const bundle = await setupFromCloud();
934
+ setupClaudeCredentials();
935
+ setupGitAndGh();
936
+ initWorkspaceIfEmpty();
937
+ let services;
194
938
  try {
195
- return JSON.parse(fs.readFileSync(p, "utf-8"));
939
+ services = resolveServices();
196
940
  } catch (err) {
197
- throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
941
+ fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
942
+ }
943
+ console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
944
+ const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
945
+ if (ingress) {
946
+ process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
947
+ const radarFqdn = ingress.hostnames.radar;
948
+ if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
949
+ else if (services.some((s) => s.name === "radar")) {
950
+ fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
951
+ }
952
+ } else if (services.length > 1) {
953
+ const first = services[0];
954
+ console.warn(
955
+ `[entrypoint] \u26A0 quick mode \u2014 only the first service "${first.name}" (port ${first.port}) will be exposed via the ephemeral *.trycloudflare.com URL. Other service(s) [${services.slice(1).map((s) => s.name).join(", ")}] will run on localhost inside the container only. Connect a Cloudflare provider in LS and set LAUNCHKIT_CF_BASE_DOMAIN to expose all services with stable subdomains.`
956
+ );
957
+ if (first.name !== "radar") {
958
+ console.warn(`[entrypoint] \u26A0 first service is "${first.name}", not "radar" \u2014 quick tunneling is owned by the radar agent today, so NO external URL will be available.`);
959
+ }
960
+ }
961
+ try {
962
+ await spawnServiceGroup(services);
963
+ process.exit(0);
964
+ } catch (err) {
965
+ console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
966
+ process.exit(1);
198
967
  }
199
968
  }
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) {
204
- try {
205
- fs.chmodSync(tmp, mode);
206
- } catch {
969
+ var import_node_child_process2, import_node_fs3, import_node_path3, REQUIRED_ENV;
970
+ var init_radar_docker_init_entry = __esm({
971
+ "src/server/radar-docker-init-entry.ts"() {
972
+ "use strict";
973
+ import_node_child_process2 = require("node:child_process");
974
+ import_node_fs3 = require("node:fs");
975
+ import_node_path3 = require("node:path");
976
+ init_mcp();
977
+ init_launch_kit_services();
978
+ init_cf_ingress();
979
+ REQUIRED_ENV = [
980
+ "CLAUDE_CREDENTIALS_B64",
981
+ "LS_PAT",
982
+ "LS_ORG_SLUG",
983
+ "LS_PROJECT_SLUG"
984
+ ];
985
+ if (!process.env.VITEST) {
986
+ main().catch((err) => {
987
+ console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
988
+ process.exit(1);
989
+ });
207
990
  }
208
991
  }
209
- fs.renameSync(tmp, absPath);
992
+ });
993
+
994
+ // src/server/init-entry.ts
995
+ var import_node_child_process3 = require("node:child_process");
996
+ var crypto = __toESM(require("node:crypto"));
997
+ var fs5 = __toESM(require("node:fs"));
998
+ var import_node_http3 = require("node:http");
999
+ var import_node_https3 = require("node:https");
1000
+ var path5 = __toESM(require("node:path"));
1001
+ var readline = __toESM(require("node:readline"));
1002
+ var import_node_url2 = require("node:url");
1003
+ init_cred_shape();
1004
+
1005
+ // src/server/git-bot-config.ts
1006
+ var import_node_child_process = require("node:child_process");
1007
+ function run(cmd, args, stdio = "inherit") {
1008
+ return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
1009
+ }
1010
+ function configureGitForBot(identity) {
1011
+ if (process.env.GH_TOKEN) {
1012
+ run("gh", ["auth", "setup-git"]);
1013
+ }
1014
+ run("git", ["config", "--global", "user.name", identity.name]);
1015
+ run("git", ["config", "--global", "user.email", identity.email]);
1016
+ run("git", ["config", "--global", "init.defaultBranch", "main"]);
1017
+ run("git", ["config", "--global", "pull.rebase", "false"]);
1018
+ }
1019
+ function parseGitIdentityFlag(value, flagName = "--git-identity") {
1020
+ const m = value.match(/^\s*(.+?)\s*<\s*([^>]+?)\s*>\s*$/);
1021
+ if (!m) {
1022
+ throw new Error(`${flagName} must be in the form "Name <email>" (got: ${value})`);
1023
+ }
1024
+ return { name: m[1], email: m[2] };
210
1025
  }
211
1026
 
212
1027
  // src/server/cred-recovery.ts
213
1028
  var fs2 = __toESM(require("node:fs"));
214
1029
  var path2 = __toESM(require("node:path"));
1030
+ init_cred_shape();
215
1031
  var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
216
1032
  function migrateLegacyCredFile(targetDir, opts) {
217
1033
  const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
@@ -292,6 +1108,7 @@ function recoverCred(targetDir, opts) {
292
1108
  }
293
1109
 
294
1110
  // src/server/init-entry.ts
1111
+ init_secrets_pull();
295
1112
  init_statusline_install();
296
1113
  var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
297
1114
  var ONBOARD_SCRIPT_NAME = "onboard";
@@ -349,8 +1166,8 @@ Wired in Claude Code (.mcp.json):
349
1166
  launch-recall \u2014 restore deleted/modified files from shadow git
350
1167
 
351
1168
  Other tools (run on demand via npx):
352
- npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
353
- npx launch-pod \u2014 full pipeline UI (separate launch-pod login)
1169
+ npx launch-radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
1170
+ npx launch-sequencer \u2014 full pipeline UI (separate sequencer login)
354
1171
  npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
355
1172
  in-browser monitor. Paste the printed URL into
356
1173
  the beacon debug panel; events stream to
@@ -363,12 +1180,12 @@ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
363
1180
  `;
364
1181
  function listEntries(dir, kind) {
365
1182
  if (kind === "commands") {
366
- return fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path4.join(dir, f) }));
1183
+ return fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path5.join(dir, f) }));
367
1184
  }
368
- 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") }));
1185
+ 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") }));
369
1186
  }
370
1187
  function renderEntries(dir, kind) {
371
- if (!fs4.existsSync(dir)) return `
1188
+ if (!fs5.existsSync(dir)) return `
372
1189
  LS slash ${kind}: (scaffold dir not bundled \u2014 this is a packaging bug)
373
1190
  `;
374
1191
  const entries = listEntries(dir, kind);
@@ -378,7 +1195,7 @@ LS slash ${kind}: (none defined)
378
1195
  const names = entries.map((e) => `/${PLUGIN_ID}:${e.name}`);
379
1196
  const colWidth = Math.max(26, ...names.map((n) => n.length + 2));
380
1197
  const lines = entries.map((entry, i) => {
381
- const text = fs4.readFileSync(entry.file, "utf-8");
1198
+ const text = fs5.readFileSync(entry.file, "utf-8");
382
1199
  const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
383
1200
  const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
384
1201
  return ` ${names[i].padEnd(colWidth)} \u2014 ${desc}`;
@@ -389,8 +1206,8 @@ ${lines.join("\n")}
389
1206
  `;
390
1207
  }
391
1208
  function renderLsCommandsSection() {
392
- const base = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
393
- return renderEntries(path4.join(base, "commands"), "commands") + renderEntries(path4.join(base, "skills"), "skills");
1209
+ const base = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
1210
+ return renderEntries(path5.join(base, "commands"), "commands") + renderEntries(path5.join(base, "skills"), "skills");
394
1211
  }
395
1212
  function getLaunchKitToolsGuide() {
396
1213
  return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
@@ -418,7 +1235,7 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
418
1235
  "--guide",
419
1236
  "--no-guide"
420
1237
  ]);
421
- var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
1238
+ var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
422
1239
  function parseArgs(argv) {
423
1240
  const args = {
424
1241
  token: process.env.LS_PAT ?? null,
@@ -426,6 +1243,7 @@ function parseArgs(argv) {
426
1243
  projectSlug: null,
427
1244
  serverUrl: DEFAULT_SERVER_URL,
428
1245
  targetDir: null,
1246
+ gitIdentity: null,
429
1247
  course: null,
430
1248
  noInstall: false,
431
1249
  noOnboard: false,
@@ -529,6 +1347,14 @@ function parseArgs(argv) {
529
1347
  args.course = val;
530
1348
  continue;
531
1349
  }
1350
+ if (key === "git-identity") {
1351
+ try {
1352
+ args.gitIdentity = parseGitIdentityFlag(val);
1353
+ } catch (err) {
1354
+ fail2(err instanceof Error ? err.message : String(err));
1355
+ }
1356
+ continue;
1357
+ }
532
1358
  unknown.push(raw);
533
1359
  continue;
534
1360
  }
@@ -541,7 +1367,7 @@ function parseArgs(argv) {
541
1367
  if (unknown.length > 0) {
542
1368
  const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
543
1369
  const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
544
- fail(`Unknown argument(s): ${unknown.join(" ")}
1370
+ fail2(`Unknown argument(s): ${unknown.join(" ")}
545
1371
  Known boolean flags: ${knownBool}
546
1372
  Known key=value flags: ${knownKv}`);
547
1373
  }
@@ -594,6 +1420,12 @@ Subcommands:
594
1420
  init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
595
1421
  refresh Re-apply scaffolds + MCP entries in an already-initialized project
596
1422
  (no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
1423
+ setup-git Configure git identity + gh credential helper in one
1424
+ shot. Use in containers / CI where init isn't needed.
1425
+ \`launch-kit setup-git --identity="Name <email>"\`.
1426
+ secrets pull Fetch decrypted secrets from cloud LS and write a .env file.
1427
+ Env resolved from --env \u2192 $LS_ENV \u2192 server-side project default \u2192
1428
+ single-env auto-pick. See \`launch-kit secrets --help\`.
597
1429
  statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
598
1430
  chips (recall, chart, deck, council) get appended. Refuses to
599
1431
  create one if none exists \u2014 additive only.
@@ -618,6 +1450,12 @@ Options:
618
1450
  becomes active; re-run with a different --course
619
1451
  and --url to add another (e.g. local + staging).
620
1452
  Use \`launch-course set <name>\` to switch later.
1453
+ --git-identity="N <e>" Non-interactive git identity for service-account /
1454
+ CI / Docker runs. Configures git user.name, user.email,
1455
+ init.defaultBranch=main, pull.rebase=false; also
1456
+ wires GH_TOKEN into git's credential helper via
1457
+ \`gh auth setup-git\` when GH_TOKEN is set. Example:
1458
+ --git-identity="Radar Bot <radar@launchpod.local>".
621
1459
  --no-install Skip dependency install step (also skips the onboard
622
1460
  script \u2014 install is its prerequisite).
623
1461
  --no-onboard Skip the onboard script even when install runs.
@@ -695,7 +1533,7 @@ async function prompt(question) {
695
1533
  resolve3(answer.trim());
696
1534
  }));
697
1535
  }
698
- function fail(msg) {
1536
+ function fail2(msg) {
699
1537
  console.error(`[launch-kit] \u2717 ${msg}`);
700
1538
  process.exit(1);
701
1539
  }
@@ -711,14 +1549,14 @@ function dryNote(msg) {
711
1549
  console.log(`[launch-kit] (dry-run) ${msg}`);
712
1550
  }
713
1551
  function which(bin) {
714
- const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
1552
+ const res = (0, import_node_child_process3.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
715
1553
  if (res.status !== 0) return null;
716
1554
  return res.stdout.split(/\r?\n/)[0]?.trim() || null;
717
1555
  }
718
1556
  function preflight() {
719
1557
  const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
720
- if (nodeMajor < 18) fail(`Node.js >= 18 required (current: ${process.versions.node}).`);
721
- if (!which("git")) fail("git not found in PATH. Install git: https://git-scm.com/downloads");
1558
+ if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
1559
+ if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
722
1560
  const hasGh = which("gh") !== null;
723
1561
  ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
724
1562
  return { hasGh };
@@ -734,7 +1572,7 @@ var ProjectInfoHttpError = class extends Error {
734
1572
  };
735
1573
  function attemptProjectInfo(args) {
736
1574
  return new Promise((resolve3, reject) => {
737
- const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
1575
+ const mcpUrl = new import_node_url2.URL("/api/mcp/project", args.serverUrl);
738
1576
  const body = JSON.stringify({
739
1577
  jsonrpc: "2.0",
740
1578
  id: 1,
@@ -744,7 +1582,7 @@ function attemptProjectInfo(args) {
744
1582
  arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
745
1583
  }
746
1584
  });
747
- const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
1585
+ const requester = mcpUrl.protocol === "https:" ? import_node_https3.request : import_node_http3.request;
748
1586
  const req = requester(
749
1587
  {
750
1588
  host: mcpUrl.hostname,
@@ -852,7 +1690,7 @@ async function callProjectInfo(args) {
852
1690
  throw lastErr;
853
1691
  }
854
1692
  function gitRemoteUrl(dir) {
855
- const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
1693
+ const res = (0, import_node_child_process3.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
856
1694
  if (res.status !== 0) return null;
857
1695
  return res.stdout.trim() || null;
858
1696
  }
@@ -861,18 +1699,18 @@ function normalizeRepoUrl(url) {
861
1699
  const sshMatch = u.match(/^git@([^:]+):(.+)$/);
862
1700
  if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
863
1701
  try {
864
- const parsed = new import_node_url.URL(u);
1702
+ const parsed = new import_node_url2.URL(u);
865
1703
  return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
866
1704
  } catch {
867
1705
  return u;
868
1706
  }
869
1707
  }
870
1708
  function isGitRepo(dir) {
871
- return fs4.existsSync(path4.join(dir, ".git"));
1709
+ return fs5.existsSync(path5.join(dir, ".git"));
872
1710
  }
873
1711
  function dirIsEmpty(dir) {
874
- if (!fs4.existsSync(dir)) return true;
875
- return fs4.readdirSync(dir).length === 0;
1712
+ if (!fs5.existsSync(dir)) return true;
1713
+ return fs5.readdirSync(dir).length === 0;
876
1714
  }
877
1715
  function cloneRepo(repoUrl, targetDir, hasGh) {
878
1716
  const isGithub = /github\.com/i.test(repoUrl);
@@ -891,20 +1729,20 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
891
1729
  dryNote(`would run: ${cmd} ${args.join(" ")}`);
892
1730
  return;
893
1731
  }
894
- const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
1732
+ const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
895
1733
  if (res.status !== 0) {
896
- fail(
1734
+ fail2(
897
1735
  `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.`
898
1736
  );
899
1737
  }
900
- if (!fs4.existsSync(path4.join(targetDir, ".git"))) {
901
- fail(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
1738
+ if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
1739
+ fail2(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
902
1740
  }
903
1741
  ok(`cloned to ${targetDir}`);
904
1742
  }
905
1743
  function writeConfigFile(targetDir, cfg, courseName) {
906
1744
  recoverCred(targetDir, getRecoveryOptions());
907
- const p = path4.join(targetDir, CONFIG_FILENAME);
1745
+ const p = path5.join(targetDir, CONFIG_FILENAME);
908
1746
  const existing = readCredFile(targetDir);
909
1747
  const isNew = existing === null;
910
1748
  const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
@@ -923,15 +1761,15 @@ function getRecoveryOptions() {
923
1761
  return { dryRun: DRY_RUN, log: recoveryLog };
924
1762
  }
925
1763
  function detectExistingBootstrap(targetDir) {
926
- if (!fs4.existsSync(path4.join(targetDir, CONFIG_FILENAME))) {
1764
+ if (!fs5.existsSync(path5.join(targetDir, CONFIG_FILENAME))) {
927
1765
  return { bootstrapped: false };
928
1766
  }
929
- const mcpPath = path4.join(targetDir, ".mcp.json");
930
- if (!fs4.existsSync(mcpPath)) {
1767
+ const mcpPath = path5.join(targetDir, ".mcp.json");
1768
+ if (!fs5.existsSync(mcpPath)) {
931
1769
  return { bootstrapped: false };
932
1770
  }
933
1771
  try {
934
- const mcp = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
1772
+ const mcp = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
935
1773
  if (mcp.mcpServers?.["launch-secure"]) {
936
1774
  return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
937
1775
  }
@@ -949,13 +1787,7 @@ function buildLaunchKitMcpEntries(cfg) {
949
1787
  },
950
1788
  "launch-chart": {
951
1789
  command: "npx",
952
- args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
953
- // Tells launch-chart to also start its HTTP UI alongside the MCP, so
954
- // users can open the chart viewer at localhost:<port> while queries
955
- // hit the MCP. Without this, the MCP runs passively (queries work,
956
- // no UI). I4 deep-merge preserves user-added env keys; this default
957
- // ensures the auto-serve UX ships out of the box.
958
- env: { LAUNCH_CHART_AUTOSERVE: "1" }
1790
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
959
1791
  },
960
1792
  "launch-deck": {
961
1793
  command: "npx",
@@ -988,14 +1820,14 @@ function mergeMcpEntry(existing, ours) {
988
1820
  return merged;
989
1821
  }
990
1822
  function mergeMcpFile(targetDir, launchKitEntries) {
991
- const p = path4.join(targetDir, ".mcp.json");
992
- const hadExisting = fs4.existsSync(p);
1823
+ const p = path5.join(targetDir, ".mcp.json");
1824
+ const hadExisting = fs5.existsSync(p);
993
1825
  let existing = {};
994
1826
  if (hadExisting) {
995
1827
  try {
996
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
1828
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
997
1829
  } catch (err) {
998
- fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
1830
+ fail2(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
999
1831
  }
1000
1832
  }
1001
1833
  const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
@@ -1017,17 +1849,17 @@ function mergeMcpFile(targetDir, launchKitEntries) {
1017
1849
  dryNote(`${action} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
1018
1850
  return { status: "skipped", summary: "(dry-run)" };
1019
1851
  }
1020
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1852
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1021
1853
  const verb = hadExisting && existingServerCount > 0 ? "merged" : "wrote";
1022
1854
  ok(`${verb === "merged" ? "merged into" : "wrote"} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
1023
1855
  const total = Object.keys(launchKitEntries).length;
1024
1856
  return { status: "ok", summary: `${verb} ${total} entries` };
1025
1857
  }
1026
1858
  function detectPackageManager(repoDir) {
1027
- const pkgPath = path4.join(repoDir, "package.json");
1028
- if (!fs4.existsSync(pkgPath)) return null;
1859
+ const pkgPath = path5.join(repoDir, "package.json");
1860
+ if (!fs5.existsSync(pkgPath)) return null;
1029
1861
  try {
1030
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1862
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1031
1863
  if (typeof pkg.packageManager === "string") {
1032
1864
  const name = pkg.packageManager.split("@")[0];
1033
1865
  const match = PACKAGE_MANAGERS.find((p) => p.name === name);
@@ -1036,7 +1868,7 @@ function detectPackageManager(repoDir) {
1036
1868
  }
1037
1869
  } catch {
1038
1870
  }
1039
- const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs4.existsSync(path4.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
1871
+ const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs5.existsSync(path5.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
1040
1872
  if (matches.length === 1) {
1041
1873
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
1042
1874
  }
@@ -1045,8 +1877,8 @@ function detectPackageManager(repoDir) {
1045
1877
  return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
1046
1878
  }
1047
1879
  for (const pm of PACKAGE_MANAGERS) {
1048
- if (pm.workspaceFiles?.some((wf) => fs4.existsSync(path4.join(repoDir, wf)))) {
1049
- return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs4.existsSync(path4.join(repoDir, wf)))})` };
1880
+ if (pm.workspaceFiles?.some((wf) => fs5.existsSync(path5.join(repoDir, wf)))) {
1881
+ return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs5.existsSync(path5.join(repoDir, wf)))})` };
1050
1882
  }
1051
1883
  }
1052
1884
  const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
@@ -1055,8 +1887,8 @@ function detectPackageManager(repoDir) {
1055
1887
  function runInstall(repoDir, detected) {
1056
1888
  const { pm } = detected;
1057
1889
  if (!which(pm.binary)) {
1058
- fail(
1059
- `${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(" ")}`
1890
+ fail2(
1891
+ `${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(" ")}`
1060
1892
  );
1061
1893
  }
1062
1894
  info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
@@ -1064,24 +1896,24 @@ function runInstall(repoDir, detected) {
1064
1896
  dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
1065
1897
  return;
1066
1898
  }
1067
- const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
1899
+ const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
1068
1900
  if (res.status !== 0) {
1069
- fail(
1901
+ fail2(
1070
1902
  `${pm.name} install failed (exit ${res.status}).
1071
1903
 
1072
1904
  Half-init state \u2014 install didn't complete, but these files ARE on disk:
1073
- - ${path4.join(repoDir, CONFIG_FILENAME)} (cred file)
1074
- - ${path4.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
1075
- - ${path4.join(repoDir, ".gitignore")} (cred line appended)
1905
+ - ${path5.join(repoDir, CONFIG_FILENAME)} (cred file)
1906
+ - ${path5.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
1907
+ - ${path5.join(repoDir, ".gitignore")} (cred line appended)
1076
1908
  - clone at ${repoDir}
1077
1909
 
1078
1910
  Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
1079
1911
 
1080
1912
  To retry install only:
1081
- cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
1913
+ cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
1082
1914
 
1083
1915
  To re-run init after fixing the install error:
1084
- cd ${path4.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
1916
+ cd ${path5.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
1085
1917
 
1086
1918
  To fully reset: delete the files listed above and the clone, then re-init.`
1087
1919
  );
@@ -1089,10 +1921,10 @@ To fully reset: delete the files listed above and the clone, then re-init.`
1089
1921
  ok(`${pm.name} install complete`);
1090
1922
  }
1091
1923
  function hasOnboardScript(repoDir) {
1092
- const pkgPath = path4.join(repoDir, "package.json");
1093
- if (!fs4.existsSync(pkgPath)) return false;
1924
+ const pkgPath = path5.join(repoDir, "package.json");
1925
+ if (!fs5.existsSync(pkgPath)) return false;
1094
1926
  try {
1095
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1927
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1096
1928
  return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
1097
1929
  } catch {
1098
1930
  return false;
@@ -1100,17 +1932,17 @@ function hasOnboardScript(repoDir) {
1100
1932
  }
1101
1933
  function runRecallInit(repoDir) {
1102
1934
  info(`scaffolding launch-recall (shadow git backup) \u2026`);
1103
- const recallEntry = path4.resolve(__dirname, "recall-entry.js");
1104
- const useSibling = fs4.existsSync(recallEntry);
1935
+ const recallEntry = path5.resolve(__dirname, "recall-entry.js");
1936
+ const useSibling = fs5.existsSync(recallEntry);
1105
1937
  const cmd = useSibling ? process.execPath : "npx";
1106
1938
  const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
1107
1939
  if (DRY_RUN) {
1108
1940
  dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
1109
1941
  return;
1110
1942
  }
1111
- const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
1943
+ const res = (0, import_node_child_process3.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
1112
1944
  if (res.status !== 0) {
1113
- 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`);
1945
+ 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`);
1114
1946
  return;
1115
1947
  }
1116
1948
  ok(`launch-recall ready (shadow git initialized)`);
@@ -1121,17 +1953,17 @@ function runOnboard(repoDir, pm) {
1121
1953
  dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
1122
1954
  return;
1123
1955
  }
1124
- const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
1956
+ const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
1125
1957
  if (res.status !== 0) {
1126
- fail(
1127
- `${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}`
1958
+ fail2(
1959
+ `${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}`
1128
1960
  );
1129
1961
  }
1130
1962
  ok(`${ONBOARD_SCRIPT_NAME} script complete`);
1131
1963
  }
1132
1964
  function ensureGitignoreLine(targetDir, line) {
1133
- const p = path4.join(targetDir, ".gitignore");
1134
- let content = fs4.existsSync(p) ? fs4.readFileSync(p, "utf-8") : "";
1965
+ const p = path5.join(targetDir, ".gitignore");
1966
+ let content = fs5.existsSync(p) ? fs5.readFileSync(p, "utf-8") : "";
1135
1967
  const lines = content.split(/\r?\n/);
1136
1968
  if (lines.some((l) => l.trim() === line)) return;
1137
1969
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -1141,28 +1973,28 @@ function ensureGitignoreLine(targetDir, line) {
1141
1973
  dryNote(`would append "${line}" to .gitignore`);
1142
1974
  return;
1143
1975
  }
1144
- fs4.writeFileSync(p, content, "utf-8");
1976
+ fs5.writeFileSync(p, content, "utf-8");
1145
1977
  ok(`appended ${line} to .gitignore`);
1146
1978
  }
1147
1979
  function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
1148
- if (!fs4.existsSync(srcDir)) return;
1149
- for (const entry of fs4.readdirSync(srcDir, { withFileTypes: true })) {
1150
- const srcPath = path4.join(srcDir, entry.name);
1151
- const destPath = path4.join(destDir, entry.name);
1980
+ if (!fs5.existsSync(srcDir)) return;
1981
+ for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) {
1982
+ const srcPath = path5.join(srcDir, entry.name);
1983
+ const destPath = path5.join(destDir, entry.name);
1152
1984
  const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
1153
1985
  if (entry.isDirectory()) {
1154
1986
  copyScaffoldDirAlways(srcPath, destPath, label);
1155
1987
  } else if (entry.isFile()) {
1156
- const existed = fs4.existsSync(destPath);
1988
+ const existed = fs5.existsSync(destPath);
1157
1989
  if (DRY_RUN) {
1158
1990
  dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
1159
1991
  continue;
1160
1992
  }
1161
- fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1162
- fs4.copyFileSync(srcPath, destPath);
1993
+ fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
1994
+ fs5.copyFileSync(srcPath, destPath);
1163
1995
  try {
1164
- const srcMode = fs4.statSync(srcPath).mode;
1165
- fs4.chmodSync(destPath, srcMode);
1996
+ const srcMode = fs5.statSync(srcPath).mode;
1997
+ fs5.chmodSync(destPath, srcMode);
1166
1998
  } catch {
1167
1999
  }
1168
2000
  ok(`${existed ? "refreshed" : "wrote"} ${label}`);
@@ -1170,25 +2002,25 @@ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
1170
2002
  }
1171
2003
  }
1172
2004
  function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
1173
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
1174
- if (!fs4.existsSync(scaffoldsRoot)) {
2005
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
2006
+ if (!fs5.existsSync(scaffoldsRoot)) {
1175
2007
  warn(`migrate-safety scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug; main onboarding unaffected`);
1176
2008
  return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1177
2009
  }
1178
2010
  const files = [
1179
2011
  {
1180
- src: path4.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
1181
- dest: path4.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
2012
+ src: path5.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
2013
+ dest: path5.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
1182
2014
  label: ".github/workflows/backup-on-migration.yml"
1183
2015
  },
1184
2016
  {
1185
- src: path4.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
1186
- dest: path4.join(targetDir, "scripts", "migrate-with-backup.sh"),
2017
+ src: path5.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
2018
+ dest: path5.join(targetDir, "scripts", "migrate-with-backup.sh"),
1187
2019
  label: "scripts/migrate-with-backup.sh"
1188
2020
  },
1189
2021
  {
1190
- src: path4.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
1191
- dest: path4.join(targetDir, "docs", "migrations-runbook.md"),
2022
+ src: path5.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
2023
+ dest: path5.join(targetDir, "docs", "migrations-runbook.md"),
1192
2024
  label: "docs/migrations-runbook.md"
1193
2025
  }
1194
2026
  ];
@@ -1223,25 +2055,25 @@ function summarizeFileCounts(counts) {
1223
2055
  }
1224
2056
  function hashFile(p) {
1225
2057
  try {
1226
- return crypto.createHash("sha256").update(fs4.readFileSync(p)).digest("hex");
2058
+ return crypto.createHash("sha256").update(fs5.readFileSync(p)).digest("hex");
1227
2059
  } catch {
1228
2060
  return null;
1229
2061
  }
1230
2062
  }
1231
2063
  function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1232
- if (!fs4.existsSync(srcPath)) {
2064
+ if (!fs5.existsSync(srcPath)) {
1233
2065
  info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
1234
2066
  return "missing-src";
1235
2067
  }
1236
- if (!fs4.existsSync(destPath)) {
2068
+ if (!fs5.existsSync(destPath)) {
1237
2069
  if (DRY_RUN) {
1238
2070
  dryNote(`would write ${label}`);
1239
2071
  return "wrote";
1240
2072
  }
1241
- fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
1242
- fs4.copyFileSync(srcPath, destPath);
2073
+ fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
2074
+ fs5.copyFileSync(srcPath, destPath);
1243
2075
  try {
1244
- fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
2076
+ fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
1245
2077
  } catch {
1246
2078
  }
1247
2079
  ok(`wrote ${label}`);
@@ -1259,9 +2091,9 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1259
2091
  dryNote(`would refresh ${label} (overrides local edits)`);
1260
2092
  return "drifted-refreshed";
1261
2093
  }
1262
- fs4.copyFileSync(srcPath, destPath);
2094
+ fs5.copyFileSync(srcPath, destPath);
1263
2095
  try {
1264
- fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
2096
+ fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
1265
2097
  } catch {
1266
2098
  }
1267
2099
  ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
@@ -1273,10 +2105,10 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
1273
2105
  var MARKETPLACE_ID = "launch-secure";
1274
2106
  var PLUGIN_ID = "kit";
1275
2107
  function isDogfoodMarketplace(targetDir) {
1276
- const p = path4.join(targetDir, ".claude", "settings.json");
1277
- if (!fs4.existsSync(p)) return { isDogfood: false };
2108
+ const p = path5.join(targetDir, ".claude", "settings.json");
2109
+ if (!fs5.existsSync(p)) return { isDogfood: false };
1278
2110
  try {
1279
- const settings = JSON.parse(fs4.readFileSync(p, "utf-8"));
2111
+ const settings = JSON.parse(fs5.readFileSync(p, "utf-8"));
1280
2112
  const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
1281
2113
  if (existingPath && existingPath !== "./.claude/marketplace") {
1282
2114
  return { isDogfood: true, existingPath };
@@ -1287,8 +2119,8 @@ function isDogfoodMarketplace(targetDir) {
1287
2119
  }
1288
2120
  }
1289
2121
  function scaffoldLsMarketplace(targetDir) {
1290
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
1291
- if (!fs4.existsSync(scaffoldsRoot)) {
2122
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
2123
+ if (!fs5.existsSync(scaffoldsRoot)) {
1292
2124
  warn(`ls-marketplace scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
1293
2125
  return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1294
2126
  }
@@ -1299,7 +2131,7 @@ function scaffoldLsMarketplace(targetDir) {
1299
2131
  ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
1300
2132
  return { status: "ok", summary: `dogfood pointer (${dogfood.existingPath})` };
1301
2133
  }
1302
- const marketplaceRoot = path4.join(targetDir, ".claude", "marketplace");
2134
+ const marketplaceRoot = path5.join(targetDir, ".claude", "marketplace");
1303
2135
  info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
1304
2136
  copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
1305
2137
  wireLsSettings(targetDir);
@@ -1307,14 +2139,14 @@ function scaffoldLsMarketplace(targetDir) {
1307
2139
  return { status: "ok", summary: "marketplace tree + settings refreshed" };
1308
2140
  }
1309
2141
  function wireLsSettings(targetDir) {
1310
- const p = path4.join(targetDir, ".claude", "settings.json");
1311
- const hadExisting = fs4.existsSync(p);
2142
+ const p = path5.join(targetDir, ".claude", "settings.json");
2143
+ const hadExisting = fs5.existsSync(p);
1312
2144
  let existing = {};
1313
2145
  if (hadExisting) {
1314
2146
  try {
1315
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
2147
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
1316
2148
  } catch (err) {
1317
- fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
2149
+ fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1318
2150
  }
1319
2151
  }
1320
2152
  const merged = { ...existing };
@@ -1342,15 +2174,15 @@ function wireLsSettings(targetDir) {
1342
2174
  dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
1343
2175
  return;
1344
2176
  }
1345
- fs4.mkdirSync(path4.dirname(p), { recursive: true });
1346
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2177
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
2178
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1347
2179
  ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
1348
2180
  }
1349
2181
  var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
1350
2182
  var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
1351
2183
  function scaffoldRecallHook(targetDir) {
1352
- const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
1353
- if (!fs4.existsSync(scaffoldsRoot)) {
2184
+ const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
2185
+ if (!fs5.existsSync(scaffoldsRoot)) {
1354
2186
  warn(`recall-hook scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
1355
2187
  return { status: "warn", summary: "scaffolds missing (packaging bug)" };
1356
2188
  }
@@ -1361,14 +2193,14 @@ function scaffoldRecallHook(targetDir) {
1361
2193
  return wired ? { status: "ok", summary: "ensure-recall.sh refreshed + SessionStart hook appended" } : { status: "ok", summary: "ensure-recall.sh refreshed (hook already wired)" };
1362
2194
  }
1363
2195
  function wireRecallHook(targetDir) {
1364
- const p = path4.join(targetDir, ".claude", "settings.json");
1365
- const hadExisting = fs4.existsSync(p);
2196
+ const p = path5.join(targetDir, ".claude", "settings.json");
2197
+ const hadExisting = fs5.existsSync(p);
1366
2198
  let existing = {};
1367
2199
  if (hadExisting) {
1368
2200
  try {
1369
- existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
2201
+ existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
1370
2202
  } catch (err) {
1371
- fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
2203
+ fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
1372
2204
  }
1373
2205
  }
1374
2206
  const hooks = existing.hooks ?? {};
@@ -1394,8 +2226,8 @@ function wireRecallHook(targetDir) {
1394
2226
  dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
1395
2227
  return true;
1396
2228
  }
1397
- fs4.mkdirSync(path4.dirname(p), { recursive: true });
1398
- fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2229
+ fs5.mkdirSync(path5.dirname(p), { recursive: true });
2230
+ fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1399
2231
  ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
1400
2232
  return true;
1401
2233
  }
@@ -1415,8 +2247,35 @@ function tryActivateStatusline() {
1415
2247
  }
1416
2248
  return null;
1417
2249
  }
1418
- async function main() {
2250
+ async function main2() {
1419
2251
  const subcommand = process.argv[2];
2252
+ if (subcommand === "radar-docker-init") {
2253
+ await Promise.resolve().then(() => (init_radar_docker_init_entry(), radar_docker_init_entry_exports));
2254
+ return;
2255
+ }
2256
+ if (subcommand === "setup-git") {
2257
+ if (process.argv[3] === "--help" || process.argv[3] === "-h") {
2258
+ console.log('usage: launch-kit setup-git --identity="Name <email>"');
2259
+ console.log("");
2260
+ console.log(" Runs `gh auth setup-git` when GH_TOKEN is set, then sets the");
2261
+ console.log(" global git user.name, user.email, init.defaultBranch=main, and");
2262
+ console.log(" pull.rebase=false. Idempotent; safe to re-run.");
2263
+ return;
2264
+ }
2265
+ let identityVal = null;
2266
+ for (const a of process.argv.slice(3)) {
2267
+ if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
2268
+ else fail2(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
2269
+ }
2270
+ if (!identityVal) fail2(`launch-kit setup-git requires --identity="Name <email>".`);
2271
+ try {
2272
+ const identity = parseGitIdentityFlag(identityVal, "--identity");
2273
+ configureGitForBot(identity);
2274
+ } catch (err) {
2275
+ fail2(err instanceof Error ? err.message : String(err));
2276
+ }
2277
+ return;
2278
+ }
1420
2279
  if (subcommand === "statusline") {
1421
2280
  const action = process.argv[3];
1422
2281
  if (!action || action === "--help" || action === "-h") {
@@ -1433,17 +2292,50 @@ async function main() {
1433
2292
  for (const a of process.argv.slice(4)) {
1434
2293
  if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
1435
2294
  else if (a === "--compact") compactArg = true;
1436
- else fail(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
2295
+ else fail2(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
1437
2296
  }
1438
2297
  const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
1439
2298
  let res;
1440
2299
  if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
1441
2300
  else if (action === "deactivate") res = deactivateStatusline2();
1442
- else fail(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
2301
+ else fail2(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
1443
2302
  if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
1444
2303
  else info(`statusline ${res.outcome} \u2014 ${res.message}`);
1445
2304
  return;
1446
2305
  }
2306
+ if (subcommand === "secrets") {
2307
+ const action = process.argv[3];
2308
+ if (!action || action === "--help" || action === "-h") {
2309
+ console.log("usage: launch-kit secrets pull [--env=<slug>] [--dir=<path>] [--file=<name>]");
2310
+ console.log("");
2311
+ console.log(" --env=<slug> env to pull from. Overrides $LS_ENV and the project-level default.");
2312
+ console.log(" --dir=<path> project root (default: cwd). Must contain a launch-kit cred file.");
2313
+ console.log(" --file=<name> output file relative to --dir (default: .env.local).");
2314
+ console.log("");
2315
+ console.log(" Resolution order: --env \u2192 $LS_ENV \u2192 server-side default \u2192 single-env auto-pick.");
2316
+ return;
2317
+ }
2318
+ if (action !== "pull") {
2319
+ fail2(`Unknown secrets action: "${action}". Supported: pull.`);
2320
+ }
2321
+ let envOverride = null;
2322
+ let dirArg = null;
2323
+ let fileArg = ".env.local";
2324
+ for (const a of process.argv.slice(4)) {
2325
+ if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
2326
+ else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
2327
+ else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
2328
+ else fail2(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
2329
+ }
2330
+ const targetDir = path5.resolve(dirArg ?? process.cwd());
2331
+ const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
2332
+ try {
2333
+ await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
2334
+ } catch (err) {
2335
+ fail2(err instanceof Error ? err.message : String(err));
2336
+ }
2337
+ return;
2338
+ }
1447
2339
  const args = parseArgs(process.argv.slice(2));
1448
2340
  if (args.help) {
1449
2341
  if (subcommand === "refresh") printRefreshHelp();
@@ -1451,10 +2343,10 @@ async function main() {
1451
2343
  return;
1452
2344
  }
1453
2345
  if (!subcommand || subcommand.startsWith("--")) {
1454
- fail(`missing subcommand. Usage: launch-kit <init|refresh|statusline> [options]. Run with --help.`);
2346
+ fail2(`missing subcommand. Usage: launch-kit <init|refresh|statusline|secrets> [options]. Run with --help.`);
1455
2347
  }
1456
2348
  if (subcommand !== "init" && subcommand !== "refresh") {
1457
- fail(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline. Run with --help for usage.`);
2349
+ fail2(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline, secrets. Run with --help for usage.`);
1458
2350
  }
1459
2351
  DRY_RUN = args.dryRun;
1460
2352
  VERBOSE = args.verbose || DRY_RUN;
@@ -1469,8 +2361,8 @@ async function main() {
1469
2361
  }
1470
2362
  async function mainRefresh(args) {
1471
2363
  const cwd = process.cwd();
1472
- const targetDir = path4.resolve(args.targetDir ?? cwd);
1473
- if (!fs4.existsSync(targetDir)) fail(`target dir does not exist: ${targetDir}`);
2364
+ const targetDir = path5.resolve(args.targetDir ?? cwd);
2365
+ if (!fs5.existsSync(targetDir)) fail2(`target dir does not exist: ${targetDir}`);
1474
2366
  let cred;
1475
2367
  let source;
1476
2368
  try {
@@ -1478,7 +2370,7 @@ async function mainRefresh(args) {
1478
2370
  cred = recovery.cred;
1479
2371
  source = recovery.source;
1480
2372
  } catch (err) {
1481
- fail(err instanceof Error ? err.message : String(err));
2373
+ fail2(err instanceof Error ? err.message : String(err));
1482
2374
  }
1483
2375
  if (cred && source === "mcp") {
1484
2376
  info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
@@ -1487,24 +2379,24 @@ async function mainRefresh(args) {
1487
2379
  if (DRY_RUN) {
1488
2380
  dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
1489
2381
  } else {
1490
- writeJsonAtomic(path4.join(targetDir, CONFIG_FILENAME), nested2, 384);
2382
+ writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
1491
2383
  ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
1492
2384
  }
1493
2385
  }
1494
2386
  if (!cred) {
1495
- fail(
1496
- `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.`
2387
+ fail2(
2388
+ `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.`
1497
2389
  );
1498
2390
  }
1499
2391
  const nested = toNested(cred);
1500
- if (!nested) fail(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
2392
+ if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
1501
2393
  const active = nested.profiles[nested.active];
1502
- if (!active) fail(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
2394
+ if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
1503
2395
  info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
1504
2396
  header("launch-kit refresh", [
1505
2397
  ["course", nested.active],
1506
2398
  ["project", `${active.orgSlug}/${active.projectSlug}`],
1507
- ["dir", path4.relative(cwd, targetDir) || "."]
2399
+ ["dir", path5.relative(cwd, targetDir) || "."]
1508
2400
  ]);
1509
2401
  const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
1510
2402
  phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
@@ -1532,8 +2424,8 @@ async function mainRefresh(args) {
1532
2424
  }
1533
2425
  }
1534
2426
  async function mainInit(args) {
1535
- const probeDir = path4.resolve(args.targetDir ?? process.cwd());
1536
- if (!args.force && fs4.existsSync(probeDir)) {
2427
+ const probeDir = path5.resolve(args.targetDir ?? process.cwd());
2428
+ if (!args.force && fs5.existsSync(probeDir)) {
1537
2429
  const detection = detectExistingBootstrap(probeDir);
1538
2430
  if (detection.bootstrapped) {
1539
2431
  info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
@@ -1542,8 +2434,8 @@ async function mainInit(args) {
1542
2434
  }
1543
2435
  }
1544
2436
  if (!args.token || !args.orgSlug || !args.projectSlug) {
1545
- const recoveryDir = path4.resolve(args.targetDir ?? process.cwd());
1546
- if (fs4.existsSync(recoveryDir)) {
2437
+ const recoveryDir = path5.resolve(args.targetDir ?? process.cwd());
2438
+ if (fs5.existsSync(recoveryDir)) {
1547
2439
  const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
1548
2440
  const nested = cred ? toNested(cred) : null;
1549
2441
  const recovered = nested ? nested.profiles[nested.active] : cred;
@@ -1570,10 +2462,10 @@ async function mainInit(args) {
1570
2462
  const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
1571
2463
  args.token = t || null;
1572
2464
  }
1573
- if (!args.token) fail("--token (or LS_PAT env) is required.");
1574
- if (!/^ls_pat_/.test(args.token)) fail("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
1575
- if (!args.orgSlug) fail("--org=<orgSlug> is required.");
1576
- if (!args.projectSlug) fail("--project=<projectSlug> is required.");
2465
+ if (!args.token) fail2("--token (or LS_PAT env) is required.");
2466
+ if (!/^ls_pat_/.test(args.token)) fail2("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
2467
+ if (!args.orgSlug) fail2("--org=<orgSlug> is required.");
2468
+ if (!args.projectSlug) fail2("--project=<projectSlug> is required.");
1577
2469
  header("launch-kit init", [
1578
2470
  ["org", args.orgSlug],
1579
2471
  ["project", args.projectSlug],
@@ -1581,39 +2473,43 @@ async function mainInit(args) {
1581
2473
  ]);
1582
2474
  const { hasGh } = preflight();
1583
2475
  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)"}` });
2476
+ if (args.gitIdentity) {
2477
+ configureGitForBot(args.gitIdentity);
2478
+ phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 gh credential helper wired" : ""}` });
2479
+ }
1584
2480
  info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
1585
2481
  let resolved;
1586
2482
  try {
1587
2483
  resolved = await callProjectInfo(args);
1588
2484
  } catch (err) {
1589
- fail(err instanceof Error ? err.message : String(err));
2485
+ fail2(err instanceof Error ? err.message : String(err));
1590
2486
  }
1591
2487
  ok(`resolved "${resolved.projectName}"`);
1592
2488
  phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
1593
2489
  if (!resolved.repositoryUrl) {
1594
- fail(
2490
+ fail2(
1595
2491
  `Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
1596
2492
  );
1597
2493
  }
1598
2494
  const repoUrl = resolved.repositoryUrl;
1599
2495
  const cwd = process.cwd();
1600
- const targetDir = path4.resolve(args.targetDir ?? path4.join(cwd, resolved.projectSlug));
2496
+ const targetDir = path5.resolve(args.targetDir ?? path5.join(cwd, resolved.projectSlug));
1601
2497
  const normalizedRemote = normalizeRepoUrl(repoUrl);
1602
2498
  let skipClone = false;
1603
- if (fs4.existsSync(targetDir)) {
2499
+ if (fs5.existsSync(targetDir)) {
1604
2500
  if (isGitRepo(targetDir)) {
1605
2501
  const existingRemote = gitRemoteUrl(targetDir);
1606
2502
  if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
1607
2503
  ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
1608
2504
  skipClone = true;
1609
2505
  } else {
1610
- fail(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
2506
+ fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
1611
2507
  }
1612
2508
  } else if (!dirIsEmpty(targetDir)) {
1613
- fail(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
2509
+ fail2(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
1614
2510
  }
1615
2511
  }
1616
- const relTarget = path4.relative(cwd, targetDir) || ".";
2512
+ const relTarget = path5.relative(cwd, targetDir) || ".";
1617
2513
  if (!skipClone) {
1618
2514
  section(`Cloning ${repoUrl}`);
1619
2515
  cloneRepo(repoUrl, targetDir, hasGh);
@@ -1643,10 +2539,28 @@ async function mainInit(args) {
1643
2539
  section(`Installing dependencies (${detected.pm.name})`);
1644
2540
  runInstall(targetDir, detected);
1645
2541
  phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
1646
- if (!args.noOnboard && hasOnboardScript(targetDir)) {
1647
- section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
1648
- runOnboard(targetDir, detected.pm);
1649
- phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
2542
+ if (!args.noOnboard) {
2543
+ if (hasOnboardScript(targetDir)) {
2544
+ section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
2545
+ runOnboard(targetDir, detected.pm);
2546
+ phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
2547
+ } else {
2548
+ section("Pulling environment secrets");
2549
+ info("running launch-kit secrets pull \u2026");
2550
+ if (DRY_RUN) {
2551
+ dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
2552
+ phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
2553
+ } else {
2554
+ try {
2555
+ await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
2556
+ phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
2557
+ } catch (err) {
2558
+ const msg = err instanceof Error ? err.message : String(err);
2559
+ warn(`secrets pull skipped \u2014 ${msg}`);
2560
+ phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
2561
+ }
2562
+ }
2563
+ }
1650
2564
  }
1651
2565
  }
1652
2566
  const hasOnboard = hasOnboardScript(targetDir);
@@ -1686,7 +2600,7 @@ async function mainInit(args) {
1686
2600
  ]);
1687
2601
  if (showGuide) console.log(getLaunchKitToolsGuide());
1688
2602
  }
1689
- main().catch((err) => {
2603
+ main2().catch((err) => {
1690
2604
  console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
1691
2605
  process.exit(1);
1692
2606
  });