@launchsecure/launch-kit 0.0.27 → 0.0.28

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 (45) hide show
  1. package/dist/beacon/beacon.mjs +1003 -440
  2. package/dist/beacon/beacon.mjs.map +1 -1
  3. package/dist/beacon/beacon.umd.js +45 -24
  4. package/dist/beacon/beacon.umd.js.map +1 -1
  5. package/dist/beacon/types/capture/events.d.ts +20 -0
  6. package/dist/beacon/types/capture/events.d.ts.map +1 -0
  7. package/dist/beacon/types/element.d.ts +1 -0
  8. package/dist/beacon/types/element.d.ts.map +1 -1
  9. package/dist/beacon/types/index.d.ts +2 -1
  10. package/dist/beacon/types/index.d.ts.map +1 -1
  11. package/dist/beacon/types/monitor/dom.d.ts +13 -0
  12. package/dist/beacon/types/monitor/dom.d.ts.map +1 -0
  13. package/dist/beacon/types/monitor/index.d.ts +19 -0
  14. package/dist/beacon/types/monitor/index.d.ts.map +1 -0
  15. package/dist/beacon/types/monitor/network.d.ts +12 -0
  16. package/dist/beacon/types/monitor/network.d.ts.map +1 -0
  17. package/dist/beacon/types/monitor/transport.d.ts +27 -0
  18. package/dist/beacon/types/monitor/transport.d.ts.map +1 -0
  19. package/dist/beacon/types/monitor/types.d.ts +117 -0
  20. package/dist/beacon/types/monitor/types.d.ts.map +1 -0
  21. package/dist/beacon/types/types.d.ts +10 -0
  22. package/dist/beacon/types/types.d.ts.map +1 -1
  23. package/dist/beacon/types/ui/drawer.d.ts +3 -1
  24. package/dist/beacon/types/ui/drawer.d.ts.map +1 -1
  25. package/dist/beacon/types/ui/monitor-panel.d.ts +19 -0
  26. package/dist/beacon/types/ui/monitor-panel.d.ts.map +1 -0
  27. package/dist/server/beacon-monitor-entry.js +353 -0
  28. package/dist/server/cli.js +50 -2
  29. package/dist/server/council-entry.js +0 -0
  30. package/dist/server/course-entry.js +246 -0
  31. package/dist/server/fb-wizard.js +0 -0
  32. package/dist/server/init-entry.js +394 -64
  33. package/dist/server/orbit-entry.js +187 -24
  34. package/package.json +24 -23
  35. package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
  36. package/scaffolds/ls-marketplace/plugins/ls/.claude-plugin/plugin.json +28 -0
  37. package/scaffolds/ls-marketplace/plugins/ls/commands/activate-beacon.md +216 -0
  38. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-array.md +92 -0
  39. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-clear.md +68 -0
  40. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-pulse.md +80 -0
  41. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-scan.md +62 -0
  42. package/scaffolds/ls-marketplace/plugins/ls/commands/show-mcp-status.md +109 -0
  43. package/scaffolds/ls-marketplace/plugins/ls/commands/standup.md +177 -0
  44. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  45. package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -0
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/server/beacon-monitor-entry.ts
5
+ var import_node_crypto = require("node:crypto");
6
+ var import_node_path2 = require("node:path");
7
+
8
+ // src/server/beacon/ndjson-writer.ts
9
+ var import_node_fs = require("node:fs");
10
+ var import_node_path = require("node:path");
11
+ function openNdjsonWriter(opts) {
12
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(opts.path), { recursive: true });
13
+ const stream = (0, import_node_fs.createWriteStream)(opts.path, { flags: "a" });
14
+ let total = 0;
15
+ let closed = false;
16
+ stream.on("error", (err) => {
17
+ opts.onError?.(err);
18
+ });
19
+ return {
20
+ write(event) {
21
+ if (closed) return;
22
+ let line;
23
+ try {
24
+ line = JSON.stringify(event);
25
+ } catch (err) {
26
+ opts.onError?.(err instanceof Error ? err : new Error(String(err)));
27
+ return;
28
+ }
29
+ stream.write(line + "\n");
30
+ total += 1;
31
+ },
32
+ count() {
33
+ return total;
34
+ },
35
+ path() {
36
+ return opts.path;
37
+ },
38
+ close() {
39
+ return new Promise((resolveClose) => {
40
+ if (closed) {
41
+ resolveClose();
42
+ return;
43
+ }
44
+ closed = true;
45
+ stream.end(() => resolveClose());
46
+ });
47
+ }
48
+ };
49
+ }
50
+
51
+ // src/server/beacon/server.ts
52
+ var import_node_http = require("node:http");
53
+ var MAX_BODY_BYTES = 1048576;
54
+ var TOKEN_PATH_PREFIX = "/m/";
55
+ function writeCors(res, origin) {
56
+ res.setHeader("Access-Control-Allow-Origin", origin && origin !== "null" ? origin : "*");
57
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
58
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
59
+ res.setHeader("Access-Control-Max-Age", "600");
60
+ res.setHeader("Vary", "Origin");
61
+ }
62
+ function readBody(req) {
63
+ return new Promise((resolve2, reject) => {
64
+ const chunks = [];
65
+ let total = 0;
66
+ req.on("data", (chunk) => {
67
+ total += chunk.length;
68
+ if (total > MAX_BODY_BYTES) {
69
+ reject(new Error("payload too large"));
70
+ req.destroy();
71
+ return;
72
+ }
73
+ chunks.push(chunk);
74
+ });
75
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
76
+ req.on("error", (err) => reject(err));
77
+ });
78
+ }
79
+ function isMonitorBatchEnvelope(value) {
80
+ if (!value || typeof value !== "object") return false;
81
+ const v = value;
82
+ if (typeof v.sessionId !== "string") return false;
83
+ if (!Array.isArray(v.events)) return false;
84
+ if (!v.meta || typeof v.meta !== "object") return false;
85
+ const meta = v.meta;
86
+ if (typeof meta.url !== "string" || typeof meta.userAgent !== "string") return false;
87
+ return true;
88
+ }
89
+ function startBeaconServer(opts) {
90
+ return new Promise((resolveStart, rejectStart) => {
91
+ const server = (0, import_node_http.createServer)(async (req, res) => {
92
+ writeCors(res, req.headers.origin);
93
+ if (req.method === "OPTIONS") {
94
+ res.writeHead(204);
95
+ res.end();
96
+ return;
97
+ }
98
+ if (req.method !== "POST") {
99
+ res.writeHead(405, { "content-type": "text/plain" });
100
+ res.end("method not allowed");
101
+ return;
102
+ }
103
+ const url = req.url ?? "/";
104
+ const pathOnly = url.split("?")[0] ?? "/";
105
+ if (!pathOnly.startsWith(TOKEN_PATH_PREFIX)) {
106
+ res.writeHead(404, { "content-type": "text/plain" });
107
+ res.end("not found");
108
+ return;
109
+ }
110
+ const provided = pathOnly.slice(TOKEN_PATH_PREFIX.length).replace(/\/+$/, "");
111
+ if (provided !== opts.token) {
112
+ res.writeHead(403, { "content-type": "text/plain" });
113
+ res.end("bad token");
114
+ return;
115
+ }
116
+ let bodyText;
117
+ try {
118
+ bodyText = await readBody(req);
119
+ } catch (err) {
120
+ opts.onBadBatch?.(`body read failed: ${err.message}`, req);
121
+ res.writeHead(413, { "content-type": "text/plain" });
122
+ res.end("payload error");
123
+ return;
124
+ }
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(bodyText);
128
+ } catch {
129
+ opts.onBadBatch?.("invalid JSON", req);
130
+ res.writeHead(400, { "content-type": "text/plain" });
131
+ res.end("invalid json");
132
+ return;
133
+ }
134
+ if (!isMonitorBatchEnvelope(parsed)) {
135
+ opts.onBadBatch?.("schema mismatch", req);
136
+ res.writeHead(400, { "content-type": "text/plain" });
137
+ res.end("bad batch shape");
138
+ return;
139
+ }
140
+ try {
141
+ opts.onBatch(parsed, req);
142
+ } catch (err) {
143
+ opts.onServerError?.(err instanceof Error ? err : new Error(String(err)));
144
+ }
145
+ res.writeHead(204);
146
+ res.end();
147
+ });
148
+ server.on("error", (err) => {
149
+ opts.onServerError?.(err);
150
+ rejectStart(err);
151
+ });
152
+ server.listen(opts.port, opts.host, () => {
153
+ const addr = server.address();
154
+ const port = addr ? addr.port : opts.port;
155
+ const url = `http://${opts.host}:${port}/m/${opts.token}`;
156
+ resolveStart({
157
+ url,
158
+ port,
159
+ close() {
160
+ return new Promise((resolveClose) => {
161
+ server.close(() => resolveClose());
162
+ });
163
+ }
164
+ });
165
+ });
166
+ });
167
+ }
168
+
169
+ // src/server/beacon-monitor-entry.ts
170
+ function parseMonitorFlags(argv) {
171
+ const flags = { port: 9876, host: "127.0.0.1", quiet: false };
172
+ for (let i = 0; i < argv.length; i++) {
173
+ const a = argv[i];
174
+ if (a === "--port") {
175
+ const n = Number(argv[++i]);
176
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
177
+ die(`--port must be an integer in 0..65535 (got "${argv[i]}")`);
178
+ }
179
+ flags.port = n;
180
+ } else if (a === "--host") {
181
+ const v = argv[++i];
182
+ if (!v) die("--host requires a value");
183
+ flags.host = v;
184
+ } else if (a === "--out") {
185
+ const v = argv[++i];
186
+ if (!v) die("--out requires a path");
187
+ flags.out = v;
188
+ } else if (a === "--token") {
189
+ const v = argv[++i];
190
+ if (!v) die("--token requires a value");
191
+ if (!/^[A-Za-z0-9_-]+$/.test(v)) die("--token must be alphanumeric (plus _ or -)");
192
+ flags.token = v;
193
+ } else if (a === "--quiet") {
194
+ flags.quiet = true;
195
+ } else if (a === "--help" || a === "-h") {
196
+ printHelp();
197
+ process.exit(0);
198
+ } else if (a.startsWith("--")) {
199
+ die(`unknown flag: ${a}`);
200
+ } else {
201
+ die(`unexpected argument: ${a}`);
202
+ }
203
+ }
204
+ return flags;
205
+ }
206
+ function mintToken() {
207
+ return (0, import_node_crypto.randomBytes)(4).toString("hex");
208
+ }
209
+ async function runMonitor(argv) {
210
+ const flags = parseMonitorFlags(argv);
211
+ const token = flags.token ?? mintToken();
212
+ const projectRoot = process.cwd();
213
+ const outPath = flags.out ? (0, import_node_path2.resolve)(projectRoot, flags.out) : (0, import_node_path2.resolve)(projectRoot, ".launchsecure", `beacon-${token}.ndjson`);
214
+ const writer = openNdjsonWriter({
215
+ path: outPath,
216
+ onError: (err) => {
217
+ process.stderr.write(`
218
+ [launch-beacon] ndjson write error: ${err.message}
219
+ `);
220
+ }
221
+ });
222
+ const stats = {
223
+ events: 0,
224
+ badBatches: 0,
225
+ lastKind: "",
226
+ lastAt: 0
227
+ };
228
+ let server;
229
+ try {
230
+ server = await startBeaconServer({
231
+ port: flags.port,
232
+ host: flags.host,
233
+ token,
234
+ onBatch: (batch) => {
235
+ for (const raw of batch.events) {
236
+ writer.write(raw);
237
+ stats.events += 1;
238
+ const ev = raw;
239
+ if (typeof ev.kind === "string") stats.lastKind = ev.kind;
240
+ if (typeof ev.ts === "number") stats.lastAt = ev.ts;
241
+ }
242
+ draw();
243
+ },
244
+ onBadBatch: (reason) => {
245
+ stats.badBatches += 1;
246
+ process.stderr.write(`
247
+ [launch-beacon] bad batch: ${reason}
248
+ `);
249
+ },
250
+ onServerError: (err) => {
251
+ process.stderr.write(`
252
+ [launch-beacon] server error: ${err.message}
253
+ `);
254
+ }
255
+ });
256
+ } catch (err) {
257
+ const msg = err instanceof Error ? err.message : String(err);
258
+ if (msg.includes("EADDRINUSE")) {
259
+ die(`port ${flags.port} already in use \u2014 pass --port <n> to choose another`);
260
+ }
261
+ die(`failed to start: ${msg}`);
262
+ return;
263
+ }
264
+ process.stdout.write(`
265
+ launch-beacon monitor \u2014 listening
266
+ `);
267
+ process.stdout.write(` paste URL ${server.url}
268
+ `);
269
+ process.stdout.write(` writing ${outPath}
270
+ `);
271
+ process.stdout.write(` session ${token}
272
+
273
+ `);
274
+ let heartbeatTimer = null;
275
+ const draw = () => {
276
+ if (flags.quiet) return;
277
+ const since = stats.lastAt ? Math.max(0, Math.round((Date.now() - stats.lastAt) / 1e3)) : null;
278
+ const sinceStr = since === null ? "\u2014" : since < 5 ? "just now" : `${since}s ago`;
279
+ const lastStr = stats.lastKind ? `${stats.lastKind} (${sinceStr})` : "\u2014";
280
+ const badStr = stats.badBatches > 0 ? ` | bad: ${stats.badBatches}` : "";
281
+ const line = ` events: ${stats.events}${badStr} | last: ${lastStr}`;
282
+ process.stdout.write(`\r\x1B[K${line}`);
283
+ };
284
+ if (!flags.quiet) {
285
+ heartbeatTimer = setInterval(draw, 1e3);
286
+ draw();
287
+ }
288
+ const shutdown = async (signal) => {
289
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
290
+ process.stdout.write(`
291
+
292
+ [launch-beacon] caught ${signal}, shutting down\u2026
293
+ `);
294
+ try {
295
+ await server.close();
296
+ } catch (err) {
297
+ process.stderr.write(`[launch-beacon] server close error: ${err.message}
298
+ `);
299
+ }
300
+ try {
301
+ await writer.close();
302
+ } catch (err) {
303
+ process.stderr.write(`[launch-beacon] writer close error: ${err.message}
304
+ `);
305
+ }
306
+ process.stdout.write(` events written: ${writer.count()}
307
+ `);
308
+ process.stdout.write(` file: ${outPath}
309
+ `);
310
+ process.exit(0);
311
+ };
312
+ process.on("SIGINT", () => void shutdown("SIGINT"));
313
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
314
+ }
315
+ function printHelp() {
316
+ process.stdout.write(
317
+ `launch-beacon
318
+
319
+ Usage:
320
+ launch-beacon monitor [flags] Start the HTTP receiver for beacon monitor batches
321
+
322
+ Flags for "monitor":
323
+ --port <n> port to listen on (default: 9876)
324
+ --host <host> host to bind to (default: 127.0.0.1)
325
+ --out <path> output NDJSON file (default: .launchsecure/beacon-<token>.ndjson)
326
+ --token <str> use this token instead of a random 8-char hex
327
+ --quiet suppress the live heartbeat line
328
+
329
+ The printed URL is what you paste into the beacon debug panel. Beacon POSTs
330
+ monitor batches there; this process writes each event as a JSON line to the
331
+ output file. Ctrl+C shuts down cleanly.
332
+ `
333
+ );
334
+ }
335
+ async function main() {
336
+ const argv = process.argv.slice(2);
337
+ const subcommand = argv[0];
338
+ if (!subcommand || subcommand === "monitor") {
339
+ await runMonitor(subcommand === "monitor" ? argv.slice(1) : argv);
340
+ return;
341
+ }
342
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
343
+ printHelp();
344
+ return;
345
+ }
346
+ die(`unknown subcommand: ${subcommand}`);
347
+ }
348
+ function die(msg) {
349
+ process.stderr.write(`[launch-beacon] ${msg}
350
+ `);
351
+ process.exit(2);
352
+ }
353
+ void main();
@@ -27091,7 +27091,7 @@ function buildAnalyzerPrompt(ctx) {
27091
27091
  lines.push("If this feedback contains MULTIPLE distinct issues, the first line categorises the PRIMARY one. List the others on subsequent lines as `also <bug|feature|\u2026>: <one sentence>`. The first line is harvested as the card preview; the rest belong to your detailed analysis.");
27092
27092
  lines.push("");
27093
27093
  lines.push("ISSUE ENUMERATION \u2014 DO THIS BEFORE ANY OTHER STEP.");
27094
- lines.push("The user's intent is spread across THREE places: the FEEDBACK BODY, each PIN's NOTE, and the SCREENSHOT. Sub-bugs frequently live in pin notes, not the body. Before any tool call, enumerate every distinct issue you can identify across all three sources. For each issue, decide one of:");
27094
+ lines.push("The user's intent + diagnostic signal is spread across FOUR places: the FEEDBACK BODY, each PIN's NOTE, any RUNTIME EVENTS the beacon captured around the report (silent errors / unhandled rejections \u2014 these often reveal the real failure even when the body is vague), and the SCREENSHOT. Sub-bugs frequently live in pin notes, not the body. Before any tool call, enumerate every distinct issue you can identify across all four sources. For each issue, decide one of:");
27095
27095
  lines.push(" \u2022 fix it now (add to your plan)");
27096
27096
  lines.push(" \u2022 defer it (note why \u2014 out of scope, requires user input, etc.)");
27097
27097
  lines.push(" \u2022 ambiguous \u2192 surface via `AskUserQuestion` BEFORE editing. Do NOT silently drop unclear fragments.");
@@ -27141,6 +27141,21 @@ function buildAnalyzerPrompt(ctx) {
27141
27141
  }
27142
27142
  lines.push("");
27143
27143
  }
