@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.
- package/dist/beacon/beacon.mjs +1003 -440
- package/dist/beacon/beacon.mjs.map +1 -1
- package/dist/beacon/beacon.umd.js +45 -24
- package/dist/beacon/beacon.umd.js.map +1 -1
- package/dist/beacon/types/capture/events.d.ts +20 -0
- package/dist/beacon/types/capture/events.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +1 -0
- package/dist/beacon/types/element.d.ts.map +1 -1
- package/dist/beacon/types/index.d.ts +2 -1
- package/dist/beacon/types/index.d.ts.map +1 -1
- package/dist/beacon/types/monitor/dom.d.ts +13 -0
- package/dist/beacon/types/monitor/dom.d.ts.map +1 -0
- package/dist/beacon/types/monitor/index.d.ts +19 -0
- package/dist/beacon/types/monitor/index.d.ts.map +1 -0
- package/dist/beacon/types/monitor/network.d.ts +12 -0
- package/dist/beacon/types/monitor/network.d.ts.map +1 -0
- package/dist/beacon/types/monitor/transport.d.ts +27 -0
- package/dist/beacon/types/monitor/transport.d.ts.map +1 -0
- package/dist/beacon/types/monitor/types.d.ts +117 -0
- package/dist/beacon/types/monitor/types.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +10 -0
- package/dist/beacon/types/types.d.ts.map +1 -1
- package/dist/beacon/types/ui/drawer.d.ts +3 -1
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -1
- package/dist/beacon/types/ui/monitor-panel.d.ts +19 -0
- package/dist/beacon/types/ui/monitor-panel.d.ts.map +1 -0
- package/dist/server/beacon-monitor-entry.js +353 -0
- package/dist/server/cli.js +50 -2
- package/dist/server/council-entry.js +0 -0
- package/dist/server/course-entry.js +246 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +394 -64
- package/dist/server/orbit-entry.js +187 -24
- package/package.json +24 -23
- package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
- package/scaffolds/ls-marketplace/plugins/ls/.claude-plugin/plugin.json +28 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/activate-beacon.md +216 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-array.md +92 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-clear.md +68 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-pulse.md +80 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-scan.md +62 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/show-mcp-status.md +109 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/standup.md +177 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -0
|
@@ -25,14 +25,78 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/server/init-entry.ts
|
|
27
27
|
var import_node_child_process = require("node:child_process");
|
|
28
|
-
var
|
|
28
|
+
var fs2 = __toESM(require("node:fs"));
|
|
29
29
|
var import_node_http = require("node:http");
|
|
30
30
|
var import_node_https = require("node:https");
|
|
31
|
-
var
|
|
31
|
+
var path2 = __toESM(require("node:path"));
|
|
32
32
|
var readline = __toESM(require("node:readline"));
|
|
33
33
|
var import_node_url = require("node:url");
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// src/server/cred-shape.ts
|
|
36
|
+
var fs = __toESM(require("node:fs"));
|
|
37
|
+
var path = __toESM(require("node:path"));
|
|
35
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";
|
|
36
100
|
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
37
101
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
38
102
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
@@ -47,6 +111,38 @@ Wired in Claude Code (.mcp.json):
|
|
|
47
111
|
Other tools (run on demand via npx):
|
|
48
112
|
npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
|
|
49
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.
|
|
50
146
|
`;
|
|
51
147
|
var PACKAGE_MANAGERS = [
|
|
52
148
|
{ name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
|
|
@@ -61,9 +157,13 @@ function parseArgs(argv) {
|
|
|
61
157
|
projectSlug: null,
|
|
62
158
|
serverUrl: DEFAULT_SERVER_URL,
|
|
63
159
|
targetDir: null,
|
|
160
|
+
course: null,
|
|
64
161
|
noInstall: false,
|
|
65
162
|
noRecall: false,
|
|
66
163
|
noMigrateSafety: false,
|
|
164
|
+
noLsMarketplace: false,
|
|
165
|
+
noRecallHook: false,
|
|
166
|
+
dryRun: false,
|
|
67
167
|
help: false
|
|
68
168
|
};
|
|
69
169
|
for (const raw of argv) {
|
|
@@ -83,6 +183,18 @@ function parseArgs(argv) {
|
|
|
83
183
|
args.noMigrateSafety = true;
|
|
84
184
|
continue;
|
|
85
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
|
+
}
|
|
86
198
|
const eq = raw.indexOf("=");
|
|
87
199
|
if (!raw.startsWith("--") || eq < 0) continue;
|
|
88
200
|
const key = raw.slice(2, eq);
|
|
@@ -92,6 +204,7 @@ function parseArgs(argv) {
|
|
|
92
204
|
else if (key === "project") args.projectSlug = val;
|
|
93
205
|
else if (key === "url") args.serverUrl = val.replace(/\/+$/, "");
|
|
94
206
|
else if (key === "dir") args.targetDir = val;
|
|
207
|
+
else if (key === "course") args.course = val;
|
|
95
208
|
}
|
|
96
209
|
return args;
|
|
97
210
|
}
|
|
@@ -109,10 +222,30 @@ Required:
|
|
|
109
222
|
Options:
|
|
110
223
|
--url=<serverUrl> LaunchSecure base URL (default: ${DEFAULT_SERVER_URL}).
|
|
111
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.
|
|
112
232
|
--no-install Skip dependency install step.
|
|
113
233
|
--no-recall Skip launch-recall (shadow git backup) scaffold.
|
|
114
234
|
--no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
|
|
115
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).
|
|
116
249
|
--help Show this help.
|
|
117
250
|
|
|
118
251
|
What it does:
|
|
@@ -132,6 +265,15 @@ What it does:
|
|
|
132
265
|
8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
|
|
133
266
|
9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
|
|
134
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.
|
|
135
277
|
`);
|
|
136
278
|
}
|
|
137
279
|
async function prompt(question) {
|
|
@@ -151,6 +293,10 @@ function info(msg) {
|
|
|
151
293
|
function ok(msg) {
|
|
152
294
|
console.log(`[launch-kit] \u2713 ${msg}`);
|
|
153
295
|
}
|
|
296
|
+
var DRY_RUN = false;
|
|
297
|
+
function dryNote(msg) {
|
|
298
|
+
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
299
|
+
}
|
|
154
300
|
function which(bin) {
|
|
155
301
|
const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
156
302
|
if (res.status !== 0) return null;
|
|
@@ -265,11 +411,11 @@ function normalizeRepoUrl(url) {
|
|
|
265
411
|
}
|
|
266
412
|
}
|
|
267
413
|
function isGitRepo(dir) {
|
|
268
|
-
return
|
|
414
|
+
return fs2.existsSync(path2.join(dir, ".git"));
|
|
269
415
|
}
|
|
270
416
|
function dirIsEmpty(dir) {
|
|
271
|
-
if (!
|
|
272
|
-
return
|
|
417
|
+
if (!fs2.existsSync(dir)) return true;
|
|
418
|
+
return fs2.readdirSync(dir).length === 0;
|
|
273
419
|
}
|
|
274
420
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
275
421
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -284,6 +430,10 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
284
430
|
args = ["clone", repoUrl, targetDir];
|
|
285
431
|
info(`cloning via git: ${repoUrl} \u2192 ${targetDir}`);
|
|
286
432
|
}
|
|
433
|
+
if (DRY_RUN) {
|
|
434
|
+
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
287
437
|
const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
288
438
|
if (res.status !== 0) {
|
|
289
439
|
fail(
|
|
@@ -292,43 +442,56 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
292
442
|
}
|
|
293
443
|
ok(`cloned to ${targetDir}`);
|
|
294
444
|
}
|
|
295
|
-
function writeConfigFile(targetDir, cfg) {
|
|
445
|
+
function writeConfigFile(targetDir, cfg, courseName) {
|
|
296
446
|
migrateLegacyCredFile(targetDir);
|
|
297
|
-
const p =
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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;
|
|
303
455
|
}
|
|
304
|
-
|
|
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})`);
|
|
305
460
|
}
|
|
306
461
|
function migrateLegacyCredFile(targetDir) {
|
|
307
|
-
const legacy =
|
|
308
|
-
const dest =
|
|
309
|
-
if (!
|
|
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;
|
|
310
465
|
let parsed;
|
|
311
466
|
try {
|
|
312
|
-
parsed = JSON.parse(
|
|
467
|
+
parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
|
|
313
468
|
} catch {
|
|
314
469
|
return;
|
|
315
470
|
}
|
|
316
471
|
const pat = parsed?.pat;
|
|
317
472
|
if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
|
|
318
|
-
|
|
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);
|
|
319
478
|
removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME);
|
|
320
479
|
ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
|
|
321
480
|
}
|
|
322
481
|
function removeGitignoreLine(targetDir, line) {
|
|
323
|
-
const p =
|
|
324
|
-
if (!
|
|
325
|
-
const before =
|
|
482
|
+
const p = path2.join(targetDir, ".gitignore");
|
|
483
|
+
if (!fs2.existsSync(p)) return;
|
|
484
|
+
const before = fs2.readFileSync(p, "utf-8");
|
|
326
485
|
const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
|
|
327
486
|
if (after === before) return;
|
|
328
|
-
|
|
487
|
+
if (DRY_RUN) {
|
|
488
|
+
dryNote(`would remove "${line}" from .gitignore`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
fs2.writeFileSync(p, after, "utf-8");
|
|
329
492
|
ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
|
|
330
493
|
}
|
|
331
|
-
var LAUNCH_SECURE_HEADERS_HELPER = `node -e 'const j=JSON.parse(require("fs").readFileSync(".launch-secure.cred.config","utf-8"));process.stdout.write(JSON.stringify({Authorization:"Bearer "+
|
|
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}))'`;
|
|
332
495
|
function buildLaunchKitMcpEntries(cfg) {
|
|
333
496
|
return {
|
|
334
497
|
"launch-secure": {
|
|
@@ -356,30 +519,39 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
356
519
|
};
|
|
357
520
|
}
|
|
358
521
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
359
|
-
const p =
|
|
360
|
-
const hadExisting =
|
|
522
|
+
const p = path2.join(targetDir, ".mcp.json");
|
|
523
|
+
const hadExisting = fs2.existsSync(p);
|
|
361
524
|
let existing = {};
|
|
362
525
|
if (hadExisting) {
|
|
363
526
|
try {
|
|
364
|
-
existing = JSON.parse(
|
|
527
|
+
existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
365
528
|
} catch (err) {
|
|
366
529
|
fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
530
|
}
|
|
368
531
|
}
|
|
369
532
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
370
533
|
const merged = { ...existing, mcpServers: { ...existing.mcpServers ?? {} } };
|
|
534
|
+
const overwrites = [];
|
|
535
|
+
const additions = [];
|
|
371
536
|
for (const [name, entry] of Object.entries(launchKitEntries)) {
|
|
537
|
+
if (merged.mcpServers[name]) overwrites.push(name);
|
|
538
|
+
else additions.push(name);
|
|
372
539
|
merged.mcpServers[name] = entry;
|
|
373
540
|
}
|
|
374
|
-
|
|
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");
|
|
375
547
|
const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
|
|
376
548
|
ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
377
549
|
}
|
|
378
550
|
function detectPackageManager(repoDir) {
|
|
379
|
-
const pkgPath =
|
|
380
|
-
if (!
|
|
551
|
+
const pkgPath = path2.join(repoDir, "package.json");
|
|
552
|
+
if (!fs2.existsSync(pkgPath)) return null;
|
|
381
553
|
try {
|
|
382
|
-
const pkg = JSON.parse(
|
|
554
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
383
555
|
if (typeof pkg.packageManager === "string") {
|
|
384
556
|
const name = pkg.packageManager.split("@")[0];
|
|
385
557
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -388,7 +560,7 @@ function detectPackageManager(repoDir) {
|
|
|
388
560
|
}
|
|
389
561
|
} catch {
|
|
390
562
|
}
|
|
391
|
-
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) =>
|
|
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);
|
|
392
564
|
if (matches.length === 1) {
|
|
393
565
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
394
566
|
}
|
|
@@ -397,8 +569,8 @@ function detectPackageManager(repoDir) {
|
|
|
397
569
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
398
570
|
}
|
|
399
571
|
for (const pm of PACKAGE_MANAGERS) {
|
|
400
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
401
|
-
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) =>
|
|
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)))})` };
|
|
402
574
|
}
|
|
403
575
|
}
|
|
404
576
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -408,23 +580,27 @@ function runInstall(repoDir, detected) {
|
|
|
408
580
|
const { pm } = detected;
|
|
409
581
|
if (!which(pm.binary)) {
|
|
410
582
|
fail(
|
|
411
|
-
`${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 ${
|
|
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(" ")}`
|
|
412
584
|
);
|
|
413
585
|
}
|
|
414
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
|
+
}
|
|
415
591
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
416
592
|
if (res.status !== 0) {
|
|
417
593
|
fail(
|
|
418
|
-
`${pm.name} install failed (exit ${res.status}). Configs and clone are intact \u2014 fix the underlying error and retry: cd ${
|
|
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(" ")}`
|
|
419
595
|
);
|
|
420
596
|
}
|
|
421
597
|
ok(`${pm.name} install complete`);
|
|
422
598
|
}
|
|
423
599
|
function hasOnboardScript(repoDir) {
|
|
424
|
-
const pkgPath =
|
|
425
|
-
if (!
|
|
600
|
+
const pkgPath = path2.join(repoDir, "package.json");
|
|
601
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
426
602
|
try {
|
|
427
|
-
const pkg = JSON.parse(
|
|
603
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
428
604
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
429
605
|
} catch {
|
|
430
606
|
return false;
|
|
@@ -432,74 +608,115 @@ function hasOnboardScript(repoDir) {
|
|
|
432
608
|
}
|
|
433
609
|
function runRecallInit(repoDir) {
|
|
434
610
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
435
|
-
const recallEntry =
|
|
436
|
-
const useSibling =
|
|
611
|
+
const recallEntry = path2.resolve(__dirname, "recall-entry.js");
|
|
612
|
+
const useSibling = fs2.existsSync(recallEntry);
|
|
437
613
|
const cmd = useSibling ? process.execPath : "npx";
|
|
438
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
|
+
}
|
|
439
619
|
const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
440
620
|
if (res.status !== 0) {
|
|
441
|
-
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${
|
|
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`);
|
|
442
622
|
return;
|
|
443
623
|
}
|
|
444
624
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
445
625
|
}
|
|
446
626
|
function runOnboard(repoDir, pm) {
|
|
447
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
|
+
}
|
|
448
632
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
449
633
|
if (res.status !== 0) {
|
|
450
634
|
fail(
|
|
451
|
-
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${
|
|
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}`
|
|
452
636
|
);
|
|
453
637
|
}
|
|
454
638
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
455
639
|
}
|
|
456
640
|
function ensureGitignoreLine(targetDir, line) {
|
|
457
|
-
const p =
|
|
458
|
-
let content =
|
|
641
|
+
const p = path2.join(targetDir, ".gitignore");
|
|
642
|
+
let content = fs2.existsSync(p) ? fs2.readFileSync(p, "utf-8") : "";
|
|
459
643
|
const lines = content.split(/\r?\n/);
|
|
460
644
|
if (lines.some((l) => l.trim() === line)) return;
|
|
461
645
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
462
646
|
content += `${line}
|
|
463
647
|
`;
|
|
464
|
-
|
|
648
|
+
if (DRY_RUN) {
|
|
649
|
+
dryNote(`would append "${line}" to .gitignore`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
fs2.writeFileSync(p, content, "utf-8");
|
|
465
653
|
ok(`appended ${line} to .gitignore`);
|
|
466
654
|
}
|
|
467
655
|
function copyScaffoldIfMissing(srcPath, destPath, label) {
|
|
468
|
-
if (!
|
|
469
|
-
if (
|
|
656
|
+
if (!fs2.existsSync(srcPath)) return "missing-src";
|
|
657
|
+
if (fs2.existsSync(destPath)) {
|
|
470
658
|
info(`${label} already present \u2014 leaving alone`);
|
|
471
659
|
return "existed";
|
|
472
660
|
}
|
|
473
|
-
|
|
474
|
-
|
|
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);
|
|
475
667
|
try {
|
|
476
|
-
const srcMode =
|
|
477
|
-
|
|
668
|
+
const srcMode = fs2.statSync(srcPath).mode;
|
|
669
|
+
fs2.chmodSync(destPath, srcMode);
|
|
478
670
|
} catch {
|
|
479
671
|
}
|
|
480
672
|
ok(`wrote ${label}`);
|
|
481
673
|
return "wrote";
|
|
482
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
|
+
}
|
|
483
700
|
function scaffoldMigrateSafety(targetDir) {
|
|
484
|
-
const scaffoldsRoot =
|
|
485
|
-
if (!
|
|
701
|
+
const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
702
|
+
if (!fs2.existsSync(scaffoldsRoot)) {
|
|
486
703
|
info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
487
704
|
return;
|
|
488
705
|
}
|
|
489
706
|
const files = [
|
|
490
707
|
{
|
|
491
|
-
src:
|
|
492
|
-
dest:
|
|
708
|
+
src: path2.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
709
|
+
dest: path2.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
493
710
|
label: ".github/workflows/backup-on-migration.yml"
|
|
494
711
|
},
|
|
495
712
|
{
|
|
496
|
-
src:
|
|
497
|
-
dest:
|
|
713
|
+
src: path2.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
714
|
+
dest: path2.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
498
715
|
label: "scripts/migrate-with-backup.sh"
|
|
499
716
|
},
|
|
500
717
|
{
|
|
501
|
-
src:
|
|
502
|
-
dest:
|
|
718
|
+
src: path2.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
719
|
+
dest: path2.join(targetDir, "docs", "migrations-runbook.md"),
|
|
503
720
|
label: "docs/migrations-runbook.md"
|
|
504
721
|
}
|
|
505
722
|
];
|
|
@@ -508,6 +725,101 @@ function scaffoldMigrateSafety(targetDir) {
|
|
|
508
725
|
ensureGitignoreLine(targetDir, ".backups/");
|
|
509
726
|
ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
|
|
510
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
|
+
}
|
|
511
823
|
async function main() {
|
|
512
824
|
const args = parseArgs(process.argv.slice(2));
|
|
513
825
|
if (args.help) {
|
|
@@ -518,6 +830,13 @@ async function main() {
|
|
|
518
830
|
if (subcommand && subcommand !== "init" && !subcommand.startsWith("--")) {
|
|
519
831
|
fail(`Unknown subcommand "${subcommand}". Only "init" is supported. Run with --help for usage.`);
|
|
520
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
|
+
}
|
|
521
840
|
if (!args.token) {
|
|
522
841
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
523
842
|
args.token = t || null;
|
|
@@ -542,10 +861,10 @@ async function main() {
|
|
|
542
861
|
}
|
|
543
862
|
const repoUrl = resolved.repositoryUrl;
|
|
544
863
|
const cwd = process.cwd();
|
|
545
|
-
const targetDir =
|
|
864
|
+
const targetDir = path2.resolve(args.targetDir ?? path2.join(cwd, resolved.projectSlug));
|
|
546
865
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
547
866
|
let skipClone = false;
|
|
548
|
-
if (
|
|
867
|
+
if (fs2.existsSync(targetDir)) {
|
|
549
868
|
if (isGitRepo(targetDir)) {
|
|
550
869
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
551
870
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
@@ -565,7 +884,8 @@ async function main() {
|
|
|
565
884
|
projectSlug: resolved.projectSlug,
|
|
566
885
|
serverUrl: args.serverUrl
|
|
567
886
|
};
|
|
568
|
-
|
|
887
|
+
const courseName = args.course ?? inferCourseName(cfg.serverUrl);
|
|
888
|
+
writeConfigFile(targetDir, cfg, courseName);
|
|
569
889
|
mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
|
|
570
890
|
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
571
891
|
let installSkippedReason = null;
|
|
@@ -582,8 +902,18 @@ async function main() {
|
|
|
582
902
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
583
903
|
if (!args.noRecall) runRecallInit(targetDir);
|
|
584
904
|
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
|
|
585
|
-
|
|
905
|
+
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
906
|
+
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
907
|
+
const relTarget = path2.relative(cwd, targetDir) || ".";
|
|
586
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
|
+
}
|
|
587
917
|
ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
|
|
588
918
|
if (installSkippedReason) {
|
|
589
919
|
const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
|