@launchsecure/launch-kit 0.0.27 → 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/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/chart-serve.js +3 -1
- package/dist/server/cli.js +276 -218
- package/dist/server/course-entry.js +246 -0
- package/dist/server/graph-mcp-entry.js +35 -72
- package/dist/server/init-entry.js +1051 -122
- package/dist/server/orbit-entry.js +187 -24
- package/package.json +5 -3
- package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
- package/scaffolds/ls-marketplace/plugins/kit/.claude-plugin/plugin.json +19 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-beacon.md +216 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +46 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/beacon-array.md +92 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/beacon-clear.md +68 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/beacon-pulse.md +80 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/beacon-scan.md +62 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/deactivate-statusline.md +34 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/show-mcp-status.md +109 -0
- package/scaffolds/ls-marketplace/plugins/kit/commands/standup.md +191 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -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,20 +30,273 @@ 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
|
+
|
|
149
|
+
// src/server/cred-shape.ts
|
|
150
|
+
var fs = __toESM(require("node:fs"));
|
|
151
|
+
var path = __toESM(require("node:path"));
|
|
35
152
|
var CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
153
|
+
function inferCourseName(serverUrl) {
|
|
154
|
+
try {
|
|
155
|
+
const host = new URL(serverUrl).hostname.toLowerCase();
|
|
156
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
|
|
157
|
+
if (host.includes("staging")) return "staging";
|
|
158
|
+
if (host.endsWith(".vercel.app")) return "prod";
|
|
159
|
+
return host.split(".")[0] || "default";
|
|
160
|
+
} catch {
|
|
161
|
+
return "default";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function toNested(cred) {
|
|
165
|
+
if (cred.profiles && cred.active && cred.profiles[cred.active]) {
|
|
166
|
+
return { active: cred.active, profiles: cred.profiles };
|
|
167
|
+
}
|
|
168
|
+
if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const name = inferCourseName(cred.serverUrl);
|
|
172
|
+
return {
|
|
173
|
+
active: name,
|
|
174
|
+
profiles: {
|
|
175
|
+
[name]: {
|
|
176
|
+
pat: cred.pat,
|
|
177
|
+
orgSlug: cred.orgSlug,
|
|
178
|
+
projectSlug: cred.projectSlug,
|
|
179
|
+
serverUrl: cred.serverUrl
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function upsertProfile(existing, name, profile) {
|
|
185
|
+
const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
|
|
186
|
+
return {
|
|
187
|
+
active: name,
|
|
188
|
+
profiles: { ...base.profiles, [name]: profile }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function readCredFile(repoRoot) {
|
|
192
|
+
const p = path.join(repoRoot, CONFIG_FILENAME);
|
|
193
|
+
if (!fs.existsSync(p)) return null;
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function writeJsonAtomic(absPath, value, mode) {
|
|
201
|
+
const tmp = `${absPath}.tmp`;
|
|
202
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
203
|
+
if (mode !== void 0) {
|
|
204
|
+
try {
|
|
205
|
+
fs.chmodSync(tmp, mode);
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
fs.renameSync(tmp, absPath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/server/cred-recovery.ts
|
|
213
|
+
var fs2 = __toESM(require("node:fs"));
|
|
214
|
+
var path2 = __toESM(require("node:path"));
|
|
36
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
|
+
|
|
294
|
+
// src/server/init-entry.ts
|
|
295
|
+
init_statusline_install();
|
|
296
|
+
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
37
297
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
38
298
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
39
|
-
var
|
|
299
|
+
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
40
300
|
Wired in Claude Code (.mcp.json):
|
|
41
301
|
launch-secure \u2014 LS API: work items, comms, secrets, members, board
|
|
42
302
|
launch-chart \u2014 code search + project graph (use instead of grep/glob)
|
|
@@ -47,13 +307,60 @@ Wired in Claude Code (.mcp.json):
|
|
|
47
307
|
Other tools (run on demand via npx):
|
|
48
308
|
npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
|
|
49
309
|
npx launch-pod \u2014 full pipeline UI (separate launch-pod login)
|
|
310
|
+
npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
|
|
311
|
+
in-browser monitor. Paste the printed URL into
|
|
312
|
+
the beacon debug panel; events stream to
|
|
313
|
+
.launchsecure/beacon-<token>.ndjson for the
|
|
314
|
+
/kit:beacon-* commands below to read.
|
|
315
|
+
`;
|
|
316
|
+
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
|
|
317
|
+
Open this repo in Claude Code; on first open you'll be prompted to install
|
|
318
|
+
the "launch-secure" marketplace \u2014 accept to enable the commands above.
|
|
50
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")}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
function getLaunchKitToolsGuide() {
|
|
341
|
+
return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
|
|
342
|
+
}
|
|
51
343
|
var PACKAGE_MANAGERS = [
|
|
52
344
|
{ name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
|
|
53
345
|
{ name: "yarn", binary: "yarn", lockfiles: ["yarn.lock"], installArgs: ["install"] },
|
|
54
346
|
{ name: "bun", binary: "bun", lockfiles: ["bun.lockb", "bun.lock"], installArgs: ["install"] },
|
|
55
347
|
{ name: "npm", binary: "npm", lockfiles: ["package-lock.json"], installArgs: ["install"] }
|
|
56
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"]);
|
|
57
364
|
function parseArgs(argv) {
|
|
58
365
|
const args = {
|
|
59
366
|
token: process.env.LS_PAT ?? null,
|
|
@@ -61,12 +368,23 @@ function parseArgs(argv) {
|
|
|
61
368
|
projectSlug: null,
|
|
62
369
|
serverUrl: DEFAULT_SERVER_URL,
|
|
63
370
|
targetDir: null,
|
|
371
|
+
course: null,
|
|
64
372
|
noInstall: false,
|
|
373
|
+
noOnboard: false,
|
|
65
374
|
noRecall: false,
|
|
66
375
|
noMigrateSafety: false,
|
|
376
|
+
noLsMarketplace: false,
|
|
377
|
+
noRecallHook: false,
|
|
378
|
+
refreshScaffolds: false,
|
|
379
|
+
quiet: false,
|
|
380
|
+
force: false,
|
|
381
|
+
dryRun: false,
|
|
67
382
|
help: false
|
|
68
383
|
};
|
|
69
|
-
|
|
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;
|
|
70
388
|
if (raw === "--help" || raw === "-h") {
|
|
71
389
|
args.help = true;
|
|
72
390
|
continue;
|
|
@@ -75,6 +393,10 @@ function parseArgs(argv) {
|
|
|
75
393
|
args.noInstall = true;
|
|
76
394
|
continue;
|
|
77
395
|
}
|
|
396
|
+
if (raw === "--no-onboard") {
|
|
397
|
+
args.noOnboard = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
78
400
|
if (raw === "--no-recall") {
|
|
79
401
|
args.noRecall = true;
|
|
80
402
|
continue;
|
|
@@ -83,23 +405,124 @@ function parseArgs(argv) {
|
|
|
83
405
|
args.noMigrateSafety = true;
|
|
84
406
|
continue;
|
|
85
407
|
}
|
|
408
|
+
if (raw === "--no-ls-marketplace") {
|
|
409
|
+
args.noLsMarketplace = true;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (raw === "--no-recall-hook") {
|
|
413
|
+
args.noRecallHook = true;
|
|
414
|
+
continue;
|
|
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
|
+
}
|
|
428
|
+
if (raw === "--dry-run") {
|
|
429
|
+
args.dryRun = true;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
86
432
|
const eq = raw.indexOf("=");
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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}`);
|
|
95
475
|
}
|
|
96
476
|
return args;
|
|
97
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
|
+
}
|
|
98
511
|
function printHelp() {
|
|
99
|
-
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.
|
|
100
522
|
|
|
101
523
|
Usage:
|
|
102
|
-
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]
|
|
103
526
|
|
|
104
527
|
Required:
|
|
105
528
|
--token=<pat> LaunchSecure PAT (ls_pat_...). Or set LS_PAT env var.
|
|
@@ -109,10 +532,45 @@ Required:
|
|
|
109
532
|
Options:
|
|
110
533
|
--url=<serverUrl> LaunchSecure base URL (default: ${DEFAULT_SERVER_URL}).
|
|
111
534
|
--dir=<path> Target directory (default: ./<projectSlug>).
|
|
112
|
-
--
|
|
535
|
+
--course=<name> Name for the course (profile) being added to
|
|
536
|
+
.launch-secure.cred.config. When omitted, inferred
|
|
537
|
+
from --url: localhost\u2192"local", *staging*\u2192"staging",
|
|
538
|
+
*.vercel.app\u2192"prod", else hostname. The course
|
|
539
|
+
becomes active; re-run with a different --course
|
|
540
|
+
and --url to add another (e.g. local + staging).
|
|
541
|
+
Use \`launch-course set <name>\` to switch later.
|
|
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.
|
|
113
545
|
--no-recall Skip launch-recall (shadow git backup) scaffold.
|
|
114
546
|
--no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
|
|
115
547
|
wrapper + GitHub Action + runbook).
|
|
548
|
+
--no-ls-marketplace Skip the Claude Code "launch-secure" marketplace
|
|
549
|
+
scaffold (.claude/marketplace/ + .claude/settings.json
|
|
550
|
+
wiring \u2014 exposes /kit:activate-beacon and future
|
|
551
|
+
ls-namespaced slash commands).
|
|
552
|
+
--no-recall-hook Skip the SessionStart hook scaffold (Claude Code
|
|
553
|
+
hook + scripts/ensure-recall.sh that auto-restarts
|
|
554
|
+
the launch-recall watcher if it died between
|
|
555
|
+
sessions). The hook is the surfacing layer for
|
|
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.
|
|
570
|
+
--dry-run Preview every file write, merge, clone, and install
|
|
571
|
+
command without making any changes. Useful before
|
|
572
|
+
re-running init against a customized project. The
|
|
573
|
+
project_info HTTP call still runs (it's read-only).
|
|
116
574
|
--help Show this help.
|
|
117
575
|
|
|
118
576
|
What it does:
|
|
@@ -132,13 +590,22 @@ What it does:
|
|
|
132
590
|
8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
|
|
133
591
|
9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
|
|
134
592
|
runbook + .backups/ gitignore line). Skip with --no-migrate-safety.
|
|
593
|
+
10. Scaffolds the Claude Code "launch-secure" marketplace at
|
|
594
|
+
.claude/marketplace/ and wires .claude/settings.json so Claude Code
|
|
595
|
+
auto-discovers it and enables the "kit" plugin (exposes
|
|
596
|
+
/kit:activate-beacon for wiring the launch-kit-beacon in-app feedback
|
|
597
|
+
widget). Skip with --no-ls-marketplace.
|
|
598
|
+
11. Scaffolds scripts/ensure-recall.sh and appends a SessionStart hook to
|
|
599
|
+
.claude/settings.json that respawns the launch-recall watcher if it
|
|
600
|
+
died between sessions. Idempotent (dedupes by hook command-match).
|
|
601
|
+
Skip with --no-recall-hook.
|
|
135
602
|
`);
|
|
136
603
|
}
|
|
137
604
|
async function prompt(question) {
|
|
138
605
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
139
|
-
return new Promise((
|
|
606
|
+
return new Promise((resolve3) => rl.question(question, (answer) => {
|
|
140
607
|
rl.close();
|
|
141
|
-
|
|
608
|
+
resolve3(answer.trim());
|
|
142
609
|
}));
|
|
143
610
|
}
|
|
144
611
|
function fail(msg) {
|
|
@@ -151,6 +618,10 @@ function info(msg) {
|
|
|
151
618
|
function ok(msg) {
|
|
152
619
|
console.log(`[launch-kit] \u2713 ${msg}`);
|
|
153
620
|
}
|
|
621
|
+
var DRY_RUN = false;
|
|
622
|
+
function dryNote(msg) {
|
|
623
|
+
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
624
|
+
}
|
|
154
625
|
function which(bin) {
|
|
155
626
|
const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
156
627
|
if (res.status !== 0) return null;
|
|
@@ -164,8 +635,17 @@ function preflight() {
|
|
|
164
635
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
165
636
|
return { hasGh };
|
|
166
637
|
}
|
|
167
|
-
|
|
168
|
-
|
|
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) => {
|
|
169
649
|
const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
|
|
170
650
|
const body = JSON.stringify({
|
|
171
651
|
jsonrpc: "2.0",
|
|
@@ -197,12 +677,24 @@ function callProjectInfo(args) {
|
|
|
197
677
|
res.on("data", (c) => chunks.push(c));
|
|
198
678
|
res.on("end", () => {
|
|
199
679
|
const text = Buffer.concat(chunks).toString("utf-8");
|
|
200
|
-
if (res.statusCode === 401
|
|
201
|
-
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)}`));
|
|
202
694
|
return;
|
|
203
695
|
}
|
|
204
696
|
if (res.statusCode && res.statusCode >= 400) {
|
|
205
|
-
reject(new
|
|
697
|
+
reject(new ProjectInfoHttpError(res.statusCode, false, `LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
206
698
|
return;
|
|
207
699
|
}
|
|
208
700
|
let json = text;
|
|
@@ -231,7 +723,7 @@ function callProjectInfo(args) {
|
|
|
231
723
|
return;
|
|
232
724
|
}
|
|
233
725
|
const payload = JSON.parse(inner);
|
|
234
|
-
|
|
726
|
+
resolve3({
|
|
235
727
|
orgSlug: payload.org.slug,
|
|
236
728
|
projectSlug: payload.project.slug,
|
|
237
729
|
projectName: payload.project.name,
|
|
@@ -243,11 +735,34 @@ function callProjectInfo(args) {
|
|
|
243
735
|
});
|
|
244
736
|
}
|
|
245
737
|
);
|
|
246
|
-
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
|
+
});
|
|
247
746
|
req.write(body);
|
|
248
747
|
req.end();
|
|
249
748
|
});
|
|
250
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
|
+
}
|
|
251
766
|
function gitRemoteUrl(dir) {
|
|
252
767
|
const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
253
768
|
if (res.status !== 0) return null;
|
|
@@ -265,11 +780,11 @@ function normalizeRepoUrl(url) {
|
|
|
265
780
|
}
|
|
266
781
|
}
|
|
267
782
|
function isGitRepo(dir) {
|
|
268
|
-
return
|
|
783
|
+
return fs4.existsSync(path4.join(dir, ".git"));
|
|
269
784
|
}
|
|
270
785
|
function dirIsEmpty(dir) {
|
|
271
|
-
if (!
|
|
272
|
-
return
|
|
786
|
+
if (!fs4.existsSync(dir)) return true;
|
|
787
|
+
return fs4.readdirSync(dir).length === 0;
|
|
273
788
|
}
|
|
274
789
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
275
790
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -284,51 +799,59 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
284
799
|
args = ["clone", repoUrl, targetDir];
|
|
285
800
|
info(`cloning via git: ${repoUrl} \u2192 ${targetDir}`);
|
|
286
801
|
}
|
|
802
|
+
if (DRY_RUN) {
|
|
803
|
+
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
287
806
|
const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
288
807
|
if (res.status !== 0) {
|
|
289
808
|
fail(
|
|
290
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.`
|
|
291
810
|
);
|
|
292
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
|
+
}
|
|
293
815
|
ok(`cloned to ${targetDir}`);
|
|
294
816
|
}
|
|
295
|
-
function writeConfigFile(targetDir, cfg) {
|
|
296
|
-
|
|
297
|
-
const p =
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
817
|
+
function writeConfigFile(targetDir, cfg, courseName) {
|
|
818
|
+
recoverCred(targetDir, getRecoveryOptions());
|
|
819
|
+
const p = path4.join(targetDir, CONFIG_FILENAME);
|
|
820
|
+
const existing = readCredFile(targetDir);
|
|
821
|
+
const isNew = existing === null;
|
|
822
|
+
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
823
|
+
if (DRY_RUN) {
|
|
824
|
+
const verb = isNew ? "write" : isUpdate ? "update course" : "add course";
|
|
825
|
+
dryNote(`would ${verb} "${courseName}" in ${CONFIG_FILENAME} (org=${cfg.orgSlug}, project=${cfg.projectSlug}, url=${cfg.serverUrl})`);
|
|
826
|
+
return;
|
|
303
827
|
}
|
|
304
|
-
|
|
828
|
+
const nested = upsertProfile(existing, courseName, cfg);
|
|
829
|
+
writeJsonAtomic(p, nested, 384);
|
|
830
|
+
const action = isNew ? "wrote" : isUpdate ? `updated course "${courseName}" in` : `added course "${courseName}" to`;
|
|
831
|
+
ok(`${action} ${CONFIG_FILENAME} (active: ${courseName})`);
|
|
305
832
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 };
|
|
840
|
+
}
|
|
841
|
+
const mcpPath = path4.join(targetDir, ".mcp.json");
|
|
842
|
+
if (!fs4.existsSync(mcpPath)) {
|
|
843
|
+
return { bootstrapped: false };
|
|
844
|
+
}
|
|
311
845
|
try {
|
|
312
|
-
|
|
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
|
+
}
|
|
313
850
|
} catch {
|
|
314
|
-
return;
|
|
315
851
|
}
|
|
316
|
-
|
|
317
|
-
if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
|
|
318
|
-
fs.renameSync(legacy, dest);
|
|
319
|
-
removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME);
|
|
320
|
-
ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
|
|
321
|
-
}
|
|
322
|
-
function removeGitignoreLine(targetDir, line) {
|
|
323
|
-
const p = path.join(targetDir, ".gitignore");
|
|
324
|
-
if (!fs.existsSync(p)) return;
|
|
325
|
-
const before = fs.readFileSync(p, "utf-8");
|
|
326
|
-
const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
|
|
327
|
-
if (after === before) return;
|
|
328
|
-
fs.writeFileSync(p, after, "utf-8");
|
|
329
|
-
ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
|
|
852
|
+
return { bootstrapped: false };
|
|
330
853
|
}
|
|
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 "+
|
|
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}))'`;
|
|
332
855
|
function buildLaunchKitMcpEntries(cfg) {
|
|
333
856
|
return {
|
|
334
857
|
"launch-secure": {
|
|
@@ -339,6 +862,11 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
339
862
|
"launch-chart": {
|
|
340
863
|
command: "npx",
|
|
341
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.
|
|
342
870
|
env: { LAUNCH_CHART_AUTOSERVE: "1" }
|
|
343
871
|
},
|
|
344
872
|
"launch-deck": {
|
|
@@ -355,31 +883,61 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
355
883
|
}
|
|
356
884
|
};
|
|
357
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
|
+
}
|
|
358
902
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
359
|
-
const p =
|
|
360
|
-
const hadExisting =
|
|
903
|
+
const p = path4.join(targetDir, ".mcp.json");
|
|
904
|
+
const hadExisting = fs4.existsSync(p);
|
|
361
905
|
let existing = {};
|
|
362
906
|
if (hadExisting) {
|
|
363
907
|
try {
|
|
364
|
-
existing = JSON.parse(
|
|
908
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
365
909
|
} catch (err) {
|
|
366
910
|
fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
911
|
}
|
|
368
912
|
}
|
|
369
913
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
370
914
|
const merged = { ...existing, mcpServers: { ...existing.mcpServers ?? {} } };
|
|
915
|
+
const overwrites = [];
|
|
916
|
+
const additions = [];
|
|
371
917
|
for (const [name, entry] of Object.entries(launchKitEntries)) {
|
|
372
|
-
merged.mcpServers[name]
|
|
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
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (DRY_RUN) {
|
|
928
|
+
const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
|
|
929
|
+
dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
930
|
+
return;
|
|
373
931
|
}
|
|
374
|
-
|
|
932
|
+
fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
375
933
|
const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
|
|
376
934
|
ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
377
935
|
}
|
|
378
936
|
function detectPackageManager(repoDir) {
|
|
379
|
-
const pkgPath =
|
|
380
|
-
if (!
|
|
937
|
+
const pkgPath = path4.join(repoDir, "package.json");
|
|
938
|
+
if (!fs4.existsSync(pkgPath)) return null;
|
|
381
939
|
try {
|
|
382
|
-
const pkg = JSON.parse(
|
|
940
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
383
941
|
if (typeof pkg.packageManager === "string") {
|
|
384
942
|
const name = pkg.packageManager.split("@")[0];
|
|
385
943
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -388,7 +946,7 @@ function detectPackageManager(repoDir) {
|
|
|
388
946
|
}
|
|
389
947
|
} catch {
|
|
390
948
|
}
|
|
391
|
-
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);
|
|
392
950
|
if (matches.length === 1) {
|
|
393
951
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
394
952
|
}
|
|
@@ -397,8 +955,8 @@ function detectPackageManager(repoDir) {
|
|
|
397
955
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
398
956
|
}
|
|
399
957
|
for (const pm of PACKAGE_MANAGERS) {
|
|
400
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
401
|
-
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)))})` };
|
|
402
960
|
}
|
|
403
961
|
}
|
|
404
962
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -408,23 +966,43 @@ function runInstall(repoDir, detected) {
|
|
|
408
966
|
const { pm } = detected;
|
|
409
967
|
if (!which(pm.binary)) {
|
|
410
968
|
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 ${
|
|
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(" ")}`
|
|
412
970
|
);
|
|
413
971
|
}
|
|
414
972
|
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
973
|
+
if (DRY_RUN) {
|
|
974
|
+
dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
415
977
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
416
978
|
if (res.status !== 0) {
|
|
417
979
|
fail(
|
|
418
|
-
`${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.`
|
|
419
997
|
);
|
|
420
998
|
}
|
|
421
999
|
ok(`${pm.name} install complete`);
|
|
422
1000
|
}
|
|
423
1001
|
function hasOnboardScript(repoDir) {
|
|
424
|
-
const pkgPath =
|
|
425
|
-
if (!
|
|
1002
|
+
const pkgPath = path4.join(repoDir, "package.json");
|
|
1003
|
+
if (!fs4.existsSync(pkgPath)) return false;
|
|
426
1004
|
try {
|
|
427
|
-
const pkg = JSON.parse(
|
|
1005
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
428
1006
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
429
1007
|
} catch {
|
|
430
1008
|
return false;
|
|
@@ -432,91 +1010,430 @@ function hasOnboardScript(repoDir) {
|
|
|
432
1010
|
}
|
|
433
1011
|
function runRecallInit(repoDir) {
|
|
434
1012
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
435
|
-
const recallEntry =
|
|
436
|
-
const useSibling =
|
|
1013
|
+
const recallEntry = path4.resolve(__dirname, "recall-entry.js");
|
|
1014
|
+
const useSibling = fs4.existsSync(recallEntry);
|
|
437
1015
|
const cmd = useSibling ? process.execPath : "npx";
|
|
438
1016
|
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
1017
|
+
if (DRY_RUN) {
|
|
1018
|
+
dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
439
1021
|
const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
440
1022
|
if (res.status !== 0) {
|
|
441
|
-
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`);
|
|
442
1024
|
return;
|
|
443
1025
|
}
|
|
444
1026
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
445
1027
|
}
|
|
446
1028
|
function runOnboard(repoDir, pm) {
|
|
447
1029
|
info(`running ${pm.binary} run ${ONBOARD_SCRIPT_NAME} \u2026`);
|
|
1030
|
+
if (DRY_RUN) {
|
|
1031
|
+
dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
448
1034
|
const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
449
1035
|
if (res.status !== 0) {
|
|
450
1036
|
fail(
|
|
451
|
-
`${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}`
|
|
452
1038
|
);
|
|
453
1039
|
}
|
|
454
1040
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
455
1041
|
}
|
|
456
1042
|
function ensureGitignoreLine(targetDir, line) {
|
|
457
|
-
const p =
|
|
458
|
-
let content =
|
|
1043
|
+
const p = path4.join(targetDir, ".gitignore");
|
|
1044
|
+
let content = fs4.existsSync(p) ? fs4.readFileSync(p, "utf-8") : "";
|
|
459
1045
|
const lines = content.split(/\r?\n/);
|
|
460
1046
|
if (lines.some((l) => l.trim() === line)) return;
|
|
461
1047
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
462
1048
|
content += `${line}
|
|
463
1049
|
`;
|
|
464
|
-
|
|
1050
|
+
if (DRY_RUN) {
|
|
1051
|
+
dryNote(`would append "${line}" to .gitignore`);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
fs4.writeFileSync(p, content, "utf-8");
|
|
465
1055
|
ok(`appended ${line} to .gitignore`);
|
|
466
1056
|
}
|
|
467
|
-
function
|
|
468
|
-
if (!
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1057
|
+
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
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);
|
|
1062
|
+
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
1063
|
+
if (entry.isDirectory()) {
|
|
1064
|
+
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
1065
|
+
} else if (entry.isFile()) {
|
|
1066
|
+
const existed = fs4.existsSync(destPath);
|
|
1067
|
+
if (DRY_RUN) {
|
|
1068
|
+
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
fs4.mkdirSync(path4.dirname(destPath), { recursive: true });
|
|
1072
|
+
fs4.copyFileSync(srcPath, destPath);
|
|
1073
|
+
try {
|
|
1074
|
+
const srcMode = fs4.statSync(srcPath).mode;
|
|
1075
|
+
fs4.chmodSync(destPath, srcMode);
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
1079
|
+
}
|
|
479
1080
|
}
|
|
480
|
-
ok(`wrote ${label}`);
|
|
481
|
-
return "wrote";
|
|
482
1081
|
}
|
|
483
|
-
function scaffoldMigrateSafety(targetDir) {
|
|
484
|
-
const scaffoldsRoot =
|
|
485
|
-
if (!
|
|
1082
|
+
function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
|
|
1083
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
1084
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
486
1085
|
info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
487
1086
|
return;
|
|
488
1087
|
}
|
|
489
1088
|
const files = [
|
|
490
1089
|
{
|
|
491
|
-
src:
|
|
492
|
-
dest:
|
|
1090
|
+
src: path4.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
1091
|
+
dest: path4.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
493
1092
|
label: ".github/workflows/backup-on-migration.yml"
|
|
494
1093
|
},
|
|
495
1094
|
{
|
|
496
|
-
src:
|
|
497
|
-
dest:
|
|
1095
|
+
src: path4.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
1096
|
+
dest: path4.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
498
1097
|
label: "scripts/migrate-with-backup.sh"
|
|
499
1098
|
},
|
|
500
1099
|
{
|
|
501
|
-
src:
|
|
502
|
-
dest:
|
|
1100
|
+
src: path4.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
1101
|
+
dest: path4.join(targetDir, "docs", "migrations-runbook.md"),
|
|
503
1102
|
label: "docs/migrations-runbook.md"
|
|
504
1103
|
}
|
|
505
1104
|
];
|
|
506
|
-
info(
|
|
507
|
-
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);
|
|
508
1107
|
ensureGitignoreLine(targetDir, ".backups/");
|
|
509
1108
|
ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
|
|
510
1109
|
}
|
|
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
|
+
}
|
|
1175
|
+
function scaffoldLsMarketplace(targetDir) {
|
|
1176
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
1177
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
1178
|
+
info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
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");
|
|
1190
|
+
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
1191
|
+
wireLsSettings(targetDir);
|
|
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`);
|
|
1193
|
+
}
|
|
1194
|
+
function wireLsSettings(targetDir) {
|
|
1195
|
+
const p = path4.join(targetDir, ".claude", "settings.json");
|
|
1196
|
+
const hadExisting = fs4.existsSync(p);
|
|
1197
|
+
let existing = {};
|
|
1198
|
+
if (hadExisting) {
|
|
1199
|
+
try {
|
|
1200
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
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
|
+
}
|
|
1211
|
+
merged.extraKnownMarketplaces = {
|
|
1212
|
+
...existing.extraKnownMarketplaces ?? {},
|
|
1213
|
+
[MARKETPLACE_ID]: {
|
|
1214
|
+
source: { source: "directory", path: targetPath }
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
const pluginKey = `${PLUGIN_ID}@${MARKETPLACE_ID}`;
|
|
1218
|
+
const existingEnabledPlugins = existing.enabledPlugins ?? {};
|
|
1219
|
+
merged.enabledPlugins = {
|
|
1220
|
+
...existingEnabledPlugins,
|
|
1221
|
+
...pluginKey in existingEnabledPlugins ? {} : { [pluginKey]: true }
|
|
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
|
+
}
|
|
1226
|
+
if (DRY_RUN) {
|
|
1227
|
+
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
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})`);
|
|
1233
|
+
}
|
|
1234
|
+
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
1235
|
+
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
1236
|
+
function scaffoldRecallHook(targetDir) {
|
|
1237
|
+
const scaffoldsRoot = path4.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
1238
|
+
if (!fs4.existsSync(scaffoldsRoot)) {
|
|
1239
|
+
info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
info("scaffolding recall-hook (SessionStart watcher-respawn hook + ensure-recall.sh) \u2026");
|
|
1243
|
+
copyScaffoldDirAlways(scaffoldsRoot, targetDir, "");
|
|
1244
|
+
wireRecallHook(targetDir);
|
|
1245
|
+
ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
|
|
1246
|
+
}
|
|
1247
|
+
function wireRecallHook(targetDir) {
|
|
1248
|
+
const p = path4.join(targetDir, ".claude", "settings.json");
|
|
1249
|
+
const hadExisting = fs4.existsSync(p);
|
|
1250
|
+
let existing = {};
|
|
1251
|
+
if (hadExisting) {
|
|
1252
|
+
try {
|
|
1253
|
+
existing = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const hooks = existing.hooks ?? {};
|
|
1259
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
1260
|
+
const alreadyWired = sessionStart.some(
|
|
1261
|
+
(group) => (group.hooks ?? []).some((h) => typeof h?.command === "string" && h.command.includes(RECALL_HOOK_SIGNATURE))
|
|
1262
|
+
);
|
|
1263
|
+
if (alreadyWired) {
|
|
1264
|
+
info(".claude/settings.json SessionStart hook already references ensure-recall.sh \u2014 leaving alone");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const newGroup = {
|
|
1268
|
+
hooks: [{ type: "command", command: RECALL_HOOK_COMMAND }]
|
|
1269
|
+
};
|
|
1270
|
+
const merged = {
|
|
1271
|
+
...existing,
|
|
1272
|
+
hooks: {
|
|
1273
|
+
...hooks,
|
|
1274
|
+
SessionStart: [...sessionStart, newGroup]
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
if (DRY_RUN) {
|
|
1278
|
+
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
fs4.mkdirSync(path4.dirname(p), { recursive: true });
|
|
1282
|
+
fs4.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1283
|
+
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
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
|
+
}
|
|
511
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
|
+
}
|
|
512
1326
|
const args = parseArgs(process.argv.slice(2));
|
|
513
1327
|
if (args.help) {
|
|
514
|
-
|
|
1328
|
+
if (subcommand === "refresh") printRefreshHelp();
|
|
1329
|
+
else printHelp();
|
|
515
1330
|
return;
|
|
516
1331
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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.`);
|
|
1337
|
+
}
|
|
1338
|
+
DRY_RUN = args.dryRun;
|
|
1339
|
+
if (DRY_RUN) {
|
|
1340
|
+
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");
|
|
1341
|
+
info("DRY RUN \u2014 no files will be written, no commands will run.");
|
|
1342
|
+
info("Lines tagged (dry-run) show what would happen.");
|
|
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");
|
|
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
|
+
}
|
|
520
1437
|
}
|
|
521
1438
|
if (!args.token) {
|
|
522
1439
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
@@ -542,10 +1459,10 @@ async function main() {
|
|
|
542
1459
|
}
|
|
543
1460
|
const repoUrl = resolved.repositoryUrl;
|
|
544
1461
|
const cwd = process.cwd();
|
|
545
|
-
const targetDir =
|
|
1462
|
+
const targetDir = path4.resolve(args.targetDir ?? path4.join(cwd, resolved.projectSlug));
|
|
546
1463
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
547
1464
|
let skipClone = false;
|
|
548
|
-
if (
|
|
1465
|
+
if (fs4.existsSync(targetDir)) {
|
|
549
1466
|
if (isGitRepo(targetDir)) {
|
|
550
1467
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
551
1468
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
@@ -565,7 +1482,8 @@ async function main() {
|
|
|
565
1482
|
projectSlug: resolved.projectSlug,
|
|
566
1483
|
serverUrl: args.serverUrl
|
|
567
1484
|
};
|
|
568
|
-
|
|
1485
|
+
const courseName = args.course ?? inferCourseName(cfg.serverUrl);
|
|
1486
|
+
writeConfigFile(targetDir, cfg, courseName);
|
|
569
1487
|
mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
|
|
570
1488
|
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
571
1489
|
let installSkippedReason = null;
|
|
@@ -577,30 +1495,41 @@ async function main() {
|
|
|
577
1495
|
installSkippedReason = "no package.json found";
|
|
578
1496
|
} else {
|
|
579
1497
|
runInstall(targetDir, detected);
|
|
580
|
-
if (hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
|
|
1498
|
+
if (!args.noOnboard && hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
|
|
581
1499
|
}
|
|
582
1500
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
583
1501
|
if (!args.noRecall) runRecallInit(targetDir);
|
|
584
|
-
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
|
|
585
|
-
|
|
1502
|
+
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
|
|
1503
|
+
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
1504
|
+
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
1505
|
+
tryActivateStatusline();
|
|
1506
|
+
const relTarget = path4.relative(cwd, targetDir) || ".";
|
|
586
1507
|
console.log("");
|
|
1508
|
+
if (DRY_RUN) {
|
|
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");
|
|
1510
|
+
info(`DRY RUN COMPLETE \u2014 no files were modified, no commands ran.`);
|
|
1511
|
+
info(`Target: ${targetDir}`);
|
|
1512
|
+
info(`Re-run without --dry-run to apply the changes shown above.`);
|
|
1513
|
+
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");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
587
1516
|
ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
|
|
588
1517
|
if (installSkippedReason) {
|
|
589
1518
|
const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
|
|
590
|
-
const onboardLine = hasOnboard && detected ? `
|
|
1519
|
+
const onboardLine = hasOnboard && detected && !args.noOnboard ? `
|
|
591
1520
|
${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
|
|
592
1521
|
console.log(`
|
|
593
1522
|
Next steps (install skipped: ${installSkippedReason}):
|
|
594
1523
|
cd ${relTarget}
|
|
595
1524
|
${installLine}${onboardLine}
|
|
596
|
-
claude # launch Claude Code (5 MCPs wired)
|
|
597
|
-
${
|
|
598
|
-
} else {
|
|
1525
|
+
claude # launch Claude Code (5 MCPs wired)${args.quiet ? "" : `
|
|
1526
|
+
${getLaunchKitToolsGuide()}`}`);
|
|
1527
|
+
} else if (!args.quiet) {
|
|
599
1528
|
console.log(`
|
|
600
1529
|
Next steps:
|
|
601
1530
|
cd ${relTarget}
|
|
602
1531
|
claude # launch Claude Code (5 MCPs wired)
|
|
603
|
-
${
|
|
1532
|
+
${getLaunchKitToolsGuide()}`);
|
|
604
1533
|
}
|
|
605
1534
|
}
|
|
606
1535
|
main().catch((err) => {
|