27144
+ if (ctx.events && ctx.events.length > 0) {
27145
+ lines.push(`RUNTIME EVENTS (${ctx.events.length}) \u2014 captured by the beacon between page load and Send. Treat these as the highest-signal diagnostic when the body is short. Always check whether the named source file matches a route/component you're investigating before blaming the user-described symptom.`);
27146
+ for (const ev of ctx.events) {
27147
+ const offset = formatEventOffset(ev.ts, ctx.capturedAt);
27148
+ const kindLabel = ev.kind === "unhandledrejection" ? "rejection" : "error";
27149
+ const location = ev.source ? `${ev.source}${ev.line ? `:${ev.line}` : ""}${ev.col ? `:${ev.col}` : ""}` : null;
27150
+ lines.push(`${kindLabel.toUpperCase()} (${offset}): ${ev.message}`);
27151
+ if (location) lines.push(` at ${location}`);
27152
+ if (ev.stack) {
27153
+ const stackHead = ev.stack.split("\n").slice(0, 5).join("\n").slice(0, 600);
27154
+ for (const sl of stackHead.split("\n")) lines.push(` ${sl}`);
27155
+ }
27156
+ }
27157
+ lines.push("");
27158
+ }
27144
27159
  lines.push("PROCEDURE (step 0 is the mandatory category line above \u2014 do NOT repeat the format instruction):");
