@launchsecure/launch-kit 0.0.26 → 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 (123) 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/chart-client/assets/index-CJ4mgRRF.css +1 -0
  28. package/dist/chart-client/assets/{index-Bk1hawjD.js → index-Ccy-DpI-.js} +46 -42
  29. package/dist/chart-client/index.html +2 -2
  30. package/dist/client/assets/index-DI5qSR_w.css +32 -0
  31. package/dist/client/assets/index-Dp0_okva.js +294 -0
  32. package/dist/client/index.html +2 -2
  33. package/dist/council-client/assets/index-C_-vAM9L.css +1 -0
  34. package/dist/council-client/index.html +2 -2
  35. package/dist/deck-client/assets/{_baseUniq-C2xT_eYu.js → _baseUniq-W2JQDmje.js} +1 -1
  36. package/dist/deck-client/assets/{arc-CmVL9pGd.js → arc-DIBWAId9.js} +1 -1
  37. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-BSFgdjve.js → architectureDiagram-Q4EWVU46-CAIRMvJK.js} +1 -1
  38. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-DuLzscvP.js → blockDiagram-DXYQGD6D-BeNaNiOi.js} +1 -1
  39. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CfCJB8eY.js → c4Diagram-AHTNJAMY-B9Ozi62h.js} +1 -1
  40. package/dist/deck-client/assets/channel-CRdozqbp.js +1 -0
  41. package/dist/deck-client/assets/{chunk-4BX2VUAB-DxmLYTWZ.js → chunk-4BX2VUAB-D7AZ47dt.js} +1 -1
  42. package/dist/deck-client/assets/{chunk-4TB4RGXK-CCnf7GFE.js → chunk-4TB4RGXK-DnVnNPcI.js} +1 -1
  43. package/dist/deck-client/assets/{chunk-55IACEB6-Db9DApcj.js → chunk-55IACEB6-UKYs-YNd.js} +1 -1
  44. package/dist/deck-client/assets/{chunk-EDXVE4YY-DmYDq8ZI.js → chunk-EDXVE4YY-D43b-SKn.js} +1 -1
  45. package/dist/deck-client/assets/{chunk-FMBD7UC4-BGhUlF20.js → chunk-FMBD7UC4-QzBAoyyW.js} +1 -1
  46. package/dist/deck-client/assets/{chunk-OYMX7WX6-CpEnicQZ.js → chunk-OYMX7WX6-Cjif4r6W.js} +1 -1
  47. package/dist/deck-client/assets/{chunk-QZHKN3VN-Doa7LKwf.js → chunk-QZHKN3VN-CqLDirEI.js} +1 -1
  48. package/dist/deck-client/assets/{chunk-YZCP3GAM-CpkIlH6V.js → chunk-YZCP3GAM-_FQvmMs4.js} +1 -1
  49. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-lIZMp57W.js +1 -0
  50. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-lIZMp57W.js +1 -0
  51. package/dist/deck-client/assets/clone-BtWeSTyJ.js +1 -0
  52. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bkh8Bfcb.js → cose-bilkent-S5V4N54A-rfrocesE.js} +1 -1
  53. package/dist/deck-client/assets/{dagre-KV5264BT-Bp0XpTgH.js → dagre-KV5264BT-Bv_7DJat.js} +1 -1
  54. package/dist/deck-client/assets/{diagram-5BDNPKRD-ZHiyGYPQ.js → diagram-5BDNPKRD-4F1414G5.js} +1 -1
  55. package/dist/deck-client/assets/{diagram-G4DWMVQ6-BW-Q8_H5.js → diagram-G4DWMVQ6-C4-Pszqm.js} +1 -1
  56. package/dist/deck-client/assets/{diagram-MMDJMWI5-6I3LTafu.js → diagram-MMDJMWI5-B647TIx9.js} +1 -1
  57. package/dist/deck-client/assets/{diagram-TYMM5635-CyM5YK28.js → diagram-TYMM5635-BFAqpezd.js} +1 -1
  58. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-CjNxVJHk.js → erDiagram-SMLLAGMA-BfBfrJOC.js} +1 -1
  59. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-BDQHuAJR.js → flowDiagram-DWJPFMVM-DX9YAYes.js} +1 -1
  60. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-B7MnkpbP.js → ganttDiagram-T4ZO3ILL-DCuiy7wF.js} +1 -1
  61. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-C9dZAcYD.js → gitGraphDiagram-UUTBAWPF-CGp1IXUh.js} +1 -1
  62. package/dist/deck-client/assets/{graph-CjdBnzUy.js → graph-B7g8aoxv.js} +1 -1
  63. package/dist/deck-client/assets/{index-DeIVPW63.js → index-Dg1r-WSN.js} +3 -3
  64. package/dist/deck-client/assets/index-DsIZ3LqL.css +1 -0
  65. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-C7d3iRC3.js → infoDiagram-42DDH7IO-L3fahMkF.js} +1 -1
  66. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-BcYGKj09.js → ishikawaDiagram-UXIWVN3A-aS_EjWBZ.js} +1 -1
  67. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DqFlRrOL.js → journeyDiagram-VCZTEJTY-djTSQZF9.js} +1 -1
  68. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BJhPp1NR.js → kanban-definition-6JOO6SKY-CcTHo4CM.js} +1 -1
  69. package/dist/deck-client/assets/{layout-DIeS6GvK.js → layout-mEJiadb7.js} +1 -1
  70. package/dist/deck-client/assets/{linear-He_yJy5H.js → linear-XgTKqyRu.js} +1 -1
  71. package/dist/deck-client/assets/{min-DQ6Kx06t.js → min-Ct9jZdpd.js} +1 -1
  72. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-sQ62L8T2.js → mindmap-definition-QFDTVHPH-BaFxCGNU.js} +1 -1
  73. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-BqCWmU2K.js → pieDiagram-DEJITSTG-CIbYYjtw.js} +1 -1
  74. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-rQ1TJOoe.js → quadrantDiagram-34T5L4WZ-D9EtCOvh.js} +1 -1
  75. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BO2MPBOM.js → requirementDiagram-MS252O5E-xeni9eVG.js} +1 -1
  76. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BgsHEVex.js → sankeyDiagram-XADWPNL6-LYeknz9h.js} +1 -1
  77. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-B3j1yMLU.js → sequenceDiagram-FGHM5R23-RDbsKFZf.js} +1 -1
  78. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-C8jFlZou.js → stateDiagram-FHFEXIEX-BH1Zjglk.js} +1 -1
  79. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BrV78NDR.js +1 -0
  80. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-tM-qo4Zk.js → timeline-definition-GMOUNBTQ-IFXxKptt.js} +1 -1
  81. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-B0-6kOEu.js → vennDiagram-DHZGUBPP-D-sLkQs9.js} +1 -1
  82. package/dist/deck-client/assets/{wardley-RL74JXVD-HpBk07P-.js → wardley-RL74JXVD-C010F8l4.js} +1 -1
  83. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-BkA1NLDE.js → wardleyDiagram-NUSXRM2D-BTjjuDU3.js} +1 -1
  84. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CEKGSuI-.js → xychartDiagram-5P7HB3ND-AYbv92n-.js} +1 -1
  85. package/dist/deck-client/index.html +2 -2
  86. package/dist/server/beacon-monitor-entry.js +353 -0
  87. package/dist/server/chart-serve.js +3836 -3750
  88. package/dist/server/cli.js +8789 -8219
  89. package/dist/server/council-entry.js +17 -5
  90. package/dist/server/council-serve.js +8 -3
  91. package/dist/server/course-entry.js +246 -0
  92. package/dist/server/deck-mcp-entry.js +24 -12
  93. package/dist/server/deck-serve.js +11 -8
  94. package/dist/server/graph-mcp-entry.js +5005 -4865
  95. package/dist/server/init-entry.js +939 -0
  96. package/dist/server/orbit-entry.js +2435 -0
  97. package/dist/server/parse-worker-entry.js +4721 -0
  98. package/dist/server/recall-entry.js +356 -18
  99. package/package.json +11 -4
  100. package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
  101. package/scaffolds/ls-marketplace/plugins/ls/.claude-plugin/plugin.json +28 -0
  102. package/scaffolds/ls-marketplace/plugins/ls/commands/activate-beacon.md +216 -0
  103. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-array.md +92 -0
  104. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-clear.md +68 -0
  105. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-pulse.md +80 -0
  106. package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-scan.md +62 -0
  107. package/scaffolds/ls-marketplace/plugins/ls/commands/show-mcp-status.md +109 -0
  108. package/scaffolds/ls-marketplace/plugins/ls/commands/standup.md +177 -0
  109. package/scaffolds/migrate-safety/.github/workflows/backup-on-migration.yml +72 -0
  110. package/scaffolds/migrate-safety/docs/migrations-runbook.md +172 -0
  111. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +294 -0
  112. package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -0
  113. package/dist/chart-client/assets/index-DpaGa3bY.css +0 -1
  114. package/dist/client/assets/index-Bfel4OQ5.css +0 -32
  115. package/dist/client/assets/index-eC-WuUWB.js +0 -291
  116. package/dist/council-client/assets/index-P5kMsT5a.css +0 -1
  117. package/dist/deck-client/assets/channel-B4aNO8ZB.js +0 -1
  118. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +0 -1
  119. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +0 -1
  120. package/dist/deck-client/assets/clone-HduFm7qU.js +0 -1
  121. package/dist/deck-client/assets/index-LKZDAS9S.css +0 -1
  122. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +0 -1
  123. /package/dist/council-client/assets/{index-Cs_MVXHf.js → index-Dt4zWKSj.js} +0 -0
