@launchsecure/launch-kit 0.0.28 → 0.0.29
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/server/chart-serve.js +3 -1
- package/dist/server/cli.js +231 -221
- package/dist/server/council-entry.js +0 -0
- package/dist/server/course-entry.js +3 -3
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph-mcp-entry.js +35 -72
- package/dist/server/init-entry.js +794 -195
- package/package.json +22 -21
- package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +4 -4
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/.claude-plugin/plugin.json +1 -10
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/activate-beacon.md +2 -2
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +46 -0
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-array.md +3 -3
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-clear.md +2 -2
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-pulse.md +7 -7
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/beacon-scan.md +7 -7
- package/scaffolds/ls-marketplace/plugins/kit/commands/deactivate-statusline.md +34 -0
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/show-mcp-status.md +6 -6
- package/scaffolds/ls-marketplace/plugins/{ls → kit}/commands/standup.md +52 -38
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
- package/scaffolds/statusline/statusline-mcp.sh +192 -0
- package/scaffolds/statusline/statusline-wrapper.sh +50 -0
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,12 +30,119 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
33
|
+
// src/server/statusline-install.ts
|
|
34
|
+
var statusline_install_exports = {};
|
|
35
|
+
__export(statusline_install_exports, {
|
|
36
|
+
activateStatusline: () => activateStatusline,
|
|
37
|
+
deactivateStatusline: () => deactivateStatusline
|
|
38
|
+
});
|
|
39
|
+
function readSettings() {
|
|
40
|
+
if (!fs3.existsSync(SETTINGS_PATH)) return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs3.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeSettings(s) {
|
|
48
|
+
fs3.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
|
|
49
|
+
fs3.writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
50
|
+
}
|
|
51
|
+
function readScaffold(name) {
|
|
52
|
+
const p = path3.resolve(__dirname, "..", "..", "scaffolds", "statusline", name);
|
|
53
|
+
return fs3.readFileSync(p, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
function writeScripts() {
|
|
56
|
+
fs3.mkdirSync(LK_DIR, { recursive: true });
|
|
57
|
+
fs3.writeFileSync(WRAPPER_PATH, readScaffold("statusline-wrapper.sh"), { mode: 493 });
|
|
58
|
+
fs3.writeFileSync(CHIP_PATH, readScaffold("statusline-mcp.sh"), { mode: 493 });
|
|
59
|
+
}
|
|
60
|
+
function wrapperCommand(opts) {
|
|
61
|
+
const env = [];
|
|
62
|
+
if (opts.show) env.push(`LK_STATUSLINE_SHOW=${opts.show}`);
|
|
63
|
+
if (opts.compact) env.push(`LK_STATUSLINE_COMPACT=1`);
|
|
64
|
+
const prefix = env.length > 0 ? env.join(" ") + " " : "";
|
|
65
|
+
return `${prefix}bash ${WRAPPER_PATH}`;
|
|
66
|
+
}
|
|
67
|
+
function activateStatusline(opts = {}) {
|
|
68
|
+
const settings = readSettings();
|
|
69
|
+
if (!settings) {
|
|
70
|
+
return { ok: false, outcome: "no-settings", message: `no ~/.claude/settings.json \u2014 nothing to wrap` };
|
|
71
|
+
}
|
|
72
|
+
const currentCmd = settings.statusLine?.command;
|
|
73
|
+
const alreadyWrapped = typeof currentCmd === "string" && currentCmd.includes(WRAPPER_PATH);
|
|
74
|
+
if (alreadyWrapped) {
|
|
75
|
+
writeScripts();
|
|
76
|
+
if (opts.show !== void 0 || opts.compact !== void 0) {
|
|
77
|
+
const updated = {
|
|
78
|
+
...settings,
|
|
79
|
+
statusLine: { type: "command", command: wrapperCommand(opts) }
|
|
80
|
+
};
|
|
81
|
+
writeSettings(updated);
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (opts.show) parts.push(`--show=${opts.show}`);
|
|
84
|
+
if (opts.compact) parts.push("--compact");
|
|
85
|
+
const desc = parts.length > 0 ? parts.join(" ") : "default (all chips, verbose)";
|
|
86
|
+
return { ok: true, outcome: "refreshed", message: `refreshed chip scripts and updated mode: ${desc}` };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, outcome: "refreshed", message: "statusline already wrapped \u2014 refreshed chip scripts only" };
|
|
89
|
+
}
|
|
90
|
+
if (!currentCmd) {
|
|
91
|
+
return { ok: false, outcome: "no-statusline", message: "no statusLine.command in ~/.claude/settings.json \u2014 launch-kit only extends an existing statusline" };
|
|
92
|
+
}
|
|
93
|
+
writeScripts();
|
|
94
|
+
const wrapped = {
|
|
95
|
+
...settings,
|
|
96
|
+
statusLine: { type: "command", command: wrapperCommand(opts) },
|
|
97
|
+
[ORIGINAL_KEY]: settings.statusLine
|
|
98
|
+
};
|
|
99
|
+
writeSettings(wrapped);
|
|
100
|
+
const modeParts = [];
|
|
101
|
+
if (opts.show) modeParts.push(`chips: ${opts.show}`);
|
|
102
|
+
if (opts.compact) modeParts.push("compact mode");
|
|
103
|
+
const modeDesc = modeParts.length > 0 ? ` (${modeParts.join(", ")})` : "";
|
|
104
|
+
return { ok: true, outcome: "activated", message: `wrapped statusLine.command${modeDesc}; original stashed under ${ORIGINAL_KEY}` };
|
|
105
|
+
}
|
|
106
|
+
function deactivateStatusline() {
|
|
107
|
+
const settings = readSettings();
|
|
108
|
+
if (!settings) return { ok: false, outcome: "no-settings", message: "no ~/.claude/settings.json" };
|
|
109
|
+
const original = settings[ORIGINAL_KEY];
|
|
110
|
+
if (!original) {
|
|
111
|
+
return { ok: false, outcome: "not-active", message: `no ${ORIGINAL_KEY} in settings.json \u2014 statusline isn't wrapped by launch-kit` };
|
|
112
|
+
}
|
|
113
|
+
const restored = { ...settings, statusLine: original };
|
|
114
|
+
delete restored[ORIGINAL_KEY];
|
|
115
|
+
writeSettings(restored);
|
|
116
|
+
for (const p of [WRAPPER_PATH, CHIP_PATH]) {
|
|
117
|
+
try {
|
|
118
|
+
fs3.unlinkSync(p);
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, outcome: "deactivated", message: "restored original statusLine.command" };
|
|
123
|
+
}
|
|
124
|
+
var fs3, path3, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
|
|
125
|
+
var init_statusline_install = __esm({
|
|
126
|
+
"src/server/statusline-install.ts"() {
|
|
127
|
+
"use strict";
|
|
128
|
+
fs3 = __toESM(require("node:fs"));
|
|
129
|
+
path3 = __toESM(require("node:path"));
|
|
130
|
+
import_node_os = require("node:os");
|
|
131
|
+
LK_DIR = path3.join((0, import_node_os.homedir)(), ".launchsecure");
|
|
132
|
+
WRAPPER_PATH = path3.join(LK_DIR, "statusline-wrapper.sh");
|
|
133
|
+
CHIP_PATH = path3.join(LK_DIR, "statusline-mcp.sh");
|
|
134
|
+
SETTINGS_PATH = path3.join((0, import_node_os.homedir)(), ".claude", "settings.json");
|
|
135
|
+
ORIGINAL_KEY = "_launchKitStatuslineOriginal";
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
26
139
|
// src/server/init-entry.ts
|
|
27
140
|
var import_node_child_process = require("node:child_process");
|
|
28
|
-
var
|
|
141
|
+
var crypto = __toESM(require("node:crypto"));
|
|
142
|
+
var fs4 = __toESM(require("node:fs"));
|
|
29
143
|
var import_node_http = require("node:http");
|
|
30
144
|
var import_node_https = require("node:https");
|
|
31
|
-
var
|
|
145
|
+
var path4 = __toESM(require("node:path"));
|
|
32
146
|
var readline = __toESM(require("node:readline"));
|
|
33
147
|
var import_node_url = require("node:url");
|
|
34
148
|
|
|
@@ -95,12 +209,94 @@ function writeJsonAtomic(absPath, value, mode) {
|
|
|
95
209
|
fs.renameSync(tmp, absPath);
|
|
96
210
|
}
|
|
97
211
|
|
|
212
|
+
// src/server/cred-recovery.ts
|
|
213
|
+
var fs2 = __toESM(require("node:fs"));
|
|
214
|
+
var path2 = __toESM(require("node:path"));
|
|
215
|
+
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
216
|
+
function migrateLegacyCredFile(targetDir, opts) {
|
|
217
|
+
const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
218
|
+
const dest = path2.join(targetDir, CONFIG_FILENAME);
|
|
219
|
+
if (!fs2.existsSync(legacy) || fs2.existsSync(dest)) return;
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const pat = parsed?.pat;
|
|
227
|
+
if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
|
|
228
|
+
if (opts.dryRun) {
|
|
229
|
+
opts.log.dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
fs2.renameSync(legacy, dest);
|
|
233
|
+
removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME, opts);
|
|
234
|
+
opts.log.ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
|
|
235
|
+
}
|
|
236
|
+
function removeGitignoreLine(targetDir, line, opts) {
|
|
237
|
+
const p = path2.join(targetDir, ".gitignore");
|
|
238
|
+
if (!fs2.existsSync(p)) return;
|
|
239
|
+
const before = fs2.readFileSync(p, "utf-8");
|
|
240
|
+
const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
|
|
241
|
+
if (after === before) return;
|
|
242
|
+
if (opts.dryRun) {
|
|
243
|
+
opts.log.dryNote(`would remove "${line}" from .gitignore`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
fs2.writeFileSync(p, after, "utf-8");
|
|
247
|
+
opts.log.ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
|
|
248
|
+
}
|
|
249
|
+
function recoverCredFromMcp(targetDir) {
|
|
250
|
+
const p = path2.join(targetDir, ".mcp.json");
|
|
251
|
+
if (!fs2.existsSync(p)) return null;
|
|
252
|
+
let mcp;
|
|
253
|
+
try {
|
|
254
|
+
mcp = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const entry = mcp.mcpServers?.["launch-secure"];
|
|
259
|
+
if (!entry?.headers || !entry.url) return null;
|
|
260
|
+
const auth = entry.headers["Authorization"];
|
|
261
|
+
const pat = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice("Bearer ".length).trim() : null;
|
|
262
|
+
const orgSlug = entry.headers["X-Org-Slug"];
|
|
263
|
+
const projectSlug = entry.headers["X-Project-Slug"];
|
|
264
|
+
if (!pat || !pat.startsWith("ls_pat_") || !orgSlug || !projectSlug) return null;
|
|
265
|
+
const serverUrl = entry.url.replace(/\/api\/mcp\/project\/?$/, "").replace(/\/+$/, "");
|
|
266
|
+
return { pat, orgSlug, projectSlug, serverUrl };
|
|
267
|
+
}
|
|
268
|
+
function recoverCred(targetDir, opts) {
|
|
269
|
+
migrateLegacyCredFile(targetDir, opts);
|
|
270
|
+
let cred = null;
|
|
271
|
+
try {
|
|
272
|
+
cred = readCredFile(targetDir);
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
if (cred) return { cred, source: "cred-file" };
|
|
276
|
+
if (opts.dryRun) {
|
|
277
|
+
const legacyPath = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
278
|
+
if (fs2.existsSync(legacyPath)) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(fs2.readFileSync(legacyPath, "utf-8"));
|
|
281
|
+
if (typeof parsed?.pat === "string" && parsed.pat.startsWith("ls_pat_")) {
|
|
282
|
+
opts.log.info(`(dry-run) using legacy ${LEGACY_CONFIG_FILENAME} for preview \u2014 a real run would migrate it to ${CONFIG_FILENAME} first`);
|
|
283
|
+
return { cred: parsed, source: "legacy-dry-run" };
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const recovered = recoverCredFromMcp(targetDir);
|
|
290
|
+
if (recovered) return { cred: recovered, source: "mcp" };
|
|
291
|
+
return { cred: null, source: null };
|
|
292
|
+
}
|
|
293
|
+
|
|
98
294
|
// src/server/init-entry.ts
|
|
295
|
+
init_statusline_install();
|
|
99
296
|
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
100
|
-
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
101
297
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
102
298
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
103
|
-
var
|
|
299
|
+
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
104
300
|
Wired in Claude Code (.mcp.json):
|
|
105
301
|
launch-secure \u2014 LS API: work items, comms, secrets, members, board
|
|
106
302
|
launch-chart \u2014 code search + project graph (use instead of grep/glob)
|
|
@@ -115,41 +311,56 @@ Other tools (run on demand via npx):
|
|
|
115
311
|
in-browser monitor. Paste the printed URL into
|
|
116
312
|
the beacon debug panel; events stream to
|
|
117
313
|
.launchsecure/beacon-<token>.ndjson for the
|
|
118
|
-
/
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
314
|
+
/kit:beacon-* commands below to read.
|
|
315
|
+
`;
|
|
316
|
+
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
|
|
144
317
|
Open this repo in Claude Code; on first open you'll be prompted to install
|
|
145
|
-
the "
|
|
318
|
+
the "launch-secure" marketplace \u2014 accept to enable the commands above.
|
|
319
|
+
`;
|
|
320
|
+
function renderLsCommandsSection() {
|
|
321
|
+
const dir = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit", "commands");
|
|
322
|
+
if (!fs4.existsSync(dir)) {
|
|
323
|
+
return "\nLS slash commands: (scaffold dir not bundled \u2014 this is a packaging bug)\n";
|
|
324
|
+
}
|
|
325
|
+
const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
|
|
326
|
+
if (files.length === 0) return "\nLS slash commands: (none defined)\n";
|
|
327
|
+
const lines = files.map((file) => {
|
|
328
|
+
const name = file.replace(/\.md$/, "");
|
|
329
|
+
const text = fs4.readFileSync(path4.join(dir, file), "utf-8");
|
|
330
|
+
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
331
|
+
const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
332
|
+
const padded = `/${PLUGIN_ID}:${name}`.padEnd(26);
|
|
333
|
+
return ` ${padded} \u2014 ${desc}`;
|
|
334
|
+
});
|
|
335
|
+
return `
|
|
336
|
+
LS slash commands (run inside Claude Code in this project):
|
|
337
|
+
${lines.join("\n")}
|
|
146
338
|
`;
|
|
339
|
+
}
|
|
340
|
+
function getLaunchKitToolsGuide() {
|
|
341
|
+
return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
|
|
342
|
+
}
|
|
147
343
|
var PACKAGE_MANAGERS = [
|
|
148
344
|
{ name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
|
|
149
345
|
{ name: "yarn", binary: "yarn", lockfiles: ["yarn.lock"], installArgs: ["install"] },
|
|
150
346
|
{ name: "bun", binary: "bun", lockfiles: ["bun.lockb", "bun.lock"], installArgs: ["install"] },
|
|
151
347
|
{ name: "npm", binary: "npm", lockfiles: ["package-lock.json"], installArgs: ["install"] }
|
|
152
348
|
];
|
|
349
|
+
var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
350
|
+
"--help",
|
|
351
|
+
"-h",
|
|
352
|
+
"--no-install",
|
|
353
|
+
"--no-onboard",
|
|
354
|
+
"--no-recall",
|
|
355
|
+
"--no-migrate-safety",
|
|
356
|
+
"--no-ls-marketplace",
|
|
357
|
+
"--no-recall-hook",
|
|
358
|
+
"--refresh-scaffolds",
|
|
359
|
+
"--quiet",
|
|
360
|
+
"--force",
|
|
361
|
+
"--dry-run"
|
|
362
|
+
]);
|
|
363
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
|
|
153
364
|
function parseArgs(argv) {
|
|
154
365
|
const args = {
|
|
155
366
|
token: process.env.LS_PAT ?? null,
|
|
@@ -159,14 +370,21 @@ function parseArgs(argv) {
|
|
|
159
370
|
targetDir: null,
|
|
160
371
|
course: null,
|
|
161
372
|
noInstall: false,
|
|
373
|
+
noOnboard: false,
|
|
162
374
|
noRecall: false,
|
|
163
375
|
noMigrateSafety: false,
|
|
164
376
|
noLsMarketplace: false,
|
|
165
377
|
noRecallHook: false,
|
|
378
|
+
refreshScaffolds: false,
|
|
379
|
+
quiet: false,
|
|
380
|
+
force: false,
|
|
166
381
|
dryRun: false,
|
|
167
382
|
help: false
|
|
168
383
|
};
|
|
169
|
-
|
|
384
|
+
const unknown = [];
|
|
385
|
+
for (let i = 0; i < argv.length; i++) {
|
|
386
|
+
const raw = argv[i];
|
|
387
|
+
if (i === 0 && !raw.startsWith("--") && !raw.startsWith("-")) continue;
|
|
170
388
|
if (raw === "--help" || raw === "-h") {
|
|
171
389
|
args.help = true;
|
|
172
390
|
continue;
|
|
@@ -175,6 +393,10 @@ function parseArgs(argv) {
|
|
|
175
393
|
args.noInstall = true;
|
|
176
394
|
continue;
|
|
177
395
|
}
|
|
396
|
+
if (raw === "--no-onboard") {
|
|
397
|
+
args.noOnboard = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
178
400
|
if (raw === "--no-recall") {
|
|
179
401
|
args.noRecall = true;
|
|
180
402
|
continue;
|
|
@@ -191,28 +413,116 @@ function parseArgs(argv) {
|
|
|
191
413
|
args.noRecallHook = true;
|
|
192
414
|
continue;
|
|
193
415
|
}
|
|
416
|
+
if (raw === "--refresh-scaffolds") {
|
|
417
|
+
args.refreshScaffolds = true;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (raw === "--quiet") {
|
|
421
|
+
args.quiet = true;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (raw === "--force") {
|
|
425
|
+
args.force = true;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
194
428
|
if (raw === "--dry-run") {
|
|
195
429
|
args.dryRun = true;
|
|
196
430
|
continue;
|
|
197
431
|
}
|
|
198
432
|
const eq = raw.indexOf("=");
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
433
|
+
if (raw.startsWith("--") && eq > 0) {
|
|
434
|
+
const key = raw.slice(2, eq);
|
|
435
|
+
const val = raw.slice(eq + 1);
|
|
436
|
+
if (key === "token") {
|
|
437
|
+
args.token = val;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (key === "org") {
|
|
441
|
+
args.orgSlug = val;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (key === "project") {
|
|
445
|
+
args.projectSlug = val;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (key === "url") {
|
|
449
|
+
args.serverUrl = val.replace(/\/+$/, "");
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (key === "dir") {
|
|
453
|
+
args.targetDir = val;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (key === "course") {
|
|
457
|
+
args.course = val;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
unknown.push(raw);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (raw.startsWith("-")) {
|
|
464
|
+
unknown.push(raw);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
unknown.push(raw);
|
|
468
|
+
}
|
|
469
|
+
if (unknown.length > 0) {
|
|
470
|
+
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
471
|
+
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
472
|
+
fail(`Unknown argument(s): ${unknown.join(" ")}
|
|
473
|
+
Known boolean flags: ${knownBool}
|
|
474
|
+
Known key=value flags: ${knownKv}`);
|
|
208
475
|
}
|
|
209
476
|
return args;
|
|
210
477
|
}
|
|
478
|
+
function printRefreshHelp() {
|
|
479
|
+
console.log(`launch-kit refresh \u2014 re-apply launch-kit scaffolds in an already-initialized project
|
|
480
|
+
|
|
481
|
+
Usage:
|
|
482
|
+
npx @launchsecure/launch-kit@latest refresh [--dir=<path>] [options]
|
|
483
|
+
|
|
484
|
+
What it does:
|
|
485
|
+
Re-merges .mcp.json with the latest launch-kit entries (preserves your other
|
|
486
|
+
entries), refreshes the launch-secure marketplace tree at .claude/marketplace/
|
|
487
|
+
(picks up any new /kit:* commands), refreshes scripts/ensure-recall.sh and
|
|
488
|
+
the SessionStart hook, and refreshes the migrate-safety scaffold.
|
|
489
|
+
|
|
490
|
+
Reads org/project/serverUrl/pat from the existing .launch-secure.cred.config.
|
|
491
|
+
Does NOT clone, re-install deps, re-prompt for PAT, or re-run the onboard
|
|
492
|
+
script. Use \`init\` for those.
|
|
493
|
+
|
|
494
|
+
Options:
|
|
495
|
+
--dir=<path> Target directory (default: cwd). Must contain a
|
|
496
|
+
valid .launch-secure.cred.config.
|
|
497
|
+
--no-migrate-safety Skip refreshing the migrate-safety scaffold.
|
|
498
|
+
--no-ls-marketplace Skip refreshing the launch-secure marketplace.
|
|
499
|
+
--no-recall-hook Skip refreshing the recall-hook scaffold.
|
|
500
|
+
--refresh-scaffolds Force-overwrite migrate-safety files (default is to
|
|
501
|
+
preserve user edits). Use this to pull updates
|
|
502
|
+
published to @launchsecure/launch-kit.
|
|
503
|
+
--quiet Suppress the post-run tools guide.
|
|
504
|
+
--dry-run Preview every file write without making changes.
|
|
505
|
+
--help Show this help.
|
|
506
|
+
|
|
507
|
+
Tip: prefix the command with @latest (\`launch-kit@latest refresh\`) to force
|
|
508
|
+
npx to pull the newest published version instead of using a cached older one.
|
|
509
|
+
`);
|
|
510
|
+
}
|
|
211
511
|
function printHelp() {
|
|
212
|
-
console.log(`launch-kit
|
|
512
|
+
console.log(`launch-kit \u2014 bootstrap and refresh a LaunchSecure project on this machine
|
|
513
|
+
|
|
514
|
+
Subcommands:
|
|
515
|
+
init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
|
|
516
|
+
refresh Re-apply scaffolds + MCP entries in an already-initialized project
|
|
517
|
+
(no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
|
|
518
|
+
statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
|
|
519
|
+
chips (recall, chart, deck, council) get appended. Refuses to
|
|
520
|
+
create one if none exists \u2014 additive only.
|
|
521
|
+
statusline deactivate Restore the original statusLine.command and remove kit scripts.
|
|
213
522
|
|
|
214
523
|
Usage:
|
|
215
|
-
npx launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
|
|
524
|
+
npx @launchsecure/launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
|
|
525
|
+
npx @launchsecure/launch-kit@latest refresh [--dir=<path>] [options]
|
|
216
526
|
|
|
217
527
|
Required:
|
|
218
528
|
--token=<pat> LaunchSecure PAT (ls_pat_...). Or set LS_PAT env var.
|
|
@@ -229,19 +539,34 @@ Options:
|
|
|
229
539
|
becomes active; re-run with a different --course
|
|
230
540
|
and --url to add another (e.g. local + staging).
|
|
231
541
|
Use \`launch-course set <name>\` to switch later.
|
|
232
|
-
--no-install Skip dependency install step
|
|
542
|
+
--no-install Skip dependency install step (also skips the onboard
|
|
543
|
+
script \u2014 install is its prerequisite).
|
|
544
|
+
--no-onboard Skip the onboard script even when install runs.
|
|
233
545
|
--no-recall Skip launch-recall (shadow git backup) scaffold.
|
|
234
546
|
--no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
|
|
235
547
|
wrapper + GitHub Action + runbook).
|
|
236
|
-
--no-ls-marketplace Skip the Claude Code "
|
|
548
|
+
--no-ls-marketplace Skip the Claude Code "launch-secure" marketplace
|
|
237
549
|
scaffold (.claude/marketplace/ + .claude/settings.json
|
|
238
|
-
wiring \u2014 exposes /
|
|
550
|
+
wiring \u2014 exposes /kit:activate-beacon and future
|
|
239
551
|
ls-namespaced slash commands).
|
|
240
552
|
--no-recall-hook Skip the SessionStart hook scaffold (Claude Code
|
|
241
553
|
hook + scripts/ensure-recall.sh that auto-restarts
|
|
242
554
|
the launch-recall watcher if it died between
|
|
243
555
|
sessions). The hook is the surfacing layer for
|
|
244
556
|
watcher-died-silently scenarios.
|
|
557
|
+
--refresh-scaffolds Force-overwrite migrate-safety scaffold files even
|
|
558
|
+
when they already exist. Default behavior preserves
|
|
559
|
+
user edits; use this to pull updates published to
|
|
560
|
+
@launchsecure/launch-kit (e.g., a newer
|
|
561
|
+
migrate-with-backup.sh).
|
|
562
|
+
--quiet Suppress the post-run tools guide (the long block
|
|
563
|
+
listing /kit:* commands and wired MCPs). Useful for
|
|
564
|
+
idempotent re-runs in CI or scripts.
|
|
565
|
+
--force Skip the auto-delegate-to-refresh check. By default
|
|
566
|
+
init detects an existing bootstrap (cred file +
|
|
567
|
+
launch-secure MCP entry) and runs refresh instead.
|
|
568
|
+
Pass --force to re-init from scratch even when the
|
|
569
|
+
target dir is already set up.
|
|
245
570
|
--dry-run Preview every file write, merge, clone, and install
|
|
246
571
|
command without making any changes. Useful before
|
|
247
572
|
re-running init against a customized project. The
|
|
@@ -265,10 +590,10 @@ What it does:
|
|
|
265
590
|
8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
|
|
266
591
|
9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
|
|
267
592
|
runbook + .backups/ gitignore line). Skip with --no-migrate-safety.
|
|
268
|
-
10. Scaffolds the Claude Code "
|
|
593
|
+
10. Scaffolds the Claude Code "launch-secure" marketplace at
|
|
269
594
|
.claude/marketplace/ and wires .claude/settings.json so Claude Code
|
|
270
|
-
auto-discovers it and enables the "
|
|
271
|
-
/
|
|
595
|
+
auto-discovers it and enables the "kit" plugin (exposes
|
|
596
|
+
/kit:activate-beacon for wiring the launch-kit-beacon in-app feedback
|
|
272
597
|
widget). Skip with --no-ls-marketplace.
|
|
273
598
|
11. Scaffolds scripts/ensure-recall.sh and appends a SessionStart hook to
|
|
274
599
|
.claude/settings.json that respawns the launch-recall watcher if it
|
|
@@ -278,9 +603,9 @@ What it does:
|
|
|
278
603
|
}
|
|
279
604
|
async function prompt(question) {
|
|
280
605
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
281
|
-
return new Promise((
|
|
606
|
+
return new Promise((resolve3) => rl.question(question, (answer) => {
|
|
282
607
|
rl.close();
|
|
283
|
-
|
|
608
|
+
resolve3(answer.trim());
|
|
284
609
|
}));
|
|
285
610
|
}
|
|
286
611
|
function fail(msg) {
|
|
@@ -310,8 +635,17 @@ function preflight() {
|
|
|
310
635
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
311
636
|
return { hasGh };
|
|
312
637
|
}
|
|
313
|
-
|
|
314
|
-
|
|
638
|
+
var PROJECT_INFO_TIMEOUT_MS = 3e4;
|
|
639
|
+
var PROJECT_INFO_MAX_ATTEMPTS = 3;
|
|
640
|
+
var ProjectInfoHttpError = class extends Error {
|
|
641
|
+
constructor(status, retryable, message) {
|
|
642
|
+
super(message);
|
|
643
|
+
this.status = status;
|
|
644
|
+
this.retryable = retryable;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
function attemptProjectInfo(args) {
|
|
648
|
+
return new Promise((resolve3, reject) => {
|
|
315
649
|
const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
|
|
316
650
|
const body = JSON.stringify({
|
|
317
651
|
jsonrpc: "2.0",
|
|
@@ -343,12 +677,24 @@ function callProjectInfo(args) {
|
|
|
343
677
|
res.on("data", (c) => chunks.push(c));
|
|
344
678
|
res.on("end", () => {
|
|
345
679
|
const text = Buffer.concat(chunks).toString("utf-8");
|
|
346
|
-
if (res.statusCode === 401
|
|
347
|
-
reject(new
|
|
680
|
+
if (res.statusCode === 401) {
|
|
681
|
+
reject(new ProjectInfoHttpError(401, false, `PAT rejected (401). Token is invalid or expired. Generate a new PAT at ${args.serverUrl}/settings/tokens and retry.`));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (res.statusCode === 403) {
|
|
685
|
+
reject(new ProjectInfoHttpError(403, false, `Access denied (403). Token is valid but lacks access to ${args.orgSlug}/${args.projectSlug}. Check membership/permissions on the project.`));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (res.statusCode === 404) {
|
|
689
|
+
reject(new ProjectInfoHttpError(404, false, `Project not found (404). Verify ${args.orgSlug}/${args.projectSlug} exists at ${args.serverUrl}.`));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (res.statusCode && res.statusCode >= 500) {
|
|
693
|
+
reject(new ProjectInfoHttpError(res.statusCode, true, `LaunchSecure server error ${res.statusCode}: ${text.slice(0, 200)}`));
|
|
348
694
|
return;
|
|
349
695
|
}
|
|
350
696
|
if (res.statusCode && res.statusCode >= 400) {
|
|
351
|
-
reject(new
|
|
697
|
+
reject(new ProjectInfoHttpError(res.statusCode, false, `LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
352
698
|
return;
|
|
353
699
|
}
|
|
354
700
|
let json = text;
|
|
@@ -377,7 +723,7 @@ function callProjectInfo(args) {
|
|
|
377
723
|
return;
|
|
378
724
|
}
|
|
379
725
|
const payload = JSON.parse(inner);
|
|
380
|
-
|
|
726
|
+
resolve3({
|
|
381
727
|
orgSlug: payload.org.slug,
|
|
382
728
|
projectSlug: payload.project.slug,
|
|
383
729
|
projectName: payload.project.name,
|
|
@@ -389,11 +735,34 @@ function callProjectInfo(args) {
|
|
|
389
735
|
});
|
|
390
736
|
}
|
|
391
737
|
);
|
|
392
|
-
req.
|
|
738
|
+
req.setTimeout(PROJECT_INFO_TIMEOUT_MS, () => {
|
|
739
|
+
req.destroy(new Error(`project_info timed out after ${PROJECT_INFO_TIMEOUT_MS / 1e3}s`));
|
|
740
|
+
});
|
|
741
|
+
req.on("error", (err) => {
|
|
742
|
+
const code = err.code;
|
|
743
|
+
const retryable = code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EAI_AGAIN" || /timed out/.test(err.message);
|
|
744
|
+
reject(retryable ? new ProjectInfoHttpError(0, true, err.message) : err);
|
|
745
|
+
});
|
|
393
746
|
req.write(body);
|
|
394
747
|
req.end();
|
|
395
748
|
});
|
|
396
749
|
}
|
|
750
|
+
async function callProjectInfo(args) {
|
|
751
|
+
let lastErr;
|
|
752
|
+
for (let attempt = 1; attempt <= PROJECT_INFO_MAX_ATTEMPTS; attempt++) {
|
|
753
|
+
try {
|
|
754
|
+
return await attemptProjectInfo(args);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
lastErr = err;
|
|
757
|
+
const retryable = err instanceof ProjectInfoHttpError && err.retryable;
|
|
758
|
+
if (!retryable || attempt === PROJECT_INFO_MAX_ATTEMPTS) break;
|
|
759
|
+
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
760
|
+
info(`project_info attempt ${attempt}/${PROJECT_INFO_MAX_ATTEMPTS} failed (${err instanceof Error ? err.message : String(err)}) \u2014 retrying in ${delayMs}ms`);
|
|
761
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
throw lastErr;
|
|
765
|
+
}
|
|
397
766
|
function gitRemoteUrl(dir) {
|
|
398
767
|
const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
399
768
|
if (res.status !== 0) return null;
|
|
@@ -411,11 +780,11 @@ function normalizeRepoUrl(url) {
|
|
|
411
780
|
}
|
|
412
781
|
}
|
|
413
782
|
function isGitRepo(dir) {
|
|
414
|
-
return
|
|
783
|
+
return fs4.existsSync(path4.join(dir, ".git"));
|
|
415
784
|
}
|
|
416
785
|
function dirIsEmpty(dir) {
|
|
417
|
-
if (!
|
|
418
|
-
return
|
|
786
|
+
if (!fs4.existsSync(dir)) return true;
|
|
787
|
+
return fs4.readdirSync(dir).length === 0;
|
|
419
788
|
}
|
|
420
789
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
421
790
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -440,11 +809,14 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
440
809
|
`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
810
|
);
|
|
442
811
|
}
|
|
812
|
+
if (!fs4.existsSync(path4.join(targetDir, ".git"))) {
|
|
813
|
+
fail(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
814
|
+
}
|
|
443
815
|
ok(`cloned to ${targetDir}`);
|
|
444
816
|
}
|
|
445
817
|
function writeConfigFile(targetDir, cfg, courseName) {
|
|
446
|
-
|
|
447
|
-
const p =
|
|
818
|
+
recoverCred(targetDir, getRecoveryOptions());
|
|
819
|
+
const p = path4.join(targetDir, CONFIG_FILENAME);
|
|
448
820
|
const existing = readCredFile(targetDir);
|
|
449
821
|
const isNew = existing === null;
|
|
450
822
|
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
@@ -458,38 +830,26 @@ function writeConfigFile(targetDir, cfg, courseName) {
|
|
|
458
830
|
const action = isNew ? "wrote" : isUpdate ? `updated course "${courseName}" in` : `added course "${courseName}" to`;
|
|
459
831
|
ok(`${action} ${CONFIG_FILENAME} (active: ${courseName})`);
|
|
460
832
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
} catch {
|
|
469
|
-
return;
|
|
833
|
+
var recoveryLog = { info, ok, dryNote };
|
|
834
|
+
function getRecoveryOptions() {
|
|
835
|
+
return { dryRun: DRY_RUN, log: recoveryLog };
|
|
836
|
+
}
|
|
837
|
+
function detectExistingBootstrap(targetDir) {
|
|
838
|
+
if (!fs4.existsSync(path4.join(targetDir, CONFIG_FILENAME))) {
|
|
839
|
+
return { bootstrapped: false };
|
|
470
840
|
}
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
|
|
475
|
-
return;
|
|
841
|
+
const mcpPath = path4.join(targetDir, ".mcp.json");
|
|
842
|
+
if (!fs4.existsSync(mcpPath)) {
|
|
843
|
+
return { bootstrapped: false };
|
|
476
844
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
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;
|
|
845
|
+
try {
|
|
846
|
+
const mcp = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
|
|
847
|
+
if (mcp.mcpServers?.["launch-secure"]) {
|
|
848
|
+
return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
490
851
|
}
|
|
491
|
-
|
|
492
|
-
ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
|
|
852
|
+
return { bootstrapped: false };
|
|
493
853
|
}
|
|
494
854
|
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
855
|
function buildLaunchKitMcpEntries(cfg) {
|
|
@@ -502,6 +862,11 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
502
862
|
"launch-chart": {
|
|
503
863
|
command: "npx",
|
|
504
864
|
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
|
|
865
|
+
// Tells launch-chart to also start its HTTP UI alongside the MCP, so
|
|
866
|
+
// users can open the chart viewer at localhost:<port> while queries
|
|
867
|
+
// hit the MCP. Without this, the MCP runs passively (queries work,
|
|
868
|
+
// no UI). I4 deep-merge preserves user-added env keys; this default
|
|
869
|
+
// ensures the auto-serve UX ships out of the box.
|
|
505
870
|
env: { LAUNCH_CHART_AUTOSERVE: "1" }
|
|
506
871
|
},
|
|
507
872
|
"launch-deck": {
|
|
@@ -518,13 +883,29 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
518
883
|
}
|
|
519
884
|
};
|
|
520
885
|
}
|
|
886
|
+
function mergeMcpEntry(existing, ours) {
|
|
887
|
+
const merged = { ...existing, ...ours };
|
|
888
|
+
if (existing.headers || ours.headers) {
|
|
889
|
+
const authKeys = /* @__PURE__ */ new Set(["Authorization", "X-Org-Slug", "X-Project-Slug"]);
|
|
890
|
+
const baseHeaders = ours.headersHelper ? Object.fromEntries(Object.entries(existing.headers ?? {}).filter(([k]) => !authKeys.has(k))) : { ...existing.headers ?? {} };
|
|
891
|
+
const combinedHeaders = { ...baseHeaders, ...ours.headers ?? {} };
|
|
892
|
+
if (Object.keys(combinedHeaders).length > 0) merged.headers = combinedHeaders;
|
|
893
|
+
else delete merged.headers;
|
|
894
|
+
}
|
|
895
|
+
if (existing.env || ours.env) {
|
|
896
|
+
const combinedEnv = { ...existing.env ?? {}, ...ours.env ?? {} };
|
|
897
|
+
if (Object.keys(combinedEnv).length > 0) merged.env = combinedEnv;
|
|
898
|
+
else delete merged.env;
|
|
899
|
+
}
|
|
900
|
+
return merged;
|
|
901
|
+
}
|
|
521
902
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
522
|
-
const p =
|
|
523
|
-
const hadExisting =
|
|
903
|
+
const p = path4.join(targetDir, ".mcp.json");
|
|
904
|
+
const hadExisting = fs4.existsSync(p);
|
|
524
905
|
let existing = {};
|
|
525
906
|
if (hadExisting) {
|
|
526
907
|
try {
|
|
527
|
-
existing = JSON.parse(
|
|
908
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
528
909
|
} catch (err) {
|
|
529
910
|
fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
530
911
|
}
|
|
@@ -534,24 +915,29 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
534
915
|
const overwrites = [];
|
|
535
916
|
const additions = [];
|
|
536
917
|
for (const [name, entry] of Object.entries(launchKitEntries)) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
918
|
+
const existingEntry = merged.mcpServers[name];
|
|
919
|
+
if (existingEntry) {
|
|
920
|
+
overwrites.push(name);
|
|
921
|
+
merged.mcpServers[name] = mergeMcpEntry(existingEntry, entry);
|
|
922
|
+
} else {
|
|
923
|
+
additions.push(name);
|
|
924
|
+
merged.mcpServers[name] = entry;
|
|
925
|
+
}
|
|
540
926
|
}
|
|
541
927
|
if (DRY_RUN) {
|
|
542
928
|
const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
|
|
543
929
|
dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
544
930
|
return;
|
|
545
931
|
}
|
|
546
|
-
|
|
932
|
+
fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
547
933
|
const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
|
|
548
934
|
ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
549
935
|
}
|
|
550
936
|
function detectPackageManager(repoDir) {
|
|
551
|
-
const pkgPath =
|
|
552
|
-
if (!
|
|
937
|
+
const pkgPath = path4.join(repoDir, "package.json");
|
|
938
|
+
if (!fs4.existsSync(pkgPath)) return null;
|
|
553
939
|
try {
|
|
554
|
-
const pkg = JSON.parse(
|
|
940
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
555
941
|
if (typeof pkg.packageManager === "string") {
|
|
556
942
|
const name = pkg.packageManager.split("@")[0];
|
|
557
943
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -560,7 +946,7 @@ function detectPackageManager(repoDir) {
|
|
|
560
946
|
}
|
|
561
947
|
} catch {
|
|
562
948
|
}
|
|
563
|
-
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) =>
|
|
949
|
+
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs4.existsSync(path4.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
|
|
564
950
|
if (matches.length === 1) {
|
|
565
951
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
566
952
|
}
|
|
@@ -569,8 +955,8 @@ function detectPackageManager(repoDir) {
|
|
|
569
955
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
570
956
|
}
|
|
571
957
|
for (const pm of PACKAGE_MANAGERS) {
|
|
572
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
573
|
-
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) =>
|
|
958
|
+
if (pm.workspaceFiles?.some((wf) => fs4.existsSync(path4.join(repoDir, wf)))) {
|
|
959
|
+
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs4.existsSync(path4.join(repoDir, wf)))})` };
|
|
574
960
|
}
|
|
575
961
|
}
|
|
576
962
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -580,7 +966,7 @@ function runInstall(repoDir, detected) {
|
|
|
580
966
|
const { pm } = detected;
|
|
581
967
|
if (!which(pm.binary)) {
|
|
582
968
|
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 ${
|
|
969
|
+
`${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
584
970
|
);
|
|
585
971
|
}
|
|
586
972
|
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
@@ -591,16 +977,32 @@ function runInstall(repoDir, detected) {
|
|
|
591
977
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
592
978
|
if (res.status !== 0) {
|
|
593
979
|
fail(
|
|
594
|
-
`${pm.name} install failed (exit ${res.status}).
|
|
980
|
+
`${pm.name} install failed (exit ${res.status}).
|
|
981
|
+
|
|
982
|
+
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
983
|
+
- ${path4.join(repoDir, CONFIG_FILENAME)} (cred file)
|
|
984
|
+
- ${path4.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
|
|
985
|
+
- ${path4.join(repoDir, ".gitignore")} (cred line appended)
|
|
986
|
+
- clone at ${repoDir}
|
|
987
|
+
|
|
988
|
+
Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
|
|
989
|
+
|
|
990
|
+
To retry install only:
|
|
991
|
+
cd ${path4.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
|
|
992
|
+
|
|
993
|
+
To re-run init after fixing the install error:
|
|
994
|
+
cd ${path4.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
|
|
995
|
+
|
|
996
|
+
To fully reset: delete the files listed above and the clone, then re-init.`
|
|
595
997
|
);
|
|
596
998
|
}
|
|
597
999
|
ok(`${pm.name} install complete`);
|
|
598
1000
|
}
|
|
599
1001
|
function hasOnboardScript(repoDir) {
|
|
600
|
-
const pkgPath =
|
|
601
|
-
if (!
|
|
1002
|
+
const pkgPath = path4.join(repoDir, "package.json");
|
|
1003
|
+
if (!fs4.existsSync(pkgPath)) return false;
|
|
602
1004
|
try {
|
|
603
|
-
const pkg = JSON.parse(
|
|
1005
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
604
1006
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
605
1007
|
} catch {
|
|
606
1008
|
return false;
|
|
@@ -608,8 +1010,8 @@ function hasOnboardScript(repoDir) {
|
|
|
608
1010
|
}
|
|
609
1011
|
function runRecallInit(repoDir) {
|
|
610
1012
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
611
|
-
const recallEntry =
|
|
612
|
-
const useSibling =
|
|
1013
|
+
const recallEntry = path4.resolve(__dirname, "recall-entry.js");
|
|
1014
|
+
const useSibling = fs4.existsSync(recallEntry);
|
|
613
1015
|
const cmd = useSibling ? process.execPath : "npx";
|
|
614
1016
|
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
615
1017
|
if (DRY_RUN) {
|
|
@@ -618,7 +1020,7 @@ function runRecallInit(repoDir) {
|
|
|
618
1020
|
}
|
|
619
1021
|
const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
620
1022
|
if (res.status !== 0) {
|
|
621
|
-
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${
|
|
1023
|
+
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path4.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
|
|
622
1024
|
return;
|
|
623
1025
|
}
|
|
624
1026
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
@@ -632,14 +1034,14 @@ function runOnboard(repoDir, pm) {
|
|
|
632
1034
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
633
1035
|
if (res.status !== 0) {
|
|
634
1036
|
fail(
|
|
635
|
-
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${
|
|
1037
|
+
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path4.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
|
|
636
1038
|
);
|
|
637
1039
|
}
|
|
638
1040
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
639
1041
|
}
|
|
640
1042
|
function ensureGitignoreLine(targetDir, line) {
|
|
641
|
-
const p =
|
|
642
|
-
let content =
|
|
1043
|
+
const p = path4.join(targetDir, ".gitignore");
|
|
1044
|
+
let content = fs4.existsSync(p) ? fs4.readFileSync(p, "utf-8") : "";
|
|
643
1045
|
const lines = content.split(/\r?\n/);
|
|
644
1046
|
if (lines.some((l) => l.trim() === line)) return;
|
|
645
1047
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
@@ -649,131 +1051,191 @@ function ensureGitignoreLine(targetDir, line) {
|
|
|
649
1051
|
dryNote(`would append "${line}" to .gitignore`);
|
|
650
1052
|
return;
|
|
651
1053
|
}
|
|
652
|
-
|
|
1054
|
+
fs4.writeFileSync(p, content, "utf-8");
|
|
653
1055
|
ok(`appended ${line} to .gitignore`);
|
|
654
1056
|
}
|
|
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
1057
|
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
676
|
-
if (!
|
|
677
|
-
for (const entry of
|
|
678
|
-
const srcPath =
|
|
679
|
-
const destPath =
|
|
1058
|
+
if (!fs4.existsSync(srcDir)) return;
|
|
1059
|
+
for (const entry of fs4.readdirSync(srcDir, { withFileTypes: true })) {
|
|
1060
|
+
const srcPath = path4.join(srcDir, entry.name);
|
|
1061
|
+
const destPath = path4.join(destDir, entry.name);
|
|
680
1062
|
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
681
1063
|
if (entry.isDirectory()) {
|
|
682
1064
|
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
683
1065
|
} else if (entry.isFile()) {
|
|
684
|
-
const existed =
|
|
1066
|
+
const existed = fs4.existsSync(destPath);
|
|
685
1067
|
if (DRY_RUN) {
|
|
686
1068
|
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
687
1069
|
continue;
|
|
688
1070
|
}
|
|
689
|
-
|
|
690
|
-
|
|
1071
|
+
fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
|
|
1072
|
+
fs4.copyFileSync(srcPath, destPath);
|
|
691
1073
|
try {
|
|
692
|
-
const srcMode =
|
|
693
|
-
|
|
1074
|
+
const srcMode = fs4.statSync(srcPath).mode;
|
|
1075
|
+
fs4.chmodSync(destPath, srcMode);
|
|
694
1076
|
} catch {
|
|
695
1077
|
}
|
|
696
1078
|
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
697
1079
|
}
|
|
698
1080
|
}
|
|
699
1081
|
}
|
|
700
|
-
function scaffoldMigrateSafety(targetDir) {
|
|
701
|
-
const scaffoldsRoot =
|
|
702
|
-
if (!
|
|
1082
|
+
function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
|
|
1083
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
1084
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
703
1085
|
info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
704
1086
|
return;
|
|
705
1087
|
}
|
|
706
1088
|
const files = [
|
|
707
1089
|
{
|
|
708
|
-
src:
|
|
709
|
-
dest:
|
|
1090
|
+
src: path4.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
1091
|
+
dest: path4.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
710
1092
|
label: ".github/workflows/backup-on-migration.yml"
|
|
711
1093
|
},
|
|
712
1094
|
{
|
|
713
|
-
src:
|
|
714
|
-
dest:
|
|
1095
|
+
src: path4.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
1096
|
+
dest: path4.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
715
1097
|
label: "scripts/migrate-with-backup.sh"
|
|
716
1098
|
},
|
|
717
1099
|
{
|
|
718
|
-
src:
|
|
719
|
-
dest:
|
|
1100
|
+
src: path4.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
1101
|
+
dest: path4.join(targetDir, "docs", "migrations-runbook.md"),
|
|
720
1102
|
label: "docs/migrations-runbook.md"
|
|
721
1103
|
}
|
|
722
1104
|
];
|
|
723
|
-
info(
|
|
724
|
-
for (const f of files)
|
|
1105
|
+
info(`scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook)${refreshScaffolds ? " \u2014 --refresh-scaffolds active" : ""} \u2026`);
|
|
1106
|
+
for (const f of files) copyScaffoldDriftAware(f.src, f.dest, f.label, refreshScaffolds);
|
|
725
1107
|
ensureGitignoreLine(targetDir, ".backups/");
|
|
726
1108
|
ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
|
|
727
1109
|
}
|
|
728
|
-
|
|
729
|
-
|
|
1110
|
+
function hashFile(p) {
|
|
1111
|
+
try {
|
|
1112
|
+
return crypto.createHash("sha256").update(fs4.readFileSync(p)).digest("hex");
|
|
1113
|
+
} catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
1118
|
+
if (!fs4.existsSync(srcPath)) {
|
|
1119
|
+
info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
|
|
1120
|
+
return "missing-src";
|
|
1121
|
+
}
|
|
1122
|
+
if (!fs4.existsSync(destPath)) {
|
|
1123
|
+
if (DRY_RUN) {
|
|
1124
|
+
dryNote(`would write ${label}`);
|
|
1125
|
+
return "wrote";
|
|
1126
|
+
}
|
|
1127
|
+
fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
|
|
1128
|
+
fs4.copyFileSync(srcPath, destPath);
|
|
1129
|
+
try {
|
|
1130
|
+
fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
ok(`wrote ${label}`);
|
|
1134
|
+
return "wrote";
|
|
1135
|
+
}
|
|
1136
|
+
const srcHash = hashFile(srcPath);
|
|
1137
|
+
const destHash = hashFile(destPath);
|
|
1138
|
+
if (srcHash && destHash && srcHash === destHash) {
|
|
1139
|
+
if (DRY_RUN) dryNote(`${label} in sync with shipped scaffold \u2014 no change`);
|
|
1140
|
+
else info(`${label} in sync with shipped scaffold \u2014 no change`);
|
|
1141
|
+
return "in-sync";
|
|
1142
|
+
}
|
|
1143
|
+
if (refreshScaffolds) {
|
|
1144
|
+
if (DRY_RUN) {
|
|
1145
|
+
dryNote(`would refresh ${label} (overrides local edits)`);
|
|
1146
|
+
return "drifted-refreshed";
|
|
1147
|
+
}
|
|
1148
|
+
fs4.copyFileSync(srcPath, destPath);
|
|
1149
|
+
try {
|
|
1150
|
+
fs4.chmodSync(destPath, fs4.statSync(srcPath).mode);
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
|
|
1154
|
+
return "drifted-refreshed";
|
|
1155
|
+
}
|
|
1156
|
+
info(`${label} differs from shipped scaffold (customized or older version) \u2014 preserving. Pass --refresh-scaffolds to overwrite.`);
|
|
1157
|
+
return "drifted-preserved";
|
|
1158
|
+
}
|
|
1159
|
+
var MARKETPLACE_ID = "launch-secure";
|
|
1160
|
+
var PLUGIN_ID = "kit";
|
|
1161
|
+
function isDogfoodMarketplace(targetDir) {
|
|
1162
|
+
const p = path4.join(targetDir, ".claude", "settings.json");
|
|
1163
|
+
if (!fs4.existsSync(p)) return { isDogfood: false };
|
|
1164
|
+
try {
|
|
1165
|
+
const settings = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
1166
|
+
const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
|
|
1167
|
+
if (existingPath && existingPath !== "./.claude/marketplace") {
|
|
1168
|
+
return { isDogfood: true, existingPath };
|
|
1169
|
+
}
|
|
1170
|
+
return { isDogfood: false };
|
|
1171
|
+
} catch {
|
|
1172
|
+
return { isDogfood: false };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
730
1175
|
function scaffoldLsMarketplace(targetDir) {
|
|
731
|
-
const scaffoldsRoot =
|
|
732
|
-
if (!
|
|
1176
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
1177
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
733
1178
|
info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
734
1179
|
return;
|
|
735
1180
|
}
|
|
736
|
-
const
|
|
737
|
-
|
|
1181
|
+
const dogfood = isDogfoodMarketplace(targetDir);
|
|
1182
|
+
if (dogfood.isDogfood) {
|
|
1183
|
+
info(`dogfood marketplace pointer detected (${dogfood.existingPath}) \u2014 skipping copy, only refreshing enabledPlugins`);
|
|
1184
|
+
wireLsSettings(targetDir);
|
|
1185
|
+
ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const marketplaceRoot = path4.join(targetDir, ".claude", "marketplace");
|
|
1189
|
+
info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
|
|
738
1190
|
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
739
1191
|
wireLsSettings(targetDir);
|
|
740
|
-
ok(`
|
|
1192
|
+
ok(`launch-secure marketplace ready \u2014 open this repo in Claude Code, approve the "${MARKETPLACE_ID}" marketplace prompt, then try /kit:activate-beacon, /kit:standup, or /kit:show-mcp-status`);
|
|
741
1193
|
}
|
|
742
1194
|
function wireLsSettings(targetDir) {
|
|
743
|
-
const p =
|
|
744
|
-
const hadExisting =
|
|
1195
|
+
const p = path4.join(targetDir, ".claude", "settings.json");
|
|
1196
|
+
const hadExisting = fs4.existsSync(p);
|
|
745
1197
|
let existing = {};
|
|
746
1198
|
if (hadExisting) {
|
|
747
1199
|
try {
|
|
748
|
-
existing = JSON.parse(
|
|
1200
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
749
1201
|
} catch (err) {
|
|
750
1202
|
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
751
1203
|
}
|
|
752
1204
|
}
|
|
753
1205
|
const merged = { ...existing };
|
|
1206
|
+
const existingMarketplacePath = existing.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
|
|
1207
|
+
const targetPath = existingMarketplacePath ?? "./.claude/marketplace";
|
|
1208
|
+
if (existingMarketplacePath && existingMarketplacePath !== "./.claude/marketplace") {
|
|
1209
|
+
info(`preserving existing marketplace path: ${existingMarketplacePath} (likely dogfood \u2014 leaving alone)`);
|
|
1210
|
+
}
|
|
754
1211
|
merged.extraKnownMarketplaces = {
|
|
755
1212
|
...existing.extraKnownMarketplaces ?? {},
|
|
756
1213
|
[MARKETPLACE_ID]: {
|
|
757
|
-
source: { source: "directory", path:
|
|
1214
|
+
source: { source: "directory", path: targetPath }
|
|
758
1215
|
}
|
|
759
1216
|
};
|
|
1217
|
+
const pluginKey = `${PLUGIN_ID}@${MARKETPLACE_ID}`;
|
|
1218
|
+
const existingEnabledPlugins = existing.enabledPlugins ?? {};
|
|
760
1219
|
merged.enabledPlugins = {
|
|
761
|
-
...
|
|
762
|
-
|
|
1220
|
+
...existingEnabledPlugins,
|
|
1221
|
+
...pluginKey in existingEnabledPlugins ? {} : { [pluginKey]: true }
|
|
763
1222
|
};
|
|
1223
|
+
if (pluginKey in existingEnabledPlugins && existingEnabledPlugins[pluginKey] === false) {
|
|
1224
|
+
info(`enabledPlugins["${pluginKey}"] is explicitly false \u2014 leaving disabled (re-enable manually to use /kit:* commands)`);
|
|
1225
|
+
}
|
|
764
1226
|
if (DRY_RUN) {
|
|
765
|
-
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${
|
|
1227
|
+
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
766
1228
|
return;
|
|
767
1229
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${
|
|
1230
|
+
fs4.mkdirSync(path4.dirname(p), { recursive: true });
|
|
1231
|
+
fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1232
|
+
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
|
|
771
1233
|
}
|
|
772
1234
|
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
773
1235
|
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
774
1236
|
function scaffoldRecallHook(targetDir) {
|
|
775
|
-
const scaffoldsRoot =
|
|
776
|
-
if (!
|
|
1237
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
1238
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
777
1239
|
info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
778
1240
|
return;
|
|
779
1241
|
}
|
|
@@ -783,12 +1245,12 @@ function scaffoldRecallHook(targetDir) {
|
|
|
783
1245
|
ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
|
|
784
1246
|
}
|
|
785
1247
|
function wireRecallHook(targetDir) {
|
|
786
|
-
const p =
|
|
787
|
-
const hadExisting =
|
|
1248
|
+
const p = path4.join(targetDir, ".claude", "settings.json");
|
|
1249
|
+
const hadExisting = fs4.existsSync(p);
|
|
788
1250
|
let existing = {};
|
|
789
1251
|
if (hadExisting) {
|
|
790
1252
|
try {
|
|
791
|
-
existing = JSON.parse(
|
|
1253
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
792
1254
|
} catch (err) {
|
|
793
1255
|
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
794
1256
|
}
|
|
@@ -816,19 +1278,62 @@ function wireRecallHook(targetDir) {
|
|
|
816
1278
|
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
817
1279
|
return;
|
|
818
1280
|
}
|
|
819
|
-
|
|
820
|
-
|
|
1281
|
+
fs4.mkdirSync(path4.dirname(p), { recursive: true });
|
|
1282
|
+
fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
821
1283
|
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
822
1284
|
}
|
|
1285
|
+
function tryActivateStatusline() {
|
|
1286
|
+
if (DRY_RUN) {
|
|
1287
|
+
dryNote(`would wrap ~/.claude/settings.json statusLine.command with launch-kit's MCP chip wrapper (skips silently if no statusline configured)`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const res = activateStatusline();
|
|
1291
|
+
if (res.ok && res.outcome === "activated") {
|
|
1292
|
+
ok(`statusline wrapped \u2014 MCP chips will render alongside your existing statusline next session`);
|
|
1293
|
+
} else if (res.ok && res.outcome === "refreshed") {
|
|
1294
|
+
info(`statusline already wrapped \u2014 refreshed chip scripts`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
823
1297
|
async function main() {
|
|
1298
|
+
const subcommand = process.argv[2];
|
|
1299
|
+
if (subcommand === "statusline") {
|
|
1300
|
+
const action = process.argv[3];
|
|
1301
|
+
if (!action || action === "--help" || action === "-h") {
|
|
1302
|
+
console.log("usage: launch-kit statusline activate [--show=recall,chart,deck,council] [--compact]");
|
|
1303
|
+
console.log(" launch-kit statusline deactivate");
|
|
1304
|
+
console.log("");
|
|
1305
|
+
console.log(" --show comma-separated subset of chips. Default: all four.");
|
|
1306
|
+
console.log(" --compact collapse to `mcp N/total` (green if all up, red otherwise).");
|
|
1307
|
+
console.log(" Re-run activate with different flags to change in place.");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
let showArg;
|
|
1311
|
+
let compactArg = false;
|
|
1312
|
+
for (const a of process.argv.slice(4)) {
|
|
1313
|
+
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
1314
|
+
else if (a === "--compact") compactArg = true;
|
|
1315
|
+
else fail(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
1316
|
+
}
|
|
1317
|
+
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
1318
|
+
let res;
|
|
1319
|
+
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
1320
|
+
else if (action === "deactivate") res = deactivateStatusline2();
|
|
1321
|
+
else fail(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
1322
|
+
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1323
|
+
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
824
1326
|
const args = parseArgs(process.argv.slice(2));
|
|
825
1327
|
if (args.help) {
|
|
826
|
-
|
|
1328
|
+
if (subcommand === "refresh") printRefreshHelp();
|
|
1329
|
+
else printHelp();
|
|
827
1330
|
return;
|
|
828
1331
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1332
|
+
if (!subcommand || subcommand.startsWith("--")) {
|
|
1333
|
+
fail(`missing subcommand. Usage: launch-kit <init|refresh|statusline> [options]. Run with --help.`);
|
|
1334
|
+
}
|
|
1335
|
+
if (subcommand !== "init" && subcommand !== "refresh") {
|
|
1336
|
+
fail(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline. Run with --help for usage.`);
|
|
832
1337
|
}
|
|
833
1338
|
DRY_RUN = args.dryRun;
|
|
834
1339
|
if (DRY_RUN) {
|
|
@@ -837,6 +1342,99 @@ async function main() {
|
|
|
837
1342
|
info("Lines tagged (dry-run) show what would happen.");
|
|
838
1343
|
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
1344
|
}
|
|
1345
|
+
if (subcommand === "refresh") return mainRefresh(args);
|
|
1346
|
+
return mainInit(args);
|
|
1347
|
+
}
|
|
1348
|
+
async function mainRefresh(args) {
|
|
1349
|
+
const cwd = process.cwd();
|
|
1350
|
+
const targetDir = path4.resolve(args.targetDir ?? cwd);
|
|
1351
|
+
if (!fs4.existsSync(targetDir)) fail(`target dir does not exist: ${targetDir}`);
|
|
1352
|
+
let cred;
|
|
1353
|
+
let source;
|
|
1354
|
+
try {
|
|
1355
|
+
const recovery = recoverCred(targetDir, getRecoveryOptions());
|
|
1356
|
+
cred = recovery.cred;
|
|
1357
|
+
source = recovery.source;
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1360
|
+
}
|
|
1361
|
+
if (cred && source === "mcp") {
|
|
1362
|
+
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
1363
|
+
const courseName = inferCourseName(cred.serverUrl);
|
|
1364
|
+
const nested2 = upsertProfile(null, courseName, cred);
|
|
1365
|
+
if (DRY_RUN) {
|
|
1366
|
+
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
1367
|
+
} else {
|
|
1368
|
+
writeJsonAtomic(path4.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
1369
|
+
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (!cred) {
|
|
1373
|
+
fail(
|
|
1374
|
+
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json. Refresh requires an existing cred or a hardcoded launch-secure MCP entry \u2014 run \`npx @launchsecure/launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path4.relative(cwd, targetDir) || "."}\` first.`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const nested = toNested(cred);
|
|
1378
|
+
if (!nested) fail(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
1379
|
+
const active = nested.profiles[nested.active];
|
|
1380
|
+
if (!active) fail(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
1381
|
+
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
1382
|
+
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
1383
|
+
mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
|
|
1384
|
+
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
1385
|
+
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
|
|
1386
|
+
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
1387
|
+
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
1388
|
+
tryActivateStatusline();
|
|
1389
|
+
console.log("");
|
|
1390
|
+
if (DRY_RUN) {
|
|
1391
|
+
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");
|
|
1392
|
+
info(`DRY RUN COMPLETE \u2014 refresh would have applied the above; no files modified.`);
|
|
1393
|
+
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");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
1397
|
+
if (!args.quiet) {
|
|
1398
|
+
console.log(`
|
|
1399
|
+
Skipped (refresh never runs these): clone, dependency install, onboard script, launch-recall init. Use \`npx @launchsecure/launch-kit init\` for a full bootstrap.
|
|
1400
|
+
${getLaunchKitToolsGuide()}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async function mainInit(args) {
|
|
1404
|
+
const probeDir = path4.resolve(args.targetDir ?? process.cwd());
|
|
1405
|
+
if (!args.force && fs4.existsSync(probeDir)) {
|
|
1406
|
+
const detection = detectExistingBootstrap(probeDir);
|
|
1407
|
+
if (detection.bootstrapped) {
|
|
1408
|
+
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
1409
|
+
info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
|
|
1410
|
+
return mainRefresh({ ...args, targetDir: probeDir });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
1414
|
+
const recoveryDir = path4.resolve(args.targetDir ?? process.cwd());
|
|
1415
|
+
if (fs4.existsSync(recoveryDir)) {
|
|
1416
|
+
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
1417
|
+
const nested = cred ? toNested(cred) : null;
|
|
1418
|
+
const recovered = nested ? nested.profiles[nested.active] : cred;
|
|
1419
|
+
if (recovered) {
|
|
1420
|
+
if (!args.token && recovered.pat) {
|
|
1421
|
+
args.token = recovered.pat;
|
|
1422
|
+
info(`recovered --token from existing config in ${recoveryDir}`);
|
|
1423
|
+
}
|
|
1424
|
+
if (!args.orgSlug && recovered.orgSlug) {
|
|
1425
|
+
args.orgSlug = recovered.orgSlug;
|
|
1426
|
+
info(`recovered --org=${recovered.orgSlug} from existing config`);
|
|
1427
|
+
}
|
|
1428
|
+
if (!args.projectSlug && recovered.projectSlug) {
|
|
1429
|
+
args.projectSlug = recovered.projectSlug;
|
|
1430
|
+
info(`recovered --project=${recovered.projectSlug} from existing config`);
|
|
1431
|
+
}
|
|
1432
|
+
if (args.serverUrl === DEFAULT_SERVER_URL && recovered.serverUrl) {
|
|
1433
|
+
args.serverUrl = recovered.serverUrl;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
840
1438
|
if (!args.token) {
|
|
841
1439
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
842
1440
|
args.token = t || null;
|
|
@@ -861,10 +1459,10 @@ async function main() {
|
|
|
861
1459
|
}
|
|
862
1460
|
const repoUrl = resolved.repositoryUrl;
|
|
863
1461
|
const cwd = process.cwd();
|
|
864
|
-
const targetDir =
|
|
1462
|
+
const targetDir = path4.resolve(args.targetDir ?? path4.join(cwd, resolved.projectSlug));
|
|
865
1463
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
866
1464
|
let skipClone = false;
|
|
867
|
-
if (
|
|
1465
|
+
if (fs4.existsSync(targetDir)) {
|
|
868
1466
|
if (isGitRepo(targetDir)) {
|
|
869
1467
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
870
1468
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
@@ -897,14 +1495,15 @@ async function main() {
|
|
|
897
1495
|
installSkippedReason = "no package.json found";
|
|
898
1496
|
} else {
|
|
899
1497
|
runInstall(targetDir, detected);
|
|
900
|
-
if (hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
|
|
1498
|
+
if (!args.noOnboard && hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
|
|
901
1499
|
}
|
|
902
1500
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
903
1501
|
if (!args.noRecall) runRecallInit(targetDir);
|
|
904
|
-
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
|
|
1502
|
+
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
|
|
905
1503
|
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
906
1504
|
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
907
|
-
|
|
1505
|
+
tryActivateStatusline();
|
|
1506
|
+
const relTarget = path4.relative(cwd, targetDir) || ".";
|
|
908
1507
|
console.log("");
|
|
909
1508
|
if (DRY_RUN) {
|
|
910
1509
|
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");
|
|
@@ -917,20 +1516,20 @@ async function main() {
|
|
|
917
1516
|
ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
|
|
918
1517
|
if (installSkippedReason) {
|
|
919
1518
|
const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
|
|
920
|
-
const onboardLine = hasOnboard && detected ? `
|
|
1519
|
+
const onboardLine = hasOnboard && detected && !args.noOnboard ? `
|
|
921
1520
|
${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
|
|
922
1521
|
console.log(`
|
|
923
1522
|
Next steps (install skipped: ${installSkippedReason}):
|
|
924
1523
|
cd ${relTarget}
|
|
925
1524
|
${installLine}${onboardLine}
|
|
926
|
-
claude # launch Claude Code (5 MCPs wired)
|
|
927
|
-
${
|
|
928
|
-
} else {
|
|
1525
|
+
claude # launch Claude Code (5 MCPs wired)${args.quiet ? "" : `
|
|
1526
|
+
${getLaunchKitToolsGuide()}`}`);
|
|
1527
|
+
} else if (!args.quiet) {
|
|
929
1528
|
console.log(`
|
|
930
1529
|
Next steps:
|
|
931
1530
|
cd ${relTarget}
|
|
932
1531
|
claude # launch Claude Code (5 MCPs wired)
|
|
933
|
-
${
|
|
1532
|
+
${getLaunchKitToolsGuide()}`);
|
|
934
1533
|
}
|
|
935
1534
|
}
|
|
936
1535
|
main().catch((err) => {
|