27145
27160
  let n = 1;
27146
27161
  if (ctx.screenshotLocalPath) {
@@ -27161,6 +27176,16 @@ function buildAnalyzerPrompt(ctx) {
27161
27176
  lines.push(` ${n++}. If any fix is non-trivial, ambiguous, or would touch many files, STOP and either (a) ask via \`AskUserQuestion\`, or (b) write a plan in your final response instead of editing. Better to leave the user a clear next-step than to commit a wrong change.`);
27162
27177
  return lines.join("\n");
27163
27178
  }
27179
+ function formatEventOffset(eventTs, capturedAt) {
27180
+ if (!capturedAt) return `ts=${eventTs}`;
27181
+ const diffMs = eventTs - new Date(capturedAt).getTime();
27182
+ if (!Number.isFinite(diffMs)) return `ts=${eventTs}`;
27183
+ const direction = diffMs <= 0 ? "before report" : "after report";
27184
+ const abs = Math.abs(diffMs);
27185
+ if (abs < 1e3) return `${abs}ms ${direction}`;
27186
+ if (abs < 6e4) return `${(abs / 1e3).toFixed(1)}s ${direction}`;
27187
+ return `${(abs / 6e4).toFixed(1)}m ${direction}`;
27188
+ }
27164
27189
  async function resumeAnalysisSession(params) {
27165
27190
  const { sessionId, projectDir } = params;
27166
27191
  const id = createSessionDirect(`radar: ${sessionId.slice(-8)} (resumed)`, projectDir, sessionId);
@@ -27561,6 +27586,23 @@ function buildContext(payload) {
27561
27586
  const body = typeof resource.body === "string" ? resource.body : payload.message;
27562
27587
  const severity = typeof fields.severity === "string" ? fields.severity : void 0;
27563
27588
  const route = typeof beaconMeta.url === "string" ? beaconMeta.url : void 0;
27589
+ const rawEvents = Array.isArray(fields.events) ? fields.events : [];
27590
+ const events = rawEvents.map((e) => {
27591
+ const kind = e.kind === "error" || e.kind === "unhandledrejection" ? e.kind : null;
27592
+ const ts = typeof e.ts === "number" ? e.ts : null;
27593
+ const message = typeof e.message === "string" ? e.message : null;
27594
+ if (!kind || ts === null || !message) return null;
27595
+ return {
27596
+ ts,
27597
+ kind,
27598
+ message,
27599
+ ...typeof e.stack === "string" ? { stack: e.stack } : {},
27600
+ ...typeof e.source === "string" ? { source: e.source } : {},
27601
+ ...typeof e.line === "number" ? { line: e.line } : {},
27602
+ ...typeof e.col === "number" ? { col: e.col } : {}
27603
+ };
27604
+ }).filter((e) => e !== null);
27605
+ const capturedAt = typeof beaconMeta.capturedAt === "string" ? beaconMeta.capturedAt : void 0;
27564
27606
  const viewport = isViewport(beaconMeta.viewport) ? { w: beaconMeta.viewport.w, h: beaconMeta.viewport.h } : void 0;
27565
27607
  const theme = beaconMeta.theme === "light" || beaconMeta.theme === "dark" ? beaconMeta.theme : void 0;
27566
27608
  const userAgent = typeof beaconMeta.userAgent === "string" ? beaconMeta.userAgent : void 0;
@@ -27573,6 +27615,8 @@ function buildContext(payload) {
27573
27615
  severity,
27574
27616
  route,
27575
27617
  pins: pins.length > 0 ? pins : void 0,
27618
+ events: events.length > 0 ? events : void 0,
27619
+ capturedAt,
27576
27620
  viewport,
27577
27621
  theme,
27578
27622
  userAgent,
@@ -35283,7 +35327,11 @@ function readLaunchSecureMcpConfig() {
35283
35327
  } catch (err2) {
35284
35328
  throw new Error(`Could not parse ${configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`);
35285
35329
  }
35286
- const { pat: pat2, orgSlug: orgSlug2, projectSlug: projectSlug2, serverUrl } = parsed;
35330
+ const block = parsed.profiles && parsed.active ? parsed.profiles[parsed.active] : parsed;
35331
+ if (!block) {
35332
+ throw new Error(`${CRED_CONFIG_FILENAME} active course "${parsed.active}" not found in profiles.`);
35333
+ }
35334
+ const { pat: pat2, orgSlug: orgSlug2, projectSlug: projectSlug2, serverUrl } = block;
35287
35335
  if (!pat2 || !pat2.startsWith("ls_pat_")) {
35288
35336
  throw new Error(`${CRED_CONFIG_FILENAME} "pat" is missing or not a LaunchSecure PAT (ls_pat_...).`);
35289
35337
  }
File without changes
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/server/course-entry.ts
27
+ var fs2 = __toESM(require("node:fs"));
28
+ var path2 = __toESM(require("node:path"));
29
+
30
+ // src/server/cred-shape.ts
31
+ var fs = __toESM(require("node:fs"));
32
+ var path = __toESM(require("node:path"));
33
+ var CONFIG_FILENAME = ".launch-secure.cred.config";
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 readCredFile(repoRoot) {
66
+ const p = path.join(repoRoot, CONFIG_FILENAME);
67
+ if (!fs.existsSync(p)) return null;
68
+ try {
69
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
70
+ } catch (err) {
71
+ throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
72
+ }
73
+ }
74
+ function writeJsonAtomic(absPath, value, mode) {
75
+ const tmp = `${absPath}.tmp`;
76
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
77
+ if (mode !== void 0) {
78
+ try {
79
+ fs.chmodSync(tmp, mode);
80
+ } catch {
81
+ }
82
+ }
83
+ fs.renameSync(tmp, absPath);
84
+ }
85
+
86
+ // src/server/course-entry.ts
87
+ var MCP_FILENAME = ".mcp.json";
88
+ var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
89
+ function info(msg) {
90
+ console.log(`[launch-course] ${msg}`);
91
+ }
92
+ function ok(msg) {
93
+ console.log(`[launch-course] \u2713 ${msg}`);
94
+ }
95
+ function fail(msg) {
96
+ console.error(`[launch-course] \u2717 ${msg}`);
97
+ process.exit(1);
98
+ }
99
+ function readCredFile2() {
100
+ const cred = readCredFile(process.cwd());
101
+ if (!cred) {
102
+ fail(`${CONFIG_FILENAME} not found in ${process.cwd()}. Run \`npx launch-kit init \u2026\` first.`);
103
+ }
104
+ return cred;
105
+ }
106
+ function toNested2(cred) {
107
+ const nested = toNested(cred);
108
+ if (!nested) {
109
+ fail(`${CONFIG_FILENAME} is missing required fields (pat, orgSlug, projectSlug, serverUrl) and has no profiles. Run \`npx launch-kit init \u2026\` to (re)bootstrap.`);
110
+ }
111
+ return nested;
112
+ }
113
+ function parseFlag(argv, key) {
114
+ const prefix = `--${key}=`;
115
+ const found = argv.find((a) => a.startsWith(prefix));
116
+ return found ? found.slice(prefix.length) : void 0;
117
+ }
118
+ function cmdList() {
119
+ const nested = toNested2(readCredFile2());
120
+ const names = Object.keys(nested.profiles).sort();
121
+ if (names.length === 0) {
122
+ info("(no courses)");
123
+ return;
124
+ }
125
+ info(`courses (${names.length}):`);
126
+ const nameWidth = Math.max(...names.map((n) => n.length), 8);
127
+ for (const name of names) {
128
+ const p = nested.profiles[name];
129
+ const marker = name === nested.active ? "\u2605" : " ";
130
+ console.log(` ${marker} ${name.padEnd(nameWidth)} ${p.serverUrl} (${p.orgSlug}/${p.projectSlug})`);
131
+ }
132
+ }
133
+ function cmdAdd(argv) {
134
+ const name = argv.find((a) => !a.startsWith("--"));
135
+ if (!name) fail("usage: launch-course add <name> --token=ls_pat_... --org=<slug> --project=<slug> [--url=<serverUrl>]");
136
+ const token = parseFlag(argv, "token");
137
+ const org = parseFlag(argv, "org");
138
+ const project = parseFlag(argv, "project");
139
+ const url = (parseFlag(argv, "url") ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
140
+ if (!token || !token.startsWith("ls_pat_")) fail("--token=<ls_pat_...> required");
141
+ if (!org) fail("--org=<slug> required");
142
+ if (!project) fail("--project=<slug> required");
143
+ const nested = toNested2(readCredFile2());
144
+ const existed = Boolean(nested.profiles[name]);
145
+ nested.profiles[name] = { pat: token, orgSlug: org, projectSlug: project, serverUrl: url };
146
+ writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), nested, 384);
147
+ ok(`${existed ? "updated" : "added"} course "${name}" (${url})`);
148
+ if (nested.active !== name) {
149
+ info(`active course is still "${nested.active}" \u2014 run \`launch-course set ${name}\` to switch`);
150
+ }
151
+ }
152
+ function cmdSet(argv) {
153
+ const name = argv[0];
154
+ if (!name) fail("usage: launch-course set <name>");
155
+ const nested = toNested2(readCredFile2());
156
+ const target = nested.profiles[name];
157
+ if (!target) {
158
+ const known = Object.keys(nested.profiles).sort().join(", ") || "(none)";
159
+ fail(`no course named "${name}". known: ${known}`);
160
+ }
161
+ if (nested.active === name) {
162
+ info(`already on "${name}" \u2014 nothing to do`);
163
+ return;
164
+ }
165
+ const next = { active: name, profiles: nested.profiles };
166
+ writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), next, 384);
167
+ const mcpStatus = updateMcpUrl(target.serverUrl);
168
+ ok(`active course \u2192 "${name}" (${target.serverUrl})`);
169
+ if (mcpStatus === "updated") {
170
+ info("reconnect MCP in Claude Code: /mcp \u2192 toggle launch-secure off/on (URL is read at connect time)");
171
+ }
172
+ }
173
+ function cmdRm(argv) {
174
+ const name = argv[0];
175
+ if (!name) fail("usage: launch-course rm <name>");
176
+ const nested = toNested2(readCredFile2());
177
+ if (!nested.profiles[name]) fail(`no course named "${name}"`);
178
+ if (nested.active === name) {
179
+ fail(`cannot remove the active course "${name}". \`launch-course set <other>\` first.`);
180
+ }
181
+ delete nested.profiles[name];
182
+ writeJsonAtomic(path2.join(process.cwd(), CONFIG_FILENAME), nested, 384);
183
+ ok(`removed course "${name}"`);
184
+ }
185
+ function updateMcpUrl(serverUrl) {
186
+ const p = path2.join(process.cwd(), MCP_FILENAME);
187
+ if (!fs2.existsSync(p)) {
188
+ info(`(no ${MCP_FILENAME} \u2014 skipped URL update; run \`npx launch-kit init \u2026\` to wire MCP)`);
189
+ return "missing-file";
190
+ }
191
+ let mcp;
192
+ try {
193
+ mcp = JSON.parse(fs2.readFileSync(p, "utf-8"));
194
+ } catch (err) {
195
+ fail(`could not parse ${MCP_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
196
+ }
197
+ const entry = mcp.mcpServers?.["launch-secure"];
198
+ if (!entry) {
199
+ info(`(no "launch-secure" entry in ${MCP_FILENAME} \u2014 skipped URL update)`);
200
+ return "missing-entry";
201
+ }
202
+ entry.url = `${serverUrl.replace(/\/+$/, "")}/api/mcp/project`;
203
+ writeJsonAtomic(p, mcp);
204
+ ok(`updated ${MCP_FILENAME} launch-secure.url \u2192 ${entry.url}`);
205
+ return "updated";
206
+ }
207
+ function help() {
208
+ console.log(`launch-course \u2014 manage LaunchSecure server profiles ("courses").
209
+
210
+ Usage:
211
+ launch-course list
212
+ launch-course add <name> --token=ls_pat_... --org=<slug> --project=<slug> [--url=<serverUrl>]
213
+ launch-course set <name>
214
+ launch-course rm <name>
215
+
216
+ The cred file (.launch-secure.cred.config) holds every course under \`profiles\`,
217
+ with \`active\` selecting which one drives MCP / launch-pod auth. Legacy flat
218
+ files are auto-migrated on the first multi-profile op. \`launch-course set\`
219
+ also rewrites .mcp.json's launch-secure.url so Claude Code routes to the right
220
+ server on next MCP reconnect.
221
+ `);
222
+ }
223
+ function main() {
224
+ const [cmd, ...rest] = process.argv.slice(2);
225
+ switch (cmd) {
226
+ case "list":
227
+ return cmdList();
228
+ case "add":
229
+ return cmdAdd(rest);
230
+ case "set":
231
+ case "use":
232
+ return cmdSet(rest);
233
+ case "rm":
234
+ case "remove":
235
+ case "delete":
236
+ return cmdRm(rest);
237
+ case void 0:
238
+ case "help":
239
+ case "--help":
240
+ case "-h":
241
+ return help();
242
+ default:
243
+ fail(`unknown command "${cmd}". try \`launch-course help\``);
244
+ }
245
+ }
246
+ main();
File without changes