@@ -0,0 +1,939 @@
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/init-entry.ts
27
+ var import_node_child_process = require("node:child_process");
28
+ var fs2 = __toESM(require("node:fs"));
29
+ var import_node_http = require("node:http");
30
+ var import_node_https = require("node:https");
31
+ var path2 = __toESM(require("node:path"));
32
+ var readline = __toESM(require("node:readline"));
33
+ var import_node_url = require("node:url");
34
+
35
+ // src/server/cred-shape.ts
36
+ var fs = __toESM(require("node:fs"));
37
+ var path = __toESM(require("node:path"));
38
+ var CONFIG_FILENAME = ".launch-secure.cred.config";
39
+ function inferCourseName(serverUrl) {
40
+ try {
41
+ const host = new URL(serverUrl).hostname.toLowerCase();
42
+ if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
43
+ if (host.includes("staging")) return "staging";
44
+ if (host.endsWith(".vercel.app")) return "prod";
45
+ return host.split(".")[0] || "default";
46
+ } catch {
47
+ return "default";
48
+ }
49
+ }
50
+ function toNested(cred) {
51
+ if (cred.profiles && cred.active && cred.profiles[cred.active]) {
52
+ return { active: cred.active, profiles: cred.profiles };
53
+ }
54
+ if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
55
+ return null;
56
+ }
57
+ const name = inferCourseName(cred.serverUrl);
58
+ return {
59
+ active: name,
60
+ profiles: {
61
+ [name]: {
62
+ pat: cred.pat,
63
+ orgSlug: cred.orgSlug,
64
+ projectSlug: cred.projectSlug,
65
+ serverUrl: cred.serverUrl
66
+ }
67
+ }
68
+ };
69
+ }
70
+ function upsertProfile(existing, name, profile) {
71
+ const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
72
+ return {
73
+ active: name,
74
+ profiles: { ...base.profiles, [name]: profile }
75
+ };
76
+ }
77
+ function readCredFile(repoRoot) {
78
+ const p = path.join(repoRoot, CONFIG_FILENAME);
79
+ if (!fs.existsSync(p)) return null;
80
+ try {
81
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
82
+ } catch (err) {
83
+ throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
84
+ }
85
+ }
86
+ function writeJsonAtomic(absPath, value, mode) {
87
+ const tmp = `${absPath}.tmp`;
88
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
89
+ if (mode !== void 0) {
90
+ try {
91
+ fs.chmodSync(tmp, mode);
92
+ } catch {
93
+ }
94
+ }
95
+ fs.renameSync(tmp, absPath);
96
+ }
97
+
98
+ // src/server/init-entry.ts
99
+ var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
100
+ var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
101
+ var ONBOARD_SCRIPT_NAME = "onboard";
102
+ var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
103
+ var LAUNCH_KIT_TOOLS_GUIDE = `
104
+ Wired in Claude Code (.mcp.json):
105
+ launch-secure \u2014 LS API: work items, comms, secrets, members, board
106
+ launch-chart \u2014 code search + project graph (use instead of grep/glob)
107
+ launch-deck \u2014 visual playground / blast-radius diagrams
108
+ launch-orbit \u2014 git worktree orchestration (branch / merge gates)
109
+ launch-recall \u2014 restore deleted/modified files from shadow git
110
+
111
+ Other tools (run on demand via npx):
112
+ npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
113
+ npx launch-pod \u2014 full pipeline UI (separate launch-pod login)
114
+ npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
115
+ in-browser monitor. Paste the printed URL into
116
+ the beacon debug panel; events stream to
117
+ .launchsecure/beacon-<token>.ndjson for the
118
+ /ls:beacon-* commands below to read.
119
+
120
+ LS slash commands (run inside Claude Code in this project):
121
+ /ls:activate-beacon \u2014 wire the launch-kit-beacon in-app feedback
122
+ widget into this app (mounts the <launch-kit-
123
+ beacon> Web Component + scaffolds /api/feedback
124
+ forwarding to LaunchSecure Comm Hub)
125
+ /ls:standup \u2014 draft a daily standup from work since the last
126
+ push (chart-grouped themes, work-item linkage,
127
+ release detection) and post to LS Comm Hub as
128
+ a daily_update after you confirm
129
+ /ls:show-mcp-status \u2014 show recall watcher health + last snapshot.
130
+ Add 'full' for expanded report (PID, shadow
131
+ repo size, recent snaps)
132
+ /ls:beacon-scan \u2014 scan recent events from the active
133
+ launch-beacon monitor session. Pass a kind
134
+ (error/click/fetch/route/dialog/probe) and/or
135
+ a limit to filter.
136
+ /ls:beacon-pulse \u2014 most recent error + the N events that preceded
137
+ it. "What was happening just before it broke."
138
+ /ls:beacon-array \u2014 list monitor sessions in .launchsecure/ with
139
+ event counts, last activity, liveness glyph.
140
+ Add 'full' for a per-session expanded report.
141
+ /ls:beacon-clear \u2014 wipe the latest monitor session NDJSON (or
142
+ 'all'). Confirms before deleting.
143
+
144
+ Open this repo in Claude Code; on first open you'll be prompted to install
145
+ the "launchsecure" marketplace \u2014 accept to enable the commands above.
146
+ `;
147
+ var PACKAGE_MANAGERS = [
148
+ { name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
149
+ { name: "yarn", binary: "yarn", lockfiles: ["yarn.lock"], installArgs: ["install"] },
150
+ { name: "bun", binary: "bun", lockfiles: ["bun.lockb", "bun.lock"], installArgs: ["install"] },
151
+ { name: "npm", binary: "npm", lockfiles: ["package-lock.json"], installArgs: ["install"] }
152
+ ];
153
+ function parseArgs(argv) {
154
+ const args = {
155
+ token: process.env.LS_PAT ?? null,
156
+ orgSlug: null,
157
+ projectSlug: null,
158
+ serverUrl: DEFAULT_SERVER_URL,
159
+ targetDir: null,
160
+ course: null,
161
+ noInstall: false,
162
+ noRecall: false,
163
+ noMigrateSafety: false,
164
+ noLsMarketplace: false,
165
+ noRecallHook: false,
166
+ dryRun: false,
167
+ help: false
168
+ };
169
+ for (const raw of argv) {
170
+ if (raw === "--help" || raw === "-h") {
171
+ args.help = true;
172
+ continue;
173
+ }
174
+ if (raw === "--no-install") {
175
+ args.noInstall = true;
176
+ continue;
177
+ }
178
+ if (raw === "--no-recall") {
179
+ args.noRecall = true;
180
+ continue;
181
+ }
182
+ if (raw === "--no-migrate-safety") {
183
+ args.noMigrateSafety = true;
184
+ continue;
185
+ }
186
+ if (raw === "--no-ls-marketplace") {
187
+ args.noLsMarketplace = true;
188
+ continue;
189
+ }
190
+ if (raw === "--no-recall-hook") {
191
+ args.noRecallHook = true;
192
+ continue;
193
+ }
194
+ if (raw === "--dry-run") {
195
+ args.dryRun = true;
196
+ continue;
197
+ }
198
+ const eq = raw.indexOf("=");
199
+ if (!raw.startsWith("--") || eq < 0) continue;
200
+ const key = raw.slice(2, eq);
201
+ const val = raw.slice(eq + 1);
202
+ if (key === "token") args.token = val;
203
+ else if (key === "org") args.orgSlug = val;
204
+ else if (key === "project") args.projectSlug = val;
205
+ else if (key === "url") args.serverUrl = val.replace(/\/+$/, "");
206
+ else if (key === "dir") args.targetDir = val;
207
+ else if (key === "course") args.course = val;
208
+ }
209
+ return args;
210
+ }
211
+ function printHelp() {
212
+ console.log(`launch-kit init \u2014 bootstrap a LaunchSecure project on this machine
213
+
214
+ Usage:
215
+ npx launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
216
+
217
+ Required:
218
+ --token=<pat> LaunchSecure PAT (ls_pat_...). Or set LS_PAT env var.
219
+ --org=<orgSlug> Organization slug.
220
+ --project=<projectSlug> Project slug.
221
+
222
+ Options:
223
+ --url=<serverUrl> LaunchSecure base URL (default: ${DEFAULT_SERVER_URL}).
224
+ --dir=<path> Target directory (default: ./<projectSlug>).
225
+ --course=<name> Name for the course (profile) being added to
226
+ .launch-secure.cred.config. When omitted, inferred
227
+ from --url: localhost\u2192"local", *staging*\u2192"staging",
228
+ *.vercel.app\u2192"prod", else hostname. The course
229
+ becomes active; re-run with a different --course
230
+ and --url to add another (e.g. local + staging).
231
+ Use \`launch-course set <name>\` to switch later.
232
+ --no-install Skip dependency install step.
233
+ --no-recall Skip launch-recall (shadow git backup) scaffold.
234
+ --no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
235
+ wrapper + GitHub Action + runbook).
236
+ --no-ls-marketplace Skip the Claude Code "launchsecure" marketplace
237
+ scaffold (.claude/marketplace/ + .claude/settings.json
238
+ wiring \u2014 exposes /ls:activate-beacon and future
239
+ ls-namespaced slash commands).
240
+ --no-recall-hook Skip the SessionStart hook scaffold (Claude Code
241
+ hook + scripts/ensure-recall.sh that auto-restarts
242
+ the launch-recall watcher if it died between
243
+ sessions). The hook is the surfacing layer for
244
+ watcher-died-silently scenarios.
245
+ --dry-run Preview every file write, merge, clone, and install
246
+ command without making any changes. Useful before
247
+ re-running init against a customized project. The
248
+ project_info HTTP call still runs (it's read-only).
249
+ --help Show this help.
250
+
251
+ What it does:
252
+ 1. Preflight: checks git + node + (optional) gh.
253
+ 2. Calls LaunchSecure to resolve the project + its git remote.
254
+ 3. Clones the repo (prefers gh, falls back to git) \u2014 skipped if dir already
255
+ contains the matching clone.
256
+ 4. Writes .launch-secure.cred.config (gitignored). Auto-migrates any
257
+ legacy .launch-secure.config that contains a PAT.
258
+ 5. Merges .mcp.json with 5 entries (launch-secure / -chart / -deck / -orbit /
259
+ -recall). Preserves your other MCP entries. Auth via headersHelper \u2014
260
+ no secrets written into .mcp.json.
261
+ 6. Detects package manager (packageManager field > lockfile > npm fallback)
262
+ and runs install. Skip with --no-install.
263
+ 7. If package.json declares a "${ONBOARD_SCRIPT_NAME}" script, runs it
264
+ (your repo's hook for codegen / env pull / db setup / etc.).
265
+ 8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
266
+ 9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
267
+ runbook + .backups/ gitignore line). Skip with --no-migrate-safety.
268
+ 10. Scaffolds the Claude Code "launchsecure" marketplace at
269
+ .claude/marketplace/ and wires .claude/settings.json so Claude Code
270
+ auto-discovers it and enables the "ls" plugin (exposes
271
+ /ls:activate-beacon for wiring the launch-kit-beacon in-app feedback
272
+ widget). Skip with --no-ls-marketplace.
273
+ 11. Scaffolds scripts/ensure-recall.sh and appends a SessionStart hook to
274
+ .claude/settings.json that respawns the launch-recall watcher if it
275
+ died between sessions. Idempotent (dedupes by hook command-match).
276
+ Skip with --no-recall-hook.
277
+ `);
278
+ }
279
+ async function prompt(question) {
280
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
281
+ return new Promise((resolve2) => rl.question(question, (answer) => {
282
+ rl.close();
283
+ resolve2(answer.trim());
284
+ }));
285
+ }
286
+ function fail(msg) {
287
+ console.error(`[launch-kit] \u2717 ${msg}`);
288
+ process.exit(1);
289
+ }
290
+ function info(msg) {
291
+ console.log(`[launch-kit] ${msg}`);
292
+ }
293
+ function ok(msg) {
294
+ console.log(`[launch-kit] \u2713 ${msg}`);
295
+ }
296
+ var DRY_RUN = false;
297
+ function dryNote(msg) {
298
+ console.log(`[launch-kit] (dry-run) ${msg}`);
299
+ }
300
+ function which(bin) {
301
+ const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
302
+ if (res.status !== 0) return null;
303
+ return res.stdout.split(/\r?\n/)[0]?.trim() || null;
304
+ }
305
+ function preflight() {
306
+ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
307
+ if (nodeMajor < 18) fail(`Node.js >= 18 required (current: ${process.versions.node}).`);
308
+ if (!which("git")) fail("git not found in PATH. Install git: https://git-scm.com/downloads");
309
+ const hasGh = which("gh") !== null;
310
+ ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
311
+ return { hasGh };
312
+ }
313
+ function callProjectInfo(args) {
314
+ return new Promise((resolve2, reject) => {
315
+ const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
316
+ const body = JSON.stringify({
317
+ jsonrpc: "2.0",
318
+ id: 1,
319
+ method: "tools/call",
320
+ params: {
321
+ name: "project_info",
322
+ arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
323
+ }
324
+ });
325
+ const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
326
+ const req = requester(
327
+ {
328
+ host: mcpUrl.hostname,
329
+ port: mcpUrl.port || (mcpUrl.protocol === "https:" ? 443 : 80),
330
+ path: mcpUrl.pathname,
331
+ method: "POST",
332
+ headers: {
333
+ "Content-Type": "application/json",
334
+ "Accept": "application/json, text/event-stream",
335
+ "Content-Length": String(Buffer.byteLength(body)),
336
+ "Authorization": `Bearer ${args.token}`,
337
+ "X-Org-Slug": args.orgSlug,
338
+ "X-Project-Slug": args.projectSlug
339
+ }
340
+ },
341
+ (res) => {
342
+ const chunks = [];
343
+ res.on("data", (c) => chunks.push(c));
344
+ res.on("end", () => {
345
+ const text = Buffer.concat(chunks).toString("utf-8");
346
+ if (res.statusCode === 401 || res.statusCode === 403) {
347
+ reject(new Error(`PAT rejected (${res.statusCode}). Check token + that it has access to ${args.orgSlug}/${args.projectSlug}.`));
348
+ return;
349
+ }
350
+ if (res.statusCode && res.statusCode >= 400) {
351
+ reject(new Error(`LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
352
+ return;
353
+ }
354
+ let json = text;
355
+ if (text.startsWith("event:") || text.includes("\ndata: ")) {
356
+ for (const line of text.split("\n")) {
357
+ if (line.startsWith("data: ")) {
358
+ json = line.slice(6);
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ try {
364
+ const parsed = JSON.parse(json);
365
+ if (parsed.error) {
366
+ reject(new Error(`project_info error: ${parsed.error.message ?? "unknown"}`));
367
+ return;
368
+ }
369
+ const inner = parsed.result?.content?.[0]?.text;
370
+ if (!inner) {
371
+ reject(new Error("project_info returned no content"));
372
+ return;
373
+ }
374
+ if (inner.startsWith("\u2500\u2500 Error")) {
375
+ const firstLine = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("\u2500\u2500"))[0];
376
+ reject(new Error(`project_info: ${firstLine ?? inner}`));
377
+ return;
378
+ }
379
+ const payload = JSON.parse(inner);
380
+ resolve2({
381
+ orgSlug: payload.org.slug,
382
+ projectSlug: payload.project.slug,
383
+ projectName: payload.project.name,
384
+ repositoryUrl: payload.project.repositoryUrl
385
+ });
386
+ } catch (err) {
387
+ reject(new Error(`Could not parse project_info response: ${err instanceof Error ? err.message : String(err)}`));
388
+ }
389
+ });
390
+ }
391
+ );
392
+ req.on("error", reject);
393
+ req.write(body);
394
+ req.end();
395
+ });
396
+ }
397
+ function gitRemoteUrl(dir) {
398
+ const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
399
+ if (res.status !== 0) return null;
400
+ return res.stdout.trim() || null;
401
+ }
402
+ function normalizeRepoUrl(url) {
403
+ let u = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
404
+ const sshMatch = u.match(/^git@([^:]+):(.+)$/);
405
+ if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
406
+ try {
407
+ const parsed = new import_node_url.URL(u);
408
+ return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
409
+ } catch {
410
+ return u;
411
+ }
412
+ }
413
+ function isGitRepo(dir) {
414
+ return fs2.existsSync(path2.join(dir, ".git"));
415
+ }
416
+ function dirIsEmpty(dir) {
417
+ if (!fs2.existsSync(dir)) return true;
418
+ return fs2.readdirSync(dir).length === 0;
419
+ }
420
+ function cloneRepo(repoUrl, targetDir, hasGh) {
421
+ const isGithub = /github\.com/i.test(repoUrl);
422
+ let cmd;
423
+ let args;
424
+ if (hasGh && isGithub) {
425
+ cmd = "gh";
426
+ args = ["repo", "clone", repoUrl, targetDir];
427
+ info(`cloning via gh: ${repoUrl} \u2192 ${targetDir}`);
428
+ } else {
429
+ cmd = "git";
430
+ args = ["clone", repoUrl, targetDir];
431
+ info(`cloning via git: ${repoUrl} \u2192 ${targetDir}`);
432
+ }
433
+ if (DRY_RUN) {
434
+ dryNote(`would run: ${cmd} ${args.join(" ")}`);
435
+ return;
436
+ }
437
+ const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
438
+ if (res.status !== 0) {
439
+ fail(
440
+ `Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\` or an SSH key on your GitHub account.`
441
+ );
442
+ }
443
+ ok(`cloned to ${targetDir}`);
444
+ }
445
+ function writeConfigFile(targetDir, cfg, courseName) {
446
+ migrateLegacyCredFile(targetDir);
447
+ const p = path2.join(targetDir, CONFIG_FILENAME);
448
+ const existing = readCredFile(targetDir);
449
+ const isNew = existing === null;
450
+ const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
451
+ if (DRY_RUN) {
452
+ const verb = isNew ? "write" : isUpdate ? "update course" : "add course";
453
+ dryNote(`would ${verb} "${courseName}" in ${CONFIG_FILENAME} (org=${cfg.orgSlug}, project=${cfg.projectSlug}, url=${cfg.serverUrl})`);
454
+ return;
455
+ }
456
+ const nested = upsertProfile(existing, courseName, cfg);
457
+ writeJsonAtomic(p, nested, 384);
458
+ const action = isNew ? "wrote" : isUpdate ? `updated course "${courseName}" in` : `added course "${courseName}" to`;
459
+ ok(`${action} ${CONFIG_FILENAME} (active: ${courseName})`);
460
+ }
461
+ function migrateLegacyCredFile(targetDir) {
462
+ const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
463
+ const dest = path2.join(targetDir, CONFIG_FILENAME);
464
+ if (!fs2.existsSync(legacy) || fs2.existsSync(dest)) return;
465
+ let parsed;
466
+ try {
467
+ parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
468
+ } catch {
469
+ return;
470
+ }
471
+ const pat = parsed?.pat;
472
+ if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
473
+ if (DRY_RUN) {
474
+ dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
475
+ return;
476
+ }
477
+ fs2.renameSync(legacy, dest);
478
+ removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME);
479
+ ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
480
+ }
481
+ function removeGitignoreLine(targetDir, line) {
482
+ const p = path2.join(targetDir, ".gitignore");
483
+ if (!fs2.existsSync(p)) return;
484
+ const before = fs2.readFileSync(p, "utf-8");
485
+ const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
486
+ if (after === before) return;
487
+ if (DRY_RUN) {
488
+ dryNote(`would remove "${line}" from .gitignore`);
489
+ return;
490
+ }
491
+ fs2.writeFileSync(p, after, "utf-8");
492
+ ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
493
+ }
494
+ var LAUNCH_SECURE_HEADERS_HELPER = `node -e 'const j=JSON.parse(require("fs").readFileSync(".launch-secure.cred.config","utf-8"));const c=j.profiles&&j.active?j.profiles[j.active]:j;process.stdout.write(JSON.stringify({Authorization:"Bearer "+c.pat,"X-Org-Slug":c.orgSlug,"X-Project-Slug":c.projectSlug}))'`;
495
+ function buildLaunchKitMcpEntries(cfg) {
496
+ return {
497
+ "launch-secure": {
498
+ type: "http",
499
+ url: `${cfg.serverUrl}/api/mcp/project`,
500
+ headersHelper: LAUNCH_SECURE_HEADERS_HELPER
501
+ },
502
+ "launch-chart": {
503
+ command: "npx",
504
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
505
+ env: { LAUNCH_CHART_AUTOSERVE: "1" }
506
+ },
507
+ "launch-deck": {
508
+ command: "npx",
509
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-deck"]
510
+ },
511
+ "launch-orbit": {
512
+ command: "npx",
513
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-orbit", "mcp"]
514
+ },
515
+ "launch-recall": {
516
+ command: "npx",
517
+ args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "mcp"]
518
+ }
519
+ };
520
+ }
521
+ function mergeMcpFile(targetDir, launchKitEntries) {
522
+ const p = path2.join(targetDir, ".mcp.json");
523
+ const hadExisting = fs2.existsSync(p);
524
+ let existing = {};
525
+ if (hadExisting) {
526
+ try {
527
+ existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
528
+ } catch (err) {
529
+ fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
530
+ }
531
+ }
532
+ const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
533
+ const merged = { ...existing, mcpServers: { ...existing.mcpServers ?? {} } };
534
+ const overwrites = [];
535
+ const additions = [];
536
+ for (const [name, entry] of Object.entries(launchKitEntries)) {
537
+ if (merged.mcpServers[name]) overwrites.push(name);
538
+ else additions.push(name);
539
+ merged.mcpServers[name] = entry;
540
+ }
541
+ if (DRY_RUN) {
542
+ const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
543
+ dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
544
+ return;
545
+ }
546
+ fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
547
+ const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
548
+ ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
549
+ }
550
+ function detectPackageManager(repoDir) {
551
+ const pkgPath = path2.join(repoDir, "package.json");
552
+ if (!fs2.existsSync(pkgPath)) return null;
553
+ try {
554
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
555
+ if (typeof pkg.packageManager === "string") {
556
+ const name = pkg.packageManager.split("@")[0];
557
+ const match = PACKAGE_MANAGERS.find((p) => p.name === name);
558
+ if (match) return { pm: match, source: `package.json packageManager (${pkg.packageManager})` };
559
+ info(`packageManager field "${pkg.packageManager}" is not recognized \u2014 falling back to lockfile detection`);
560
+ }
561
+ } catch {
562
+ }
563
+ const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs2.existsSync(path2.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
564
+ if (matches.length === 1) {
565
+ return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
566
+ }
567
+ if (matches.length > 1) {
568
+ info(`multiple lockfiles found (${matches.map((m) => m.lockfile).join(", ")}) \u2014 picking ${matches[0].pm.name} by precedence`);
569
+ return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
570
+ }
571
+ for (const pm of PACKAGE_MANAGERS) {
572
+ if (pm.workspaceFiles?.some((wf) => fs2.existsSync(path2.join(repoDir, wf)))) {
573
+ return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs2.existsSync(path2.join(repoDir, wf)))})` };
574
+ }
575
+ }
576
+ const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
577
+ return { pm: npm, source: "default (no signal found)" };
578
+ }
579
+ function runInstall(repoDir, detected) {
580
+ const { pm } = detected;
581
+ if (!which(pm.binary)) {
582
+ fail(
583
+ `${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
584
+ );
585
+ }
586
+ info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
587
+ if (DRY_RUN) {
588
+ dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
589
+ return;
590
+ }
591
+ const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
592
+ if (res.status !== 0) {
593
+ fail(
594
+ `${pm.name} install failed (exit ${res.status}). Configs and clone are intact \u2014 fix the underlying error and retry: cd ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
595
+ );
596
+ }
597
+ ok(`${pm.name} install complete`);
598
+ }
599
+ function hasOnboardScript(repoDir) {
600
+ const pkgPath = path2.join(repoDir, "package.json");
601
+ if (!fs2.existsSync(pkgPath)) return false;
602
+ try {
603
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
604
+ return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
605
+ } catch {
606
+ return false;
607
+ }
608
+ }
609
+ function runRecallInit(repoDir) {
610
+ info(`scaffolding launch-recall (shadow git backup) \u2026`);
611
+ const recallEntry = path2.resolve(__dirname, "recall-entry.js");
612
+ const useSibling = fs2.existsSync(recallEntry);
613
+ const cmd = useSibling ? process.execPath : "npx";
614
+ const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
615
+ if (DRY_RUN) {
616
+ dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
617
+ return;
618
+ }
619
+ const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
620
+ if (res.status !== 0) {
621
+ info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path2.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
622
+ return;
623
+ }
624
+ ok(`launch-recall ready (shadow git initialized)`);
625
+ }
626
+ function runOnboard(repoDir, pm) {
627
+ info(`running ${pm.binary} run ${ONBOARD_SCRIPT_NAME} \u2026`);
628
+ if (DRY_RUN) {
629
+ dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
630
+ return;
631
+ }
632
+ const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
633
+ if (res.status !== 0) {
634
+ fail(
635
+ `${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path2.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
636
+ );
637
+ }
638
+ ok(`${ONBOARD_SCRIPT_NAME} script complete`);
639
+ }
640
+ function ensureGitignoreLine(targetDir, line) {
641
+ const p = path2.join(targetDir, ".gitignore");
642
+ let content = fs2.existsSync(p) ? fs2.readFileSync(p, "utf-8") : "";
643
+ const lines = content.split(/\r?\n/);
644
+ if (lines.some((l) => l.trim() === line)) return;
645
+ if (content.length && !content.endsWith("\n")) content += "\n";
646
+ content += `${line}
647
+ `;
648
+ if (DRY_RUN) {
649
+ dryNote(`would append "${line}" to .gitignore`);
650
+ return;
651
+ }
652
+ fs2.writeFileSync(p, content, "utf-8");
653
+ ok(`appended ${line} to .gitignore`);
654
+ }
655
+ function copyScaffoldIfMissing(srcPath, destPath, label) {
656
+ if (!fs2.existsSync(srcPath)) return "missing-src";
657
+ if (fs2.existsSync(destPath)) {
658
+ info(`${label} already present \u2014 leaving alone`);
659
+ return "existed";
660
+ }
661
+ if (DRY_RUN) {
662
+ dryNote(`would write ${label}`);
663
+ return "wrote";
664
+ }
665
+ fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
666
+ fs2.copyFileSync(srcPath, destPath);
667
+ try {
668
+ const srcMode = fs2.statSync(srcPath).mode;
669
+ fs2.chmodSync(destPath, srcMode);
670
+ } catch {
671
+ }
672
+ ok(`wrote ${label}`);
673
+ return "wrote";
674
+ }
675
+ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
676
+ if (!fs2.existsSync(srcDir)) return;
677
+ for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
678
+ const srcPath = path2.join(srcDir, entry.name);
679
+ const destPath = path2.join(destDir, entry.name);
680
+ const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
681
+ if (entry.isDirectory()) {
682
+ copyScaffoldDirAlways(srcPath, destPath, label);
683
+ } else if (entry.isFile()) {
684
+ const existed = fs2.existsSync(destPath);
685
+ if (DRY_RUN) {
686
+ dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
687
+ continue;
688
+ }
689
+ fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
690
+ fs2.copyFileSync(srcPath, destPath);
691
+ try {
692
+ const srcMode = fs2.statSync(srcPath).mode;
693
+ fs2.chmodSync(destPath, srcMode);
694
+ } catch {
695
+ }
696
+ ok(`${existed ? "refreshed" : "wrote"} ${label}`);
697
+ }
698
+ }
699
+ }
700
+ function scaffoldMigrateSafety(targetDir) {
701
+ const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
702
+ if (!fs2.existsSync(scaffoldsRoot)) {
703
+ info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
704
+ return;
705
+ }
706
+ const files = [
707
+ {
708
+ src: path2.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
709
+ dest: path2.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
710
+ label: ".github/workflows/backup-on-migration.yml"
711
+ },
712
+ {
713
+ src: path2.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
714
+ dest: path2.join(targetDir, "scripts", "migrate-with-backup.sh"),
715
+ label: "scripts/migrate-with-backup.sh"
716
+ },
717
+ {
718
+ src: path2.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
719
+ dest: path2.join(targetDir, "docs", "migrations-runbook.md"),
720
+ label: "docs/migrations-runbook.md"
721
+ }
722
+ ];
723
+ info("scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook) \u2026");
724
+ for (const f of files) copyScaffoldIfMissing(f.src, f.dest, f.label);
725
+ ensureGitignoreLine(targetDir, ".backups/");
726
+ ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
727
+ }
728
+ var MARKETPLACE_ID = "launchsecure";
729
+ var LS_PLUGIN_ID = "ls";
730
+ function scaffoldLsMarketplace(targetDir) {
731
+ const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
732
+ if (!fs2.existsSync(scaffoldsRoot)) {
733
+ info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
734
+ return;
735
+ }
736
+ const marketplaceRoot = path2.join(targetDir, ".claude", "marketplace");
737
+ info("scaffolding ls marketplace (Claude Code /ls: namespace \u2014 refreshes every /ls:* command found in the scaffold) \u2026");
738
+ copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
739
+ wireLsSettings(targetDir);
740
+ ok(`ls marketplace ready \u2014 open this repo in Claude Code, approve the "${MARKETPLACE_ID}" marketplace prompt, then try /ls:activate-beacon, /ls:standup, or /ls:show-mcp-status`);
741
+ }
742
+ function wireLsSettings(targetDir) {
743
+ const p = path2.join(targetDir, ".claude", "settings.json");
744
+ const hadExisting = fs2.existsSync(p);
745
+ let existing = {};
746
+ if (hadExisting) {
747
+ try {
748
+ existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
749
+ } catch (err) {
750
+ fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
751
+ }
752
+ }
753
+ const merged = { ...existing };
754
+ merged.extraKnownMarketplaces = {
755
+ ...existing.extraKnownMarketplaces ?? {},
756
+ [MARKETPLACE_ID]: {
757
+ source: { source: "directory", path: "./.claude/marketplace" }
758
+ }
759
+ };
760
+ merged.enabledPlugins = {
761
+ ...existing.enabledPlugins ?? {},
762
+ [`${LS_PLUGIN_ID}@${MARKETPLACE_ID}`]: true
763
+ };
764
+ if (DRY_RUN) {
765
+ dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
766
+ return;
767
+ }
768
+ fs2.mkdirSync(path2.dirname(p), { recursive: true });
769
+ fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
770
+ ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID})`);
771
+ }
772
+ var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
773
+ var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
774
+ function scaffoldRecallHook(targetDir) {
775
+ const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
776
+ if (!fs2.existsSync(scaffoldsRoot)) {
777
+ info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
778
+ return;
779
+ }
780
+ info("scaffolding recall-hook (SessionStart watcher-respawn hook + ensure-recall.sh) \u2026");
781
+ copyScaffoldDirAlways(scaffoldsRoot, targetDir, "");
782
+ wireRecallHook(targetDir);
783
+ ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
784
+ }
785
+ function wireRecallHook(targetDir) {
786
+ const p = path2.join(targetDir, ".claude", "settings.json");
787
+ const hadExisting = fs2.existsSync(p);
788
+ let existing = {};
789
+ if (hadExisting) {
790
+ try {
791
+ existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
792
+ } catch (err) {
793
+ fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
794
+ }
795
+ }
796
+ const hooks = existing.hooks ?? {};
797
+ const sessionStart = hooks.SessionStart ?? [];
798
+ const alreadyWired = sessionStart.some(
799
+ (group) => (group.hooks ?? []).some((h) => typeof h?.command === "string" && h.command.includes(RECALL_HOOK_SIGNATURE))
800
+ );
801
+ if (alreadyWired) {
802
+ info(".claude/settings.json SessionStart hook already references ensure-recall.sh \u2014 leaving alone");
803
+ return;
804
+ }
805
+ const newGroup = {
806
+ hooks: [{ type: "command", command: RECALL_HOOK_COMMAND }]
807
+ };
808
+ const merged = {
809
+ ...existing,
810
+ hooks: {
811
+ ...hooks,
812
+ SessionStart: [...sessionStart, newGroup]
813
+ }
814
+ };
815
+ if (DRY_RUN) {
816
+ dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
817
+ return;
818
+ }
819
+ fs2.mkdirSync(path2.dirname(p), { recursive: true });
820
+ fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
821
+ ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
822
+ }
823
+ async function main() {
824
+ const args = parseArgs(process.argv.slice(2));
825
+ if (args.help) {
826
+ printHelp();
827
+ return;
828
+ }
829
+ const subcommand = process.argv[2];
830
+ if (subcommand && subcommand !== "init" && !subcommand.startsWith("--")) {
831
+ fail(`Unknown subcommand "${subcommand}". Only "init" is supported. Run with --help for usage.`);
832
+ }
833
+ DRY_RUN = args.dryRun;
834
+ if (DRY_RUN) {
835
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
836
+ info("DRY RUN \u2014 no files will be written, no commands will run.");
837
+ info("Lines tagged (dry-run) show what would happen.");
838
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
839
+ }
840
+ if (!args.token) {
841
+ const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
842
+ args.token = t || null;
843
+ }
844
+ if (!args.token) fail("--token (or LS_PAT env) is required.");
845
+ if (!/^ls_pat_/.test(args.token)) fail("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
846
+ if (!args.orgSlug) fail("--org=<orgSlug> is required.");
847
+ if (!args.projectSlug) fail("--project=<projectSlug> is required.");
848
+ const { hasGh } = preflight();
849
+ info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
850
+ let resolved;
851
+ try {
852
+ resolved = await callProjectInfo(args);
853
+ } catch (err) {
854
+ fail(err instanceof Error ? err.message : String(err));
855
+ }
856
+ ok(`resolved "${resolved.projectName}"`);
857
+ if (!resolved.repositoryUrl) {
858
+ fail(
859
+ `Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
860
+ );
861
+ }
862
+ const repoUrl = resolved.repositoryUrl;
863
+ const cwd = process.cwd();
864
+ const targetDir = path2.resolve(args.targetDir ?? path2.join(cwd, resolved.projectSlug));
865
+ const normalizedRemote = normalizeRepoUrl(repoUrl);
866
+ let skipClone = false;
867
+ if (fs2.existsSync(targetDir)) {
868
+ if (isGitRepo(targetDir)) {
869
+ const existingRemote = gitRemoteUrl(targetDir);
870
+ if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
871
+ ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
872
+ skipClone = true;
873
+ } else {
874
+ fail(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
875
+ }
876
+ } else if (!dirIsEmpty(targetDir)) {
877
+ fail(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
878
+ }
879
+ }
880
+ if (!skipClone) cloneRepo(repoUrl, targetDir, hasGh);
881
+ const cfg = {
882
+ pat: args.token,
883
+ orgSlug: resolved.orgSlug,
884
+ projectSlug: resolved.projectSlug,
885
+ serverUrl: args.serverUrl
886
+ };
887
+ const courseName = args.course ?? inferCourseName(cfg.serverUrl);
888
+ writeConfigFile(targetDir, cfg, courseName);
889
+ mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
890
+ ensureGitignoreLine(targetDir, CONFIG_FILENAME);
891
+ let installSkippedReason = null;
892
+ const detected = detectPackageManager(targetDir);
893
+ if (detected) info(`detected package manager: ${detected.pm.name} (${detected.source})`);
894
+ if (args.noInstall) {
895
+ installSkippedReason = "--no-install passed";
896
+ } else if (!detected) {
897
+ installSkippedReason = "no package.json found";
898
+ } else {
899
+ runInstall(targetDir, detected);
900
+ if (hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
901
+ }
902
+ const hasOnboard = hasOnboardScript(targetDir);
903
+ if (!args.noRecall) runRecallInit(targetDir);
904
+ if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
905
+ if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
906
+ if (!args.noRecallHook) scaffoldRecallHook(targetDir);
907
+ const relTarget = path2.relative(cwd, targetDir) || ".";
908
+ console.log("");
909
+ if (DRY_RUN) {
910
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
911
+ info(`DRY RUN COMPLETE \u2014 no files were modified, no commands ran.`);
912
+ info(`Target: ${targetDir}`);
913
+ info(`Re-run without --dry-run to apply the changes shown above.`);
914
+ info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
915
+ return;
916
+ }
917
+ ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
918
+ if (installSkippedReason) {
919
+ const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
920
+ const onboardLine = hasOnboard && detected ? `
921
+ ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
922
+ console.log(`
923
+ Next steps (install skipped: ${installSkippedReason}):
924
+ cd ${relTarget}
925
+ ${installLine}${onboardLine}
926
+ claude # launch Claude Code (5 MCPs wired)
927
+ ${LAUNCH_KIT_TOOLS_GUIDE}`);
928
+ } else {
929
+ console.log(`
930
+ Next steps:
931
+ cd ${relTarget}
932
+ claude # launch Claude Code (5 MCPs wired)
933
+ ${LAUNCH_KIT_TOOLS_GUIDE}`);
934
+ }
935
+ }
936
+ main().catch((err) => {
937
+ console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
938
+ process.exit(1);
939
+ });