@launchsecure/launch-kit 0.0.31 → 0.0.33
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 +550 -521
- package/dist/beacon/beacon.mjs.map +1 -1
- package/dist/beacon/beacon.umd.js +9 -9
- package/dist/beacon/beacon.umd.js.map +1 -1
- package/dist/beacon/types/internal/pick-mode-overlay.d.ts.map +1 -1
- package/dist/beacon/types/internal/picker.d.ts.map +1 -1
- package/dist/beacon/types/internal/pin-popover.d.ts.map +1 -1
- package/dist/beacon/types/internal/selector.d.ts.map +1 -1
- package/dist/chart-client/assets/{index-B__ARB8k.js → index-DFu2xIrM.js} +2 -2
- package/dist/chart-client/assets/index-DpKO9p0s.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/{index-h8kMzVtG.js → index-Cbw6bVdx.js} +2 -2
- package/dist/client/assets/index-Dv6dD2zY.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-AqQ9Sei6.css +1 -0
- package/dist/council-client/assets/{index-CWaDcsFR.js → index-CAsmGTzg.js} +2 -2
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-DdHaBFYO.js → _baseUniq-BiVx0WO_.js} +1 -1
- package/dist/deck-client/assets/{arc-D98e_18X.js → arc-DGMkiEzS.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-DNFZzh-4.js → architectureDiagram-Q4EWVU46-Y2WRmHtk.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-DeQvGUdX.js → blockDiagram-DXYQGD6D-_Lbfu5BQ.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-B6ekZf1n.js → c4Diagram-AHTNJAMY-CTqpYTBX.js} +1 -1
- package/dist/deck-client/assets/channel-DB6LxW_l.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-9aDWymq2.js → chunk-4BX2VUAB-liEIbPHs.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-DtKQqaI7.js → chunk-4TB4RGXK-CCc6lYvL.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-COy9hEae.js → chunk-55IACEB6-D02jJUR2.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-D_f861An.js → chunk-EDXVE4YY-BFmGMbLD.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-CmuA5UKn.js → chunk-FMBD7UC4-6wFLOVcJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-vT8z8D-0.js → chunk-OYMX7WX6-Bnr8RiBf.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-CTlwwg-R.js → chunk-QZHKN3VN-Ct82MksJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-C44yr620.js → chunk-YZCP3GAM-BXmN1diQ.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +1 -0
- package/dist/deck-client/assets/clone-DiIRH1pI.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-DBB2J2nL.js → cose-bilkent-S5V4N54A-CmQCT-mH.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-DxDTYbKl.js → dagre-KV5264BT-DDdSa9EX.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-DByWrWd1.js → diagram-5BDNPKRD-Bccks2xJ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-B8B6ddMq.js → diagram-G4DWMVQ6-CPPNgxmQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-BMUZ2PWK.js → diagram-MMDJMWI5-KrD300pS.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-Bk9e8BB-.js → diagram-TYMM5635-DefnLuQf.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DcOSwSol.js → erDiagram-SMLLAGMA-DI9FfnFP.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-DI-4BR0F.js → flowDiagram-DWJPFMVM-twKyd3Fx.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-BeZuXBoU.js → ganttDiagram-T4ZO3ILL-Wau3jhBr.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-Bcki__f-.js → gitGraphDiagram-UUTBAWPF-D9GgYXwb.js} +1 -1
- package/dist/deck-client/assets/{graph-CifKx6G1.js → graph-BhNLzyXS.js} +1 -1
- package/dist/deck-client/assets/index-B-YQq5b5.css +1 -0
- package/dist/deck-client/assets/{index-CB-qlwRT.js → index-BtQBaQ7s.js} +76 -76
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-CReN1nFN.js → infoDiagram-42DDH7IO-TylGlSG-.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-CDF_VLN_.js → ishikawaDiagram-UXIWVN3A-DAT8icpg.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DwgGrNVB.js → journeyDiagram-VCZTEJTY-D3v_XL72.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DB_zohh5.js → kanban-definition-6JOO6SKY-DNUOBiNr.js} +1 -1
- package/dist/deck-client/assets/{layout-DFfX1O3z.js → layout-COfodgwF.js} +1 -1
- package/dist/deck-client/assets/{linear-CtKb4EXj.js → linear-DmTsuIvK.js} +1 -1
- package/dist/deck-client/assets/{min-DCRRwUZv.js → min-BW1F7i1D.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-D0QBOiFe.js → mindmap-definition-QFDTVHPH-CErFzKWl.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-CD-EV5WB.js → pieDiagram-DEJITSTG-DW5F757o.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B-JXZ8xI.js → quadrantDiagram-34T5L4WZ-B1S2-TfI.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-D2_OK5Dp.js → requirementDiagram-MS252O5E-BY5BAR-5.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BbBJqVSC.js → sankeyDiagram-XADWPNL6-CE1Cp9HS.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-Db8A-Rkk.js → sequenceDiagram-FGHM5R23-IaHnbKye.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-DGJnanjS.js → stateDiagram-FHFEXIEX-CwPJm9hU.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-BRkr6T4w.js → timeline-definition-GMOUNBTQ-DVFGGSgN.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-d0rsTqFo.js → vennDiagram-DHZGUBPP-C1194MJi.js} +1 -1
- package/dist/deck-client/assets/{wardley-RL74JXVD-2t7cMqdS.js → wardley-RL74JXVD-CHZiUbBa.js} +1 -1
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-DzboAsHh.js → wardleyDiagram-NUSXRM2D-hpwdFfGj.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CgTP9u2V.js → xychartDiagram-5P7HB3ND-DYkotwy8.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/cli.js +91 -13
- package/dist/server/council-entry.js +0 -0
- package/dist/server/deck-mcp-entry.js +83 -40
- package/dist/server/deck-serve.js +54 -20
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +952 -290
- package/dist/server/radar-docker-init-entry.js +239 -0
- package/dist/server/radar-entrypoint-entry.js +99 -0
- package/dist/server/radar-teardown-entry.js +477 -0
- package/dist/server/recall-entry.js +4 -1
- package/package.json +22 -23
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
- package/scaffolds/ls-marketplace/plugins/kit/skills/diagram/SKILL.md +27 -5
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +274 -0
- 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 +82 -2
- package/scaffolds/statusline/statusline-wrapper.sh +8 -1
- package/dist/chart-client/assets/index-CDIhdgWg.css +0 -1
- package/dist/client/assets/index-CfW4n40I.css +0 -32
- package/dist/council-client/assets/index-CZim6x1u.css +0 -1
- package/dist/deck-client/assets/channel-DmR7Tyyt.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bl4ozQWs.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bl4ozQWs.js +0 -1
- package/dist/deck-client/assets/clone-BAy58j24.js +0 -1
- package/dist/deck-client/assets/index-BlTlhxFW.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CR7riiab.js +0 -1
|
@@ -30,6 +30,217 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
32
|
|
|
33
|
+
// src/server/cred-shape.ts
|
|
34
|
+
function inferCourseName(serverUrl) {
|
|
35
|
+
try {
|
|
36
|
+
const host = new URL(serverUrl).hostname.toLowerCase();
|
|
37
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
|
|
38
|
+
if (host.includes("staging")) return "staging";
|
|
39
|
+
if (host.endsWith(".vercel.app")) return "prod";
|
|
40
|
+
return host.split(".")[0] || "default";
|
|
41
|
+
} catch {
|
|
42
|
+
return "default";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function toNested(cred) {
|
|
46
|
+
if (cred.profiles && cred.active && cred.profiles[cred.active]) {
|
|
47
|
+
return { active: cred.active, profiles: cred.profiles };
|
|
48
|
+
}
|
|
49
|
+
if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const name = inferCourseName(cred.serverUrl);
|
|
53
|
+
return {
|
|
54
|
+
active: name,
|
|
55
|
+
profiles: {
|
|
56
|
+
[name]: {
|
|
57
|
+
pat: cred.pat,
|
|
58
|
+
orgSlug: cred.orgSlug,
|
|
59
|
+
projectSlug: cred.projectSlug,
|
|
60
|
+
serverUrl: cred.serverUrl
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function upsertProfile(existing, name, profile) {
|
|
66
|
+
const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
|
|
67
|
+
return {
|
|
68
|
+
active: name,
|
|
69
|
+
profiles: { ...base.profiles, [name]: profile }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function readCredFile(repoRoot) {
|
|
73
|
+
const p = path.join(repoRoot, CONFIG_FILENAME);
|
|
74
|
+
if (!fs.existsSync(p)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function writeJsonAtomic(absPath, value, mode) {
|
|
82
|
+
const tmp = `${absPath}.tmp`;
|
|
83
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
84
|
+
if (mode !== void 0) {
|
|
85
|
+
try {
|
|
86
|
+
fs.chmodSync(tmp, mode);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
fs.renameSync(tmp, absPath);
|
|
91
|
+
}
|
|
92
|
+
var fs, path, CONFIG_FILENAME;
|
|
93
|
+
var init_cred_shape = __esm({
|
|
94
|
+
"src/server/cred-shape.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
fs = __toESM(require("node:fs"));
|
|
97
|
+
path = __toESM(require("node:path"));
|
|
98
|
+
CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// src/server/secrets-pull.ts
|
|
103
|
+
var secrets_pull_exports = {};
|
|
104
|
+
__export(secrets_pull_exports, {
|
|
105
|
+
runSecretsPull: () => runSecretsPull
|
|
106
|
+
});
|
|
107
|
+
async function runSecretsPull(opts) {
|
|
108
|
+
const cred = readCredFile(opts.targetDir);
|
|
109
|
+
if (!cred) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`No ${CONFIG_FILENAME} found in ${opts.targetDir}. Run \`launch-kit init\` first.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const nested = toNested(cred);
|
|
115
|
+
if (!nested) {
|
|
116
|
+
throw new Error(`${CONFIG_FILENAME} is missing required fields (pat, orgSlug, projectSlug, serverUrl).`);
|
|
117
|
+
}
|
|
118
|
+
const profile = nested.profiles[nested.active];
|
|
119
|
+
if (!profile) {
|
|
120
|
+
throw new Error(`Active profile "${nested.active}" not found in ${CONFIG_FILENAME}.`);
|
|
121
|
+
}
|
|
122
|
+
const envsRaw = await callMcp(profile, "environments_list", {});
|
|
123
|
+
const envsResult = Array.isArray(envsRaw) ? { environments: envsRaw, defaultPullEnvSlug: null } : envsRaw;
|
|
124
|
+
const envSlug = resolveEnvSlug(opts.envOverride, envsResult);
|
|
125
|
+
if (!envSlug) {
|
|
126
|
+
const list = envsResult.environments.map((e) => e.slug).join(", ") || "(none)";
|
|
127
|
+
throw new Error(
|
|
128
|
+
`No env specified and no default set. Available: ${list}. Pass --env=<slug>, set $LS_ENV, or set a project-level default at ${profile.serverUrl}/${profile.orgSlug}/projects/${profile.projectSlug}/settings.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const pullResult = await callMcp(profile, "secrets_pull_env", { env_slug: envSlug });
|
|
132
|
+
const filePath = path3.join(opts.targetDir, opts.fileName);
|
|
133
|
+
writeEnvFile(filePath, pullResult.vars);
|
|
134
|
+
console.log(
|
|
135
|
+
`\u2714 wrote ${pullResult.vars.length} secrets from "${pullResult.env.slug}" \u2192 ${path3.relative(opts.targetDir, filePath) || opts.fileName}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
function resolveEnvSlug(override, envs) {
|
|
139
|
+
if (override) return override;
|
|
140
|
+
if (process.env.LS_ENV) return process.env.LS_ENV;
|
|
141
|
+
if (envs.defaultPullEnvSlug) return envs.defaultPullEnvSlug;
|
|
142
|
+
if (envs.environments.length === 1) return envs.environments[0].slug;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function writeEnvFile(filePath, vars) {
|
|
146
|
+
const lines = vars.map((v) => `${v.key}=${formatValue(v.value)}`);
|
|
147
|
+
fs3.writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
|
|
148
|
+
try {
|
|
149
|
+
fs3.chmodSync(filePath, 384);
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function formatValue(v) {
|
|
154
|
+
if (v === "") return "";
|
|
155
|
+
if (/[\s"'\\$`#=]/.test(v)) {
|
|
156
|
+
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
157
|
+
}
|
|
158
|
+
return v;
|
|
159
|
+
}
|
|
160
|
+
async function callMcp(profile, toolName, args) {
|
|
161
|
+
return new Promise((resolve3, reject) => {
|
|
162
|
+
const mcpUrl = new import_node_url.URL("/api/mcp/project", profile.serverUrl);
|
|
163
|
+
const body = JSON.stringify({
|
|
164
|
+
jsonrpc: "2.0",
|
|
165
|
+
id: 1,
|
|
166
|
+
method: "tools/call",
|
|
167
|
+
params: { name: toolName, arguments: args }
|
|
168
|
+
});
|
|
169
|
+
const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
170
|
+
const req = requester(
|
|
171
|
+
{
|
|
172
|
+
host: mcpUrl.hostname,
|
|
173
|
+
port: mcpUrl.port || (mcpUrl.protocol === "https:" ? 443 : 80),
|
|
174
|
+
path: mcpUrl.pathname,
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"Accept": "application/json, text/event-stream",
|
|
179
|
+
"Content-Length": String(Buffer.byteLength(body)),
|
|
180
|
+
"Authorization": `Bearer ${profile.pat}`,
|
|
181
|
+
"X-Org-Slug": profile.orgSlug,
|
|
182
|
+
"X-Project-Slug": profile.projectSlug
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
(res) => {
|
|
186
|
+
const chunks = [];
|
|
187
|
+
res.on("data", (c2) => chunks.push(c2));
|
|
188
|
+
res.on("end", () => {
|
|
189
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
190
|
+
if (res.statusCode === 401) {
|
|
191
|
+
return reject(new Error(`PAT rejected (401). Regenerate at ${profile.serverUrl}/settings/tokens.`));
|
|
192
|
+
}
|
|
193
|
+
if (res.statusCode === 403) {
|
|
194
|
+
return reject(new Error(`Access denied (403). PAT lacks the scope required for ${toolName}.`));
|
|
195
|
+
}
|
|
196
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
197
|
+
return reject(new Error(`LaunchSecure ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
198
|
+
}
|
|
199
|
+
let json = text;
|
|
200
|
+
if (text.startsWith("event:") || text.includes("\ndata: ")) {
|
|
201
|
+
for (const line of text.split("\n")) {
|
|
202
|
+
if (line.startsWith("data: ")) {
|
|
203
|
+
json = line.slice(6);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(json);
|
|
210
|
+
if (parsed.error) return reject(new Error(`${toolName}: ${parsed.error.message ?? "unknown"}`));
|
|
211
|
+
const inner = parsed.result?.content?.[0]?.text;
|
|
212
|
+
if (!inner) return reject(new Error(`${toolName} returned no content`));
|
|
213
|
+
if (inner.startsWith("\u2500\u2500 Error")) {
|
|
214
|
+
const firstLine = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("\u2500\u2500"))[0];
|
|
215
|
+
return reject(new Error(`${toolName}: ${firstLine ?? inner}`));
|
|
216
|
+
}
|
|
217
|
+
resolve3(JSON.parse(inner));
|
|
218
|
+
} catch (err) {
|
|
219
|
+
reject(new Error(`Could not parse ${toolName} response: ${err instanceof Error ? err.message : String(err)}`));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
req.setTimeout(TIMEOUT_MS, () => req.destroy(new Error(`${toolName} timed out after ${TIMEOUT_MS / 1e3}s`)));
|
|
225
|
+
req.on("error", reject);
|
|
226
|
+
req.write(body);
|
|
227
|
+
req.end();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
var fs3, path3, import_node_http, import_node_https, import_node_url, TIMEOUT_MS;
|
|
231
|
+
var init_secrets_pull = __esm({
|
|
232
|
+
"src/server/secrets-pull.ts"() {
|
|
233
|
+
"use strict";
|
|
234
|
+
fs3 = __toESM(require("node:fs"));
|
|
235
|
+
path3 = __toESM(require("node:path"));
|
|
236
|
+
import_node_http = require("node:http");
|
|
237
|
+
import_node_https = require("node:https");
|
|
238
|
+
import_node_url = require("node:url");
|
|
239
|
+
init_cred_shape();
|
|
240
|
+
TIMEOUT_MS = 15e3;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
33
244
|
// src/server/statusline-install.ts
|
|
34
245
|
var statusline_install_exports = {};
|
|
35
246
|
__export(statusline_install_exports, {
|
|
@@ -37,25 +248,25 @@ __export(statusline_install_exports, {
|
|
|
37
248
|
deactivateStatusline: () => deactivateStatusline
|
|
38
249
|
});
|
|
39
250
|
function readSettings() {
|
|
40
|
-
if (!
|
|
251
|
+
if (!fs4.existsSync(SETTINGS_PATH)) return null;
|
|
41
252
|
try {
|
|
42
|
-
return JSON.parse(
|
|
253
|
+
return JSON.parse(fs4.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
43
254
|
} catch {
|
|
44
255
|
return null;
|
|
45
256
|
}
|
|
46
257
|
}
|
|
47
258
|
function writeSettings(s) {
|
|
48
|
-
|
|
49
|
-
|
|
259
|
+
fs4.mkdirSync(path4.dirname(SETTINGS_PATH), { recursive: true });
|
|
260
|
+
fs4.writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
50
261
|
}
|
|
51
262
|
function readScaffold(name) {
|
|
52
|
-
const p =
|
|
53
|
-
return
|
|
263
|
+
const p = path4.resolve(__dirname, "..", "..", "scaffolds", "statusline", name);
|
|
264
|
+
return fs4.readFileSync(p, "utf-8");
|
|
54
265
|
}
|
|
55
266
|
function writeScripts() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
267
|
+
fs4.mkdirSync(LK_DIR, { recursive: true });
|
|
268
|
+
fs4.writeFileSync(WRAPPER_PATH, readScaffold("statusline-wrapper.sh"), { mode: 493 });
|
|
269
|
+
fs4.writeFileSync(CHIP_PATH, readScaffold("statusline-mcp.sh"), { mode: 493 });
|
|
59
270
|
}
|
|
60
271
|
function wrapperCommand(opts) {
|
|
61
272
|
const env = [];
|
|
@@ -115,103 +326,313 @@ function deactivateStatusline() {
|
|
|
115
326
|
writeSettings(restored);
|
|
116
327
|
for (const p of [WRAPPER_PATH, CHIP_PATH]) {
|
|
117
328
|
try {
|
|
118
|
-
|
|
329
|
+
fs4.unlinkSync(p);
|
|
119
330
|
} catch {
|
|
120
331
|
}
|
|
121
332
|
}
|
|
122
333
|
return { ok: true, outcome: "deactivated", message: "restored original statusLine.command" };
|
|
123
334
|
}
|
|
124
|
-
var
|
|
335
|
+
var fs4, path4, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
|
|
125
336
|
var init_statusline_install = __esm({
|
|
126
337
|
"src/server/statusline-install.ts"() {
|
|
127
338
|
"use strict";
|
|
128
|
-
|
|
129
|
-
|
|
339
|
+
fs4 = __toESM(require("node:fs"));
|
|
340
|
+
path4 = __toESM(require("node:path"));
|
|
130
341
|
import_node_os = require("node:os");
|
|
131
|
-
LK_DIR =
|
|
132
|
-
WRAPPER_PATH =
|
|
133
|
-
CHIP_PATH =
|
|
134
|
-
SETTINGS_PATH =
|
|
342
|
+
LK_DIR = path4.join((0, import_node_os.homedir)(), ".launchsecure");
|
|
343
|
+
WRAPPER_PATH = path4.join(LK_DIR, "statusline-wrapper.sh");
|
|
344
|
+
CHIP_PATH = path4.join(LK_DIR, "statusline-mcp.sh");
|
|
345
|
+
SETTINGS_PATH = path4.join((0, import_node_os.homedir)(), ".claude", "settings.json");
|
|
135
346
|
ORIGINAL_KEY = "_launchKitStatuslineOriginal";
|
|
136
347
|
}
|
|
137
348
|
});
|
|
138
349
|
|
|
139
|
-
// src/server/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
var CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
153
|
-
function inferCourseName(serverUrl) {
|
|
350
|
+
// src/server/radar/mcp.ts
|
|
351
|
+
function parseBody(text) {
|
|
352
|
+
if (!text) return {};
|
|
353
|
+
if (text.startsWith("event:") || text.includes("\ndata: ")) {
|
|
354
|
+
for (const line of text.split("\n")) {
|
|
355
|
+
if (line.startsWith("data: ")) {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(line.slice(6));
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
154
363
|
try {
|
|
155
|
-
|
|
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";
|
|
364
|
+
return JSON.parse(text);
|
|
160
365
|
} catch {
|
|
161
|
-
return
|
|
366
|
+
return {};
|
|
162
367
|
}
|
|
163
368
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
369
|
+
var import_node_https2, import_node_http2, ProjectMcpClient;
|
|
370
|
+
var init_mcp = __esm({
|
|
371
|
+
"src/server/radar/mcp.ts"() {
|
|
372
|
+
"use strict";
|
|
373
|
+
import_node_https2 = require("node:https");
|
|
374
|
+
import_node_http2 = require("node:http");
|
|
375
|
+
ProjectMcpClient = class {
|
|
376
|
+
constructor(opts) {
|
|
377
|
+
this.initialized = false;
|
|
378
|
+
this.callId = 1;
|
|
379
|
+
this.mcpUrl = new URL("/api/mcp/project", opts.serverUrl);
|
|
380
|
+
this.headers = {
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
"Accept": "application/json, text/event-stream",
|
|
383
|
+
"Authorization": `Bearer ${opts.pat}`
|
|
384
|
+
};
|
|
385
|
+
if (opts.orgSlug) this.headers["X-Org-Slug"] = opts.orgSlug;
|
|
386
|
+
if (opts.projectSlug) this.headers["X-Project-Slug"] = opts.projectSlug;
|
|
180
387
|
}
|
|
181
|
-
|
|
182
|
-
|
|
388
|
+
async call(toolName, args) {
|
|
389
|
+
await this.ensureInitialized();
|
|
390
|
+
const body = JSON.stringify({
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
id: this.callId++,
|
|
393
|
+
method: "tools/call",
|
|
394
|
+
params: { name: toolName, arguments: args }
|
|
395
|
+
});
|
|
396
|
+
const resp = await this.send(body);
|
|
397
|
+
const parsed = parseBody(resp.body);
|
|
398
|
+
if (parsed.error) {
|
|
399
|
+
throw new Error(`MCP ${toolName} failed: ${parsed.error.message ?? JSON.stringify(parsed.error)}`);
|
|
400
|
+
}
|
|
401
|
+
const text = parsed.result?.content?.[0]?.text;
|
|
402
|
+
if (!text) return parsed.result;
|
|
403
|
+
if (text.startsWith("\u2500\u2500 Error")) {
|
|
404
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
405
|
+
const reason = lines.find((l) => !l.startsWith("\u2500\u2500")) ?? text;
|
|
406
|
+
throw new Error(`MCP ${toolName} failed: ${reason}`);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(text);
|
|
410
|
+
} catch {
|
|
411
|
+
return text;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async ensureInitialized() {
|
|
415
|
+
if (this.initialized) return;
|
|
416
|
+
const initBody = JSON.stringify({
|
|
417
|
+
jsonrpc: "2.0",
|
|
418
|
+
id: this.callId++,
|
|
419
|
+
method: "initialize",
|
|
420
|
+
params: {
|
|
421
|
+
protocolVersion: "2025-03-26",
|
|
422
|
+
capabilities: {},
|
|
423
|
+
clientInfo: { name: "launchpod-feedback-agent", version: "0.0.1" }
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const initResp = await this.send(initBody);
|
|
427
|
+
if (initResp.sessionId) this.sessionId = initResp.sessionId;
|
|
428
|
+
const notifBody = JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
429
|
+
await this.send(notifBody);
|
|
430
|
+
this.initialized = true;
|
|
431
|
+
}
|
|
432
|
+
send(body) {
|
|
433
|
+
return new Promise((resolve3, reject) => {
|
|
434
|
+
const headers = {
|
|
435
|
+
...this.headers,
|
|
436
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
437
|
+
};
|
|
438
|
+
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
439
|
+
const requester = this.mcpUrl.protocol === "https:" ? import_node_https2.request : import_node_http2.request;
|
|
440
|
+
const req = requester(
|
|
441
|
+
{
|
|
442
|
+
host: this.mcpUrl.hostname,
|
|
443
|
+
port: this.mcpUrl.port || (this.mcpUrl.protocol === "https:" ? 443 : 80),
|
|
444
|
+
path: this.mcpUrl.pathname + this.mcpUrl.search,
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers
|
|
447
|
+
},
|
|
448
|
+
(res) => {
|
|
449
|
+
const chunks = [];
|
|
450
|
+
res.on("data", (c2) => chunks.push(c2));
|
|
451
|
+
res.on("end", () => {
|
|
452
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
453
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
454
|
+
reject(new Error(`MCP HTTP ${res.statusCode}: ${text}`));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const sid = res.headers["mcp-session-id"];
|
|
458
|
+
resolve3({ body: text, sessionId: typeof sid === "string" ? sid : void 0 });
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
req.on("error", reject);
|
|
463
|
+
req.write(body);
|
|
464
|
+
req.end();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/server/radar-docker-init-entry.ts
|
|
472
|
+
var radar_docker_init_entry_exports = {};
|
|
473
|
+
function fail(message) {
|
|
474
|
+
console.error(message);
|
|
475
|
+
process.exit(1);
|
|
183
476
|
}
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
profiles: { ...base.profiles, [name]: profile }
|
|
189
|
-
};
|
|
477
|
+
function requireEnv(name) {
|
|
478
|
+
const v = process.env[name];
|
|
479
|
+
if (!v) fail(`ERROR: ${name} is required but not set`);
|
|
480
|
+
return v;
|
|
190
481
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
482
|
+
function run2(cmd, args, stdio = "inherit") {
|
|
483
|
+
const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
|
|
484
|
+
return r.status ?? 1;
|
|
485
|
+
}
|
|
486
|
+
async function setupFromCloud() {
|
|
487
|
+
const pat = requireEnv("LS_PAT");
|
|
488
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
489
|
+
const orgSlug = process.env.LS_ORG_SLUG;
|
|
490
|
+
const projectSlug = process.env.LS_PROJECT_SLUG;
|
|
491
|
+
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
492
|
+
let bundle;
|
|
194
493
|
try {
|
|
195
|
-
|
|
494
|
+
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
196
495
|
} catch (err) {
|
|
197
|
-
|
|
496
|
+
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and is scoped to the right org/project.`);
|
|
198
497
|
}
|
|
498
|
+
if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
|
|
499
|
+
if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
|
|
500
|
+
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
501
|
+
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
502
|
+
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
503
|
+
if (!process.env.GH_TOKEN) {
|
|
504
|
+
fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
|
|
505
|
+
}
|
|
506
|
+
console.log(`[entrypoint] bundle from cloud: org=${process.env.LS_ORG_SLUG} project=${process.env.LS_PROJECT_SLUG} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()}`);
|
|
199
507
|
}
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
508
|
+
function setupClaudeCredentials() {
|
|
509
|
+
const home = process.env.HOME ?? "/home/launchpod";
|
|
510
|
+
const claudeDir = (0, import_node_path.join)(home, ".claude");
|
|
511
|
+
(0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
|
|
512
|
+
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
513
|
+
const credsPath = (0, import_node_path.join)(claudeDir, ".credentials.json");
|
|
514
|
+
(0, import_node_fs.writeFileSync)(credsPath, decoded);
|
|
515
|
+
(0, import_node_fs.chmodSync)(credsPath, 384);
|
|
516
|
+
const configPath = (0, import_node_path.join)(home, ".claude.json");
|
|
517
|
+
let cfg = {};
|
|
518
|
+
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
204
519
|
try {
|
|
205
|
-
|
|
520
|
+
cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
|
|
206
521
|
} catch {
|
|
522
|
+
cfg = {};
|
|
207
523
|
}
|
|
208
524
|
}
|
|
209
|
-
|
|
525
|
+
cfg.hasCompletedOnboarding = true;
|
|
526
|
+
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
527
|
+
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
528
|
+
cfg.installMethod = cfg.installMethod ?? "global";
|
|
529
|
+
(0, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
530
|
+
(0, import_node_fs.chmodSync)(configPath, 384);
|
|
531
|
+
}
|
|
532
|
+
function setupGitAndGh() {
|
|
533
|
+
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
534
|
+
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
535
|
+
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
536
|
+
if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
537
|
+
}
|
|
538
|
+
function initWorkspaceIfEmpty() {
|
|
539
|
+
process.chdir("/workspace");
|
|
540
|
+
if ((0, import_node_fs.existsSync)(".git")) {
|
|
541
|
+
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
console.log("[entrypoint] /workspace is empty \u2014 running launch-kit init");
|
|
545
|
+
const status = run2("launch-kit", [
|
|
546
|
+
"init",
|
|
547
|
+
`--token=${requireEnv("LS_PAT")}`,
|
|
548
|
+
`--org=${requireEnv("LS_ORG_SLUG")}`,
|
|
549
|
+
`--project=${requireEnv("LS_PROJECT_SLUG")}`,
|
|
550
|
+
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
551
|
+
`--dir=/workspace`
|
|
552
|
+
]);
|
|
553
|
+
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
554
|
+
}
|
|
555
|
+
function execLaunchPodRadar() {
|
|
556
|
+
console.log("[entrypoint] starting launch-pod radar");
|
|
557
|
+
const child = (0, import_node_child_process2.spawn)("launch-pod", ["radar"], { stdio: "inherit" });
|
|
558
|
+
const forward = (sig) => () => {
|
|
559
|
+
try {
|
|
560
|
+
child.kill(sig);
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
process.on("SIGTERM", forward("SIGTERM"));
|
|
565
|
+
process.on("SIGINT", forward("SIGINT"));
|
|
566
|
+
process.on("SIGHUP", forward("SIGHUP"));
|
|
567
|
+
child.on("exit", (code, signal) => {
|
|
568
|
+
if (signal) process.kill(process.pid, signal);
|
|
569
|
+
else process.exit(code ?? 0);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function main() {
|
|
573
|
+
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
574
|
+
await setupFromCloud();
|
|
575
|
+
setupClaudeCredentials();
|
|
576
|
+
setupGitAndGh();
|
|
577
|
+
initWorkspaceIfEmpty();
|
|
578
|
+
execLaunchPodRadar();
|
|
579
|
+
}
|
|
580
|
+
var import_node_child_process2, import_node_fs, import_node_path, REQUIRED_ENV;
|
|
581
|
+
var init_radar_docker_init_entry = __esm({
|
|
582
|
+
"src/server/radar-docker-init-entry.ts"() {
|
|
583
|
+
"use strict";
|
|
584
|
+
import_node_child_process2 = require("node:child_process");
|
|
585
|
+
import_node_fs = require("node:fs");
|
|
586
|
+
import_node_path = require("node:path");
|
|
587
|
+
init_mcp();
|
|
588
|
+
REQUIRED_ENV = [
|
|
589
|
+
"CLAUDE_CREDENTIALS_B64",
|
|
590
|
+
"LS_PAT"
|
|
591
|
+
];
|
|
592
|
+
main().catch((err) => {
|
|
593
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// src/server/init-entry.ts
|
|
600
|
+
var import_node_child_process3 = require("node:child_process");
|
|
601
|
+
var crypto = __toESM(require("node:crypto"));
|
|
602
|
+
var fs5 = __toESM(require("node:fs"));
|
|
603
|
+
var import_node_http3 = require("node:http");
|
|
604
|
+
var import_node_https3 = require("node:https");
|
|
605
|
+
var path5 = __toESM(require("node:path"));
|
|
606
|
+
var readline = __toESM(require("node:readline"));
|
|
607
|
+
var import_node_url2 = require("node:url");
|
|
608
|
+
init_cred_shape();
|
|
609
|
+
|
|
610
|
+
// src/server/git-bot-config.ts
|
|
611
|
+
var import_node_child_process = require("node:child_process");
|
|
612
|
+
function run(cmd, args, stdio = "inherit") {
|
|
613
|
+
return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
|
|
614
|
+
}
|
|
615
|
+
function configureGitForBot(identity) {
|
|
616
|
+
if (process.env.GH_TOKEN) {
|
|
617
|
+
run("gh", ["auth", "setup-git"]);
|
|
618
|
+
}
|
|
619
|
+
run("git", ["config", "--global", "user.name", identity.name]);
|
|
620
|
+
run("git", ["config", "--global", "user.email", identity.email]);
|
|
621
|
+
run("git", ["config", "--global", "init.defaultBranch", "main"]);
|
|
622
|
+
run("git", ["config", "--global", "pull.rebase", "false"]);
|
|
623
|
+
}
|
|
624
|
+
function parseGitIdentityFlag(value, flagName = "--git-identity") {
|
|
625
|
+
const m = value.match(/^\s*(.+?)\s*<\s*([^>]+?)\s*>\s*$/);
|
|
626
|
+
if (!m) {
|
|
627
|
+
throw new Error(`${flagName} must be in the form "Name <email>" (got: ${value})`);
|
|
628
|
+
}
|
|
629
|
+
return { name: m[1], email: m[2] };
|
|
210
630
|
}
|
|
211
631
|
|
|
212
632
|
// src/server/cred-recovery.ts
|
|
213
633
|
var fs2 = __toESM(require("node:fs"));
|
|
214
634
|
var path2 = __toESM(require("node:path"));
|
|
635
|
+
init_cred_shape();
|
|
215
636
|
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
216
637
|
function migrateLegacyCredFile(targetDir, opts) {
|
|
217
638
|
const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
@@ -292,9 +713,54 @@ function recoverCred(targetDir, opts) {
|
|
|
292
713
|
}
|
|
293
714
|
|
|
294
715
|
// src/server/init-entry.ts
|
|
716
|
+
init_secrets_pull();
|
|
295
717
|
init_statusline_install();
|
|
296
718
|
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
297
719
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
720
|
+
var TTY = process.stdout.isTTY === true && process.env.NO_COLOR !== "1";
|
|
721
|
+
var c = {
|
|
722
|
+
dim: (s) => TTY ? `\x1B[2m${s}\x1B[0m` : s,
|
|
723
|
+
bold: (s) => TTY ? `\x1B[1m${s}\x1B[0m` : s,
|
|
724
|
+
cyan: (s) => TTY ? `\x1B[36m${s}\x1B[0m` : s,
|
|
725
|
+
green: (s) => TTY ? `\x1B[32m${s}\x1B[0m` : s,
|
|
726
|
+
yellow: (s) => TTY ? `\x1B[33m${s}\x1B[0m` : s,
|
|
727
|
+
red: (s) => TTY ? `\x1B[31m${s}\x1B[0m` : s
|
|
728
|
+
};
|
|
729
|
+
var PHASE_NAME_COL = 16;
|
|
730
|
+
var HEADER_WIDTH = 64;
|
|
731
|
+
function header(title, kv) {
|
|
732
|
+
if (VERBOSE) return;
|
|
733
|
+
const dashes = "\u2500".repeat(Math.max(3, HEADER_WIDTH - title.length - 5));
|
|
734
|
+
console.log("");
|
|
735
|
+
console.log(c.cyan(`\u2500\u2500\u2500 ${c.bold(title)} ${dashes}`));
|
|
736
|
+
if (kv.length > 0) {
|
|
737
|
+
const labelWidth = Math.max(...kv.map(([k]) => k.length));
|
|
738
|
+
for (const [k, v] of kv) console.log(` ${c.dim(k.padEnd(labelWidth))} ${v}`);
|
|
739
|
+
}
|
|
740
|
+
console.log("");
|
|
741
|
+
}
|
|
742
|
+
function phase(name, result) {
|
|
743
|
+
if (VERBOSE) return;
|
|
744
|
+
const mark = result.status === "ok" ? c.green("\u2713") : result.status === "in-sync" ? c.dim("\xB7") : result.status === "skipped" ? c.dim("\u2212") : c.yellow("\u26A0");
|
|
745
|
+
const summary = result.status === "in-sync" || result.status === "skipped" ? c.dim(result.summary) : result.summary;
|
|
746
|
+
const label = name.padEnd(PHASE_NAME_COL);
|
|
747
|
+
console.log(` ${result.status === "warn" ? c.yellow(label) : label} ${mark} ${summary}`);
|
|
748
|
+
}
|
|
749
|
+
function section(title) {
|
|
750
|
+
if (VERBOSE) return;
|
|
751
|
+
console.log("");
|
|
752
|
+
console.log(` ${c.dim("\u2500 " + title + " \u2500")}`);
|
|
753
|
+
}
|
|
754
|
+
function footer(msg, hints = []) {
|
|
755
|
+
if (VERBOSE) return;
|
|
756
|
+
console.log("");
|
|
757
|
+
console.log(` ${c.bold(msg)}`);
|
|
758
|
+
for (const h of hints) console.log(` ${c.dim(h)}`);
|
|
759
|
+
console.log("");
|
|
760
|
+
}
|
|
761
|
+
function warn(msg) {
|
|
762
|
+
console.log(` ${c.yellow("\u26A0")} ${msg}`);
|
|
763
|
+
}
|
|
298
764
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
299
765
|
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
300
766
|
Wired in Claude Code (.mcp.json):
|
|
@@ -319,12 +785,12 @@ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
|
|
|
319
785
|
`;
|
|
320
786
|
function listEntries(dir, kind) {
|
|
321
787
|
if (kind === "commands") {
|
|
322
|
-
return
|
|
788
|
+
return fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path5.join(dir, f) }));
|
|
323
789
|
}
|
|
324
|
-
return
|
|
790
|
+
return fs5.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs5.existsSync(path5.join(dir, e.name, "SKILL.md"))).map((e) => e.name).sort().map((name) => ({ name, file: path5.join(dir, name, "SKILL.md") }));
|
|
325
791
|
}
|
|
326
792
|
function renderEntries(dir, kind) {
|
|
327
|
-
if (!
|
|
793
|
+
if (!fs5.existsSync(dir)) return `
|
|
328
794
|
LS slash ${kind}: (scaffold dir not bundled \u2014 this is a packaging bug)
|
|
329
795
|
`;
|
|
330
796
|
const entries = listEntries(dir, kind);
|
|
@@ -334,7 +800,7 @@ LS slash ${kind}: (none defined)
|
|
|
334
800
|
const names = entries.map((e) => `/${PLUGIN_ID}:${e.name}`);
|
|
335
801
|
const colWidth = Math.max(26, ...names.map((n) => n.length + 2));
|
|
336
802
|
const lines = entries.map((entry, i) => {
|
|
337
|
-
const text =
|
|
803
|
+
const text = fs5.readFileSync(entry.file, "utf-8");
|
|
338
804
|
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
339
805
|
const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
340
806
|
return ` ${names[i].padEnd(colWidth)} \u2014 ${desc}`;
|
|
@@ -345,8 +811,8 @@ ${lines.join("\n")}
|
|
|
345
811
|
`;
|
|
346
812
|
}
|
|
347
813
|
function renderLsCommandsSection() {
|
|
348
|
-
const base =
|
|
349
|
-
return renderEntries(
|
|
814
|
+
const base = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
|
|
815
|
+
return renderEntries(path5.join(base, "commands"), "commands") + renderEntries(path5.join(base, "skills"), "skills");
|
|
350
816
|
}
|
|
351
817
|
function getLaunchKitToolsGuide() {
|
|
352
818
|
return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
|
|
@@ -369,9 +835,12 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
369
835
|
"--refresh-scaffolds",
|
|
370
836
|
"--quiet",
|
|
371
837
|
"--force",
|
|
372
|
-
"--dry-run"
|
|
838
|
+
"--dry-run",
|
|
839
|
+
"--verbose",
|
|
840
|
+
"--guide",
|
|
841
|
+
"--no-guide"
|
|
373
842
|
]);
|
|
374
|
-
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
|
|
843
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
|
|
375
844
|
function parseArgs(argv) {
|
|
376
845
|
const args = {
|
|
377
846
|
token: process.env.LS_PAT ?? null,
|
|
@@ -379,6 +848,7 @@ function parseArgs(argv) {
|
|
|
379
848
|
projectSlug: null,
|
|
380
849
|
serverUrl: DEFAULT_SERVER_URL,
|
|
381
850
|
targetDir: null,
|
|
851
|
+
gitIdentity: null,
|
|
382
852
|
course: null,
|
|
383
853
|
noInstall: false,
|
|
384
854
|
noOnboard: false,
|
|
@@ -390,6 +860,8 @@ function parseArgs(argv) {
|
|
|
390
860
|
quiet: false,
|
|
391
861
|
force: false,
|
|
392
862
|
dryRun: false,
|
|
863
|
+
verbose: false,
|
|
864
|
+
guide: null,
|
|
393
865
|
help: false
|
|
394
866
|
};
|
|
395
867
|
const unknown = [];
|
|
@@ -440,6 +912,18 @@ function parseArgs(argv) {
|
|
|
440
912
|
args.dryRun = true;
|
|
441
913
|
continue;
|
|
442
914
|
}
|
|
915
|
+
if (raw === "--verbose") {
|
|
916
|
+
args.verbose = true;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (raw === "--guide") {
|
|
920
|
+
args.guide = true;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (raw === "--no-guide") {
|
|
924
|
+
args.guide = false;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
443
927
|
const eq = raw.indexOf("=");
|
|
444
928
|
if (raw.startsWith("--") && eq > 0) {
|
|
445
929
|
const key = raw.slice(2, eq);
|
|
@@ -468,6 +952,14 @@ function parseArgs(argv) {
|
|
|
468
952
|
args.course = val;
|
|
469
953
|
continue;
|
|
470
954
|
}
|
|
955
|
+
if (key === "git-identity") {
|
|
956
|
+
try {
|
|
957
|
+
args.gitIdentity = parseGitIdentityFlag(val);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
960
|
+
}
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
471
963
|
unknown.push(raw);
|
|
472
964
|
continue;
|
|
473
965
|
}
|
|
@@ -480,7 +972,7 @@ function parseArgs(argv) {
|
|
|
480
972
|
if (unknown.length > 0) {
|
|
481
973
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
482
974
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
483
|
-
|
|
975
|
+
fail2(`Unknown argument(s): ${unknown.join(" ")}
|
|
484
976
|
Known boolean flags: ${knownBool}
|
|
485
977
|
Known key=value flags: ${knownKv}`);
|
|
486
978
|
}
|
|
@@ -511,8 +1003,15 @@ Options:
|
|
|
511
1003
|
--refresh-scaffolds Force-overwrite migrate-safety files (default is to
|
|
512
1004
|
preserve user edits). Use this to pull updates
|
|
513
1005
|
published to @launchsecure/launch-kit.
|
|
514
|
-
--
|
|
1006
|
+
--verbose Print the legacy per-line debug log instead of the
|
|
1007
|
+
default compact phase summary. Useful when refresh
|
|
1008
|
+
reports a warn-status phase and you want detail.
|
|
1009
|
+
--guide Print the trailing tools guide (MCP table + every
|
|
1010
|
+
/kit:* command). Off by default on refresh.
|
|
1011
|
+
--quiet Suppress the tools guide and the next-steps hint.
|
|
1012
|
+
Overrides --guide.
|
|
515
1013
|
--dry-run Preview every file write without making changes.
|
|
1014
|
+
Implies --verbose.
|
|
516
1015
|
--help Show this help.
|
|
517
1016
|
|
|
518
1017
|
Tip: prefix the command with @latest (\`launch-kit@latest refresh\`) to force
|
|
@@ -526,6 +1025,12 @@ Subcommands:
|
|
|
526
1025
|
init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
|
|
527
1026
|
refresh Re-apply scaffolds + MCP entries in an already-initialized project
|
|
528
1027
|
(no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
|
|
1028
|
+
setup-git Configure git identity + gh credential helper in one
|
|
1029
|
+
shot. Use in containers / CI where init isn't needed.
|
|
1030
|
+
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
1031
|
+
secrets pull Fetch decrypted secrets from cloud LS and write a .env file.
|
|
1032
|
+
Env resolved from --env \u2192 $LS_ENV \u2192 server-side project default \u2192
|
|
1033
|
+
single-env auto-pick. See \`launch-kit secrets --help\`.
|
|
529
1034
|
statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
|
|
530
1035
|
chips (recall, chart, deck, council) get appended. Refuses to
|
|
531
1036
|
create one if none exists \u2014 additive only.
|
|
@@ -550,6 +1055,12 @@ Options:
|
|
|
550
1055
|
becomes active; re-run with a different --course
|
|
551
1056
|
and --url to add another (e.g. local + staging).
|
|
552
1057
|
Use \`launch-course set <name>\` to switch later.
|
|
1058
|
+
--git-identity="N <e>" Non-interactive git identity for service-account /
|
|
1059
|
+
CI / Docker runs. Configures git user.name, user.email,
|
|
1060
|
+
init.defaultBranch=main, pull.rebase=false; also
|
|
1061
|
+
wires GH_TOKEN into git's credential helper via
|
|
1062
|
+
\`gh auth setup-git\` when GH_TOKEN is set. Example:
|
|
1063
|
+
--git-identity="Radar Bot <radar@launchpod.local>".
|
|
553
1064
|
--no-install Skip dependency install step (also skips the onboard
|
|
554
1065
|
script \u2014 install is its prerequisite).
|
|
555
1066
|
--no-onboard Skip the onboard script even when install runs.
|
|
@@ -570,9 +1081,17 @@ Options:
|
|
|
570
1081
|
user edits; use this to pull updates published to
|
|
571
1082
|
@launchsecure/launch-kit (e.g., a newer
|
|
572
1083
|
migrate-with-backup.sh).
|
|
573
|
-
--
|
|
574
|
-
|
|
575
|
-
|
|
1084
|
+
--verbose Print the legacy per-line debug log instead of the
|
|
1085
|
+
default compact phase summary. Useful when a phase
|
|
1086
|
+
reports a warn status and you want per-file detail.
|
|
1087
|
+
--guide Print the trailing tools guide (MCP table + every
|
|
1088
|
+
/kit:* command). On by default for init; pair with
|
|
1089
|
+
--no-guide to suppress.
|
|
1090
|
+
--no-guide Suppress the trailing tools guide (handy after the
|
|
1091
|
+
first init, when you don't need the catalog again).
|
|
1092
|
+
--quiet Suppress the post-run tools guide AND the next-steps
|
|
1093
|
+
hint. Useful for idempotent re-runs in CI or scripts.
|
|
1094
|
+
Overrides --guide.
|
|
576
1095
|
--force Skip the auto-delegate-to-refresh check. By default
|
|
577
1096
|
init detects an existing bootstrap (cred file +
|
|
578
1097
|
launch-secure MCP entry) and runs refresh instead.
|
|
@@ -619,29 +1138,30 @@ async function prompt(question) {
|
|
|
619
1138
|
resolve3(answer.trim());
|
|
620
1139
|
}));
|
|
621
1140
|
}
|
|
622
|
-
function
|
|
1141
|
+
function fail2(msg) {
|
|
623
1142
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
624
1143
|
process.exit(1);
|
|
625
1144
|
}
|
|
1145
|
+
var VERBOSE = false;
|
|
626
1146
|
function info(msg) {
|
|
627
|
-
console.log(`[launch-kit] ${msg}`);
|
|
1147
|
+
if (VERBOSE) console.log(`[launch-kit] ${msg}`);
|
|
628
1148
|
}
|
|
629
1149
|
function ok(msg) {
|
|
630
|
-
console.log(`[launch-kit] \u2713 ${msg}`);
|
|
1150
|
+
if (VERBOSE) console.log(`[launch-kit] \u2713 ${msg}`);
|
|
631
1151
|
}
|
|
632
1152
|
var DRY_RUN = false;
|
|
633
1153
|
function dryNote(msg) {
|
|
634
1154
|
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
635
1155
|
}
|
|
636
1156
|
function which(bin) {
|
|
637
|
-
const res = (0,
|
|
1157
|
+
const res = (0, import_node_child_process3.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
638
1158
|
if (res.status !== 0) return null;
|
|
639
1159
|
return res.stdout.split(/\r?\n/)[0]?.trim() || null;
|
|
640
1160
|
}
|
|
641
1161
|
function preflight() {
|
|
642
1162
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
643
|
-
if (nodeMajor < 18)
|
|
644
|
-
if (!which("git"))
|
|
1163
|
+
if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
1164
|
+
if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
645
1165
|
const hasGh = which("gh") !== null;
|
|
646
1166
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
647
1167
|
return { hasGh };
|
|
@@ -657,7 +1177,7 @@ var ProjectInfoHttpError = class extends Error {
|
|
|
657
1177
|
};
|
|
658
1178
|
function attemptProjectInfo(args) {
|
|
659
1179
|
return new Promise((resolve3, reject) => {
|
|
660
|
-
const mcpUrl = new
|
|
1180
|
+
const mcpUrl = new import_node_url2.URL("/api/mcp/project", args.serverUrl);
|
|
661
1181
|
const body = JSON.stringify({
|
|
662
1182
|
jsonrpc: "2.0",
|
|
663
1183
|
id: 1,
|
|
@@ -667,7 +1187,7 @@ function attemptProjectInfo(args) {
|
|
|
667
1187
|
arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
|
|
668
1188
|
}
|
|
669
1189
|
});
|
|
670
|
-
const requester = mcpUrl.protocol === "https:" ?
|
|
1190
|
+
const requester = mcpUrl.protocol === "https:" ? import_node_https3.request : import_node_http3.request;
|
|
671
1191
|
const req = requester(
|
|
672
1192
|
{
|
|
673
1193
|
host: mcpUrl.hostname,
|
|
@@ -685,7 +1205,7 @@ function attemptProjectInfo(args) {
|
|
|
685
1205
|
},
|
|
686
1206
|
(res) => {
|
|
687
1207
|
const chunks = [];
|
|
688
|
-
res.on("data", (
|
|
1208
|
+
res.on("data", (c2) => chunks.push(c2));
|
|
689
1209
|
res.on("end", () => {
|
|
690
1210
|
const text = Buffer.concat(chunks).toString("utf-8");
|
|
691
1211
|
if (res.statusCode === 401) {
|
|
@@ -775,7 +1295,7 @@ async function callProjectInfo(args) {
|
|
|
775
1295
|
throw lastErr;
|
|
776
1296
|
}
|
|
777
1297
|
function gitRemoteUrl(dir) {
|
|
778
|
-
const res = (0,
|
|
1298
|
+
const res = (0, import_node_child_process3.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
779
1299
|
if (res.status !== 0) return null;
|
|
780
1300
|
return res.stdout.trim() || null;
|
|
781
1301
|
}
|
|
@@ -784,18 +1304,18 @@ function normalizeRepoUrl(url) {
|
|
|
784
1304
|
const sshMatch = u.match(/^git@([^:]+):(.+)$/);
|
|
785
1305
|
if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
786
1306
|
try {
|
|
787
|
-
const parsed = new
|
|
1307
|
+
const parsed = new import_node_url2.URL(u);
|
|
788
1308
|
return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
|
|
789
1309
|
} catch {
|
|
790
1310
|
return u;
|
|
791
1311
|
}
|
|
792
1312
|
}
|
|
793
1313
|
function isGitRepo(dir) {
|
|
794
|
-
return
|
|
1314
|
+
return fs5.existsSync(path5.join(dir, ".git"));
|
|
795
1315
|
}
|
|
796
1316
|
function dirIsEmpty(dir) {
|
|
797
|
-
if (!
|
|
798
|
-
return
|
|
1317
|
+
if (!fs5.existsSync(dir)) return true;
|
|
1318
|
+
return fs5.readdirSync(dir).length === 0;
|
|
799
1319
|
}
|
|
800
1320
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
801
1321
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -814,20 +1334,20 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
814
1334
|
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
815
1335
|
return;
|
|
816
1336
|
}
|
|
817
|
-
const res = (0,
|
|
1337
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
818
1338
|
if (res.status !== 0) {
|
|
819
|
-
|
|
1339
|
+
fail2(
|
|
820
1340
|
`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.`
|
|
821
1341
|
);
|
|
822
1342
|
}
|
|
823
|
-
if (!
|
|
824
|
-
|
|
1343
|
+
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1344
|
+
fail2(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
825
1345
|
}
|
|
826
1346
|
ok(`cloned to ${targetDir}`);
|
|
827
1347
|
}
|
|
828
1348
|
function writeConfigFile(targetDir, cfg, courseName) {
|
|
829
1349
|
recoverCred(targetDir, getRecoveryOptions());
|
|
830
|
-
const p =
|
|
1350
|
+
const p = path5.join(targetDir, CONFIG_FILENAME);
|
|
831
1351
|
const existing = readCredFile(targetDir);
|
|
832
1352
|
const isNew = existing === null;
|
|
833
1353
|
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
@@ -846,15 +1366,15 @@ function getRecoveryOptions() {
|
|
|
846
1366
|
return { dryRun: DRY_RUN, log: recoveryLog };
|
|
847
1367
|
}
|
|
848
1368
|
function detectExistingBootstrap(targetDir) {
|
|
849
|
-
if (!
|
|
1369
|
+
if (!fs5.existsSync(path5.join(targetDir, CONFIG_FILENAME))) {
|
|
850
1370
|
return { bootstrapped: false };
|
|
851
1371
|
}
|
|
852
|
-
const mcpPath =
|
|
853
|
-
if (!
|
|
1372
|
+
const mcpPath = path5.join(targetDir, ".mcp.json");
|
|
1373
|
+
if (!fs5.existsSync(mcpPath)) {
|
|
854
1374
|
return { bootstrapped: false };
|
|
855
1375
|
}
|
|
856
1376
|
try {
|
|
857
|
-
const mcp = JSON.parse(
|
|
1377
|
+
const mcp = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
858
1378
|
if (mcp.mcpServers?.["launch-secure"]) {
|
|
859
1379
|
return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
|
|
860
1380
|
}
|
|
@@ -872,13 +1392,7 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
872
1392
|
},
|
|
873
1393
|
"launch-chart": {
|
|
874
1394
|
command: "npx",
|
|
875
|
-
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
|
|
876
|
-
// Tells launch-chart to also start its HTTP UI alongside the MCP, so
|
|
877
|
-
// users can open the chart viewer at localhost:<port> while queries
|
|
878
|
-
// hit the MCP. Without this, the MCP runs passively (queries work,
|
|
879
|
-
// no UI). I4 deep-merge preserves user-added env keys; this default
|
|
880
|
-
// ensures the auto-serve UX ships out of the box.
|
|
881
|
-
env: { LAUNCH_CHART_AUTOSERVE: "1" }
|
|
1395
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
|
|
882
1396
|
},
|
|
883
1397
|
"launch-deck": {
|
|
884
1398
|
command: "npx",
|
|
@@ -911,14 +1425,14 @@ function mergeMcpEntry(existing, ours) {
|
|
|
911
1425
|
return merged;
|
|
912
1426
|
}
|
|
913
1427
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
914
|
-
const p =
|
|
915
|
-
const hadExisting =
|
|
1428
|
+
const p = path5.join(targetDir, ".mcp.json");
|
|
1429
|
+
const hadExisting = fs5.existsSync(p);
|
|
916
1430
|
let existing = {};
|
|
917
1431
|
if (hadExisting) {
|
|
918
1432
|
try {
|
|
919
|
-
existing = JSON.parse(
|
|
1433
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
920
1434
|
} catch (err) {
|
|
921
|
-
|
|
1435
|
+
fail2(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
922
1436
|
}
|
|
923
1437
|
}
|
|
924
1438
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -936,19 +1450,21 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
936
1450
|
}
|
|
937
1451
|
}
|
|
938
1452
|
if (DRY_RUN) {
|
|
939
|
-
const
|
|
940
|
-
dryNote(`${
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
ok(`${
|
|
1453
|
+
const action = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
|
|
1454
|
+
dryNote(`${action} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
1455
|
+
return { status: "skipped", summary: "(dry-run)" };
|
|
1456
|
+
}
|
|
1457
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1458
|
+
const verb = hadExisting && existingServerCount > 0 ? "merged" : "wrote";
|
|
1459
|
+
ok(`${verb === "merged" ? "merged into" : "wrote"} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
1460
|
+
const total = Object.keys(launchKitEntries).length;
|
|
1461
|
+
return { status: "ok", summary: `${verb} ${total} entries` };
|
|
946
1462
|
}
|
|
947
1463
|
function detectPackageManager(repoDir) {
|
|
948
|
-
const pkgPath =
|
|
949
|
-
if (!
|
|
1464
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1465
|
+
if (!fs5.existsSync(pkgPath)) return null;
|
|
950
1466
|
try {
|
|
951
|
-
const pkg = JSON.parse(
|
|
1467
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
952
1468
|
if (typeof pkg.packageManager === "string") {
|
|
953
1469
|
const name = pkg.packageManager.split("@")[0];
|
|
954
1470
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -957,7 +1473,7 @@ function detectPackageManager(repoDir) {
|
|
|
957
1473
|
}
|
|
958
1474
|
} catch {
|
|
959
1475
|
}
|
|
960
|
-
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) =>
|
|
1476
|
+
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs5.existsSync(path5.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
|
|
961
1477
|
if (matches.length === 1) {
|
|
962
1478
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
963
1479
|
}
|
|
@@ -966,8 +1482,8 @@ function detectPackageManager(repoDir) {
|
|
|
966
1482
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
967
1483
|
}
|
|
968
1484
|
for (const pm of PACKAGE_MANAGERS) {
|
|
969
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
970
|
-
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) =>
|
|
1485
|
+
if (pm.workspaceFiles?.some((wf) => fs5.existsSync(path5.join(repoDir, wf)))) {
|
|
1486
|
+
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs5.existsSync(path5.join(repoDir, wf)))})` };
|
|
971
1487
|
}
|
|
972
1488
|
}
|
|
973
1489
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -976,8 +1492,8 @@ function detectPackageManager(repoDir) {
|
|
|
976
1492
|
function runInstall(repoDir, detected) {
|
|
977
1493
|
const { pm } = detected;
|
|
978
1494
|
if (!which(pm.binary)) {
|
|
979
|
-
|
|
980
|
-
`${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 ${
|
|
1495
|
+
fail2(
|
|
1496
|
+
`${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 ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
981
1497
|
);
|
|
982
1498
|
}
|
|
983
1499
|
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
@@ -985,24 +1501,24 @@ function runInstall(repoDir, detected) {
|
|
|
985
1501
|
dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
|
|
986
1502
|
return;
|
|
987
1503
|
}
|
|
988
|
-
const res = (0,
|
|
1504
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
989
1505
|
if (res.status !== 0) {
|
|
990
|
-
|
|
1506
|
+
fail2(
|
|
991
1507
|
`${pm.name} install failed (exit ${res.status}).
|
|
992
1508
|
|
|
993
1509
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
994
|
-
- ${
|
|
995
|
-
- ${
|
|
996
|
-
- ${
|
|
1510
|
+
- ${path5.join(repoDir, CONFIG_FILENAME)} (cred file)
|
|
1511
|
+
- ${path5.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
|
|
1512
|
+
- ${path5.join(repoDir, ".gitignore")} (cred line appended)
|
|
997
1513
|
- clone at ${repoDir}
|
|
998
1514
|
|
|
999
1515
|
Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
|
|
1000
1516
|
|
|
1001
1517
|
To retry install only:
|
|
1002
|
-
cd ${
|
|
1518
|
+
cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
|
|
1003
1519
|
|
|
1004
1520
|
To re-run init after fixing the install error:
|
|
1005
|
-
cd ${
|
|
1521
|
+
cd ${path5.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
|
|
1006
1522
|
|
|
1007
1523
|
To fully reset: delete the files listed above and the clone, then re-init.`
|
|
1008
1524
|
);
|
|
@@ -1010,10 +1526,10 @@ To fully reset: delete the files listed above and the clone, then re-init.`
|
|
|
1010
1526
|
ok(`${pm.name} install complete`);
|
|
1011
1527
|
}
|
|
1012
1528
|
function hasOnboardScript(repoDir) {
|
|
1013
|
-
const pkgPath =
|
|
1014
|
-
if (!
|
|
1529
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1530
|
+
if (!fs5.existsSync(pkgPath)) return false;
|
|
1015
1531
|
try {
|
|
1016
|
-
const pkg = JSON.parse(
|
|
1532
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1017
1533
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
1018
1534
|
} catch {
|
|
1019
1535
|
return false;
|
|
@@ -1021,17 +1537,17 @@ function hasOnboardScript(repoDir) {
|
|
|
1021
1537
|
}
|
|
1022
1538
|
function runRecallInit(repoDir) {
|
|
1023
1539
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
1024
|
-
const recallEntry =
|
|
1025
|
-
const useSibling =
|
|
1540
|
+
const recallEntry = path5.resolve(__dirname, "recall-entry.js");
|
|
1541
|
+
const useSibling = fs5.existsSync(recallEntry);
|
|
1026
1542
|
const cmd = useSibling ? process.execPath : "npx";
|
|
1027
1543
|
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
1028
1544
|
if (DRY_RUN) {
|
|
1029
1545
|
dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
|
|
1030
1546
|
return;
|
|
1031
1547
|
}
|
|
1032
|
-
const res = (0,
|
|
1548
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
1033
1549
|
if (res.status !== 0) {
|
|
1034
|
-
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${
|
|
1550
|
+
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path5.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
|
|
1035
1551
|
return;
|
|
1036
1552
|
}
|
|
1037
1553
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
@@ -1042,17 +1558,17 @@ function runOnboard(repoDir, pm) {
|
|
|
1042
1558
|
dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
|
|
1043
1559
|
return;
|
|
1044
1560
|
}
|
|
1045
|
-
const res = (0,
|
|
1561
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
1046
1562
|
if (res.status !== 0) {
|
|
1047
|
-
|
|
1048
|
-
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${
|
|
1563
|
+
fail2(
|
|
1564
|
+
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path5.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
|
|
1049
1565
|
);
|
|
1050
1566
|
}
|
|
1051
1567
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
1052
1568
|
}
|
|
1053
1569
|
function ensureGitignoreLine(targetDir, line) {
|
|
1054
|
-
const p =
|
|
1055
|
-
let content =
|
|
1570
|
+
const p = path5.join(targetDir, ".gitignore");
|
|
1571
|
+
let content = fs5.existsSync(p) ? fs5.readFileSync(p, "utf-8") : "";
|
|
1056
1572
|
const lines = content.split(/\r?\n/);
|
|
1057
1573
|
if (lines.some((l) => l.trim() === line)) return;
|
|
1058
1574
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
@@ -1062,28 +1578,28 @@ function ensureGitignoreLine(targetDir, line) {
|
|
|
1062
1578
|
dryNote(`would append "${line}" to .gitignore`);
|
|
1063
1579
|
return;
|
|
1064
1580
|
}
|
|
1065
|
-
|
|
1581
|
+
fs5.writeFileSync(p, content, "utf-8");
|
|
1066
1582
|
ok(`appended ${line} to .gitignore`);
|
|
1067
1583
|
}
|
|
1068
1584
|
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
1069
|
-
if (!
|
|
1070
|
-
for (const entry of
|
|
1071
|
-
const srcPath =
|
|
1072
|
-
const destPath =
|
|
1585
|
+
if (!fs5.existsSync(srcDir)) return;
|
|
1586
|
+
for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) {
|
|
1587
|
+
const srcPath = path5.join(srcDir, entry.name);
|
|
1588
|
+
const destPath = path5.join(destDir, entry.name);
|
|
1073
1589
|
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
1074
1590
|
if (entry.isDirectory()) {
|
|
1075
1591
|
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
1076
1592
|
} else if (entry.isFile()) {
|
|
1077
|
-
const existed =
|
|
1593
|
+
const existed = fs5.existsSync(destPath);
|
|
1078
1594
|
if (DRY_RUN) {
|
|
1079
1595
|
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
1080
1596
|
continue;
|
|
1081
1597
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1598
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
1599
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1084
1600
|
try {
|
|
1085
|
-
const srcMode =
|
|
1086
|
-
|
|
1601
|
+
const srcMode = fs5.statSync(srcPath).mode;
|
|
1602
|
+
fs5.chmodSync(destPath, srcMode);
|
|
1087
1603
|
} catch {
|
|
1088
1604
|
}
|
|
1089
1605
|
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
@@ -1091,54 +1607,78 @@ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
|
1091
1607
|
}
|
|
1092
1608
|
}
|
|
1093
1609
|
function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
|
|
1094
|
-
const scaffoldsRoot =
|
|
1095
|
-
if (!
|
|
1096
|
-
|
|
1097
|
-
return;
|
|
1610
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
1611
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1612
|
+
warn(`migrate-safety scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug; main onboarding unaffected`);
|
|
1613
|
+
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1098
1614
|
}
|
|
1099
1615
|
const files = [
|
|
1100
1616
|
{
|
|
1101
|
-
src:
|
|
1102
|
-
dest:
|
|
1617
|
+
src: path5.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
1618
|
+
dest: path5.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
1103
1619
|
label: ".github/workflows/backup-on-migration.yml"
|
|
1104
1620
|
},
|
|
1105
1621
|
{
|
|
1106
|
-
src:
|
|
1107
|
-
dest:
|
|
1622
|
+
src: path5.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
1623
|
+
dest: path5.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
1108
1624
|
label: "scripts/migrate-with-backup.sh"
|
|
1109
1625
|
},
|
|
1110
1626
|
{
|
|
1111
|
-
src:
|
|
1112
|
-
dest:
|
|
1627
|
+
src: path5.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
1628
|
+
dest: path5.join(targetDir, "docs", "migrations-runbook.md"),
|
|
1113
1629
|
label: "docs/migrations-runbook.md"
|
|
1114
1630
|
}
|
|
1115
1631
|
];
|
|
1116
1632
|
info(`scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook)${refreshScaffolds ? " \u2014 --refresh-scaffolds active" : ""} \u2026`);
|
|
1117
|
-
|
|
1633
|
+
const counts = { wrote: 0, inSync: 0, drifted: 0, refreshed: 0, missing: 0 };
|
|
1634
|
+
for (const f of files) {
|
|
1635
|
+
const r = copyScaffoldDriftAware(f.src, f.dest, f.label, refreshScaffolds);
|
|
1636
|
+
if (r === "wrote") counts.wrote++;
|
|
1637
|
+
else if (r === "in-sync") counts.inSync++;
|
|
1638
|
+
else if (r === "drifted-preserved") counts.drifted++;
|
|
1639
|
+
else if (r === "drifted-refreshed") counts.refreshed++;
|
|
1640
|
+
else counts.missing++;
|
|
1641
|
+
}
|
|
1118
1642
|
ensureGitignoreLine(targetDir, ".backups/");
|
|
1119
1643
|
ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
|
|
1644
|
+
return summarizeFileCounts(counts);
|
|
1645
|
+
}
|
|
1646
|
+
function summarizeFileCounts(counts) {
|
|
1647
|
+
const total = counts.wrote + counts.inSync + counts.drifted + counts.refreshed + counts.missing;
|
|
1648
|
+
const parts = [];
|
|
1649
|
+
if (counts.wrote) parts.push(`${counts.wrote} written`);
|
|
1650
|
+
if (counts.refreshed) parts.push(`${counts.refreshed} refreshed`);
|
|
1651
|
+
if (counts.drifted) parts.push(`${counts.drifted} drifted (preserved \u2014 pass --refresh-scaffolds to update)`);
|
|
1652
|
+
if (counts.inSync && (counts.wrote || counts.refreshed || counts.drifted)) parts.push(`${counts.inSync} in sync`);
|
|
1653
|
+
if (counts.missing) parts.push(`${counts.missing} missing scaffold src`);
|
|
1654
|
+
if (parts.length === 0) parts.push(`in sync (${total} file${total === 1 ? "" : "s"})`);
|
|
1655
|
+
else if (!counts.wrote && !counts.refreshed && !counts.drifted && counts.inSync === total) {
|
|
1656
|
+
return { status: "in-sync", summary: `in sync (${total} file${total === 1 ? "" : "s"})` };
|
|
1657
|
+
}
|
|
1658
|
+
const status = counts.drifted || counts.missing ? "warn" : counts.wrote || counts.refreshed ? "ok" : "in-sync";
|
|
1659
|
+
return { status, summary: parts.join(", ") };
|
|
1120
1660
|
}
|
|
1121
1661
|
function hashFile(p) {
|
|
1122
1662
|
try {
|
|
1123
|
-
return crypto.createHash("sha256").update(
|
|
1663
|
+
return crypto.createHash("sha256").update(fs5.readFileSync(p)).digest("hex");
|
|
1124
1664
|
} catch {
|
|
1125
1665
|
return null;
|
|
1126
1666
|
}
|
|
1127
1667
|
}
|
|
1128
1668
|
function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
1129
|
-
if (!
|
|
1669
|
+
if (!fs5.existsSync(srcPath)) {
|
|
1130
1670
|
info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
|
|
1131
1671
|
return "missing-src";
|
|
1132
1672
|
}
|
|
1133
|
-
if (!
|
|
1673
|
+
if (!fs5.existsSync(destPath)) {
|
|
1134
1674
|
if (DRY_RUN) {
|
|
1135
1675
|
dryNote(`would write ${label}`);
|
|
1136
1676
|
return "wrote";
|
|
1137
1677
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1678
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
1679
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1140
1680
|
try {
|
|
1141
|
-
|
|
1681
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1142
1682
|
} catch {
|
|
1143
1683
|
}
|
|
1144
1684
|
ok(`wrote ${label}`);
|
|
@@ -1156,9 +1696,9 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1156
1696
|
dryNote(`would refresh ${label} (overrides local edits)`);
|
|
1157
1697
|
return "drifted-refreshed";
|
|
1158
1698
|
}
|
|
1159
|
-
|
|
1699
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1160
1700
|
try {
|
|
1161
|
-
|
|
1701
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1162
1702
|
} catch {
|
|
1163
1703
|
}
|
|
1164
1704
|
ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
|
|
@@ -1170,10 +1710,10 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1170
1710
|
var MARKETPLACE_ID = "launch-secure";
|
|
1171
1711
|
var PLUGIN_ID = "kit";
|
|
1172
1712
|
function isDogfoodMarketplace(targetDir) {
|
|
1173
|
-
const p =
|
|
1174
|
-
if (!
|
|
1713
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1714
|
+
if (!fs5.existsSync(p)) return { isDogfood: false };
|
|
1175
1715
|
try {
|
|
1176
|
-
const settings = JSON.parse(
|
|
1716
|
+
const settings = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1177
1717
|
const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
|
|
1178
1718
|
if (existingPath && existingPath !== "./.claude/marketplace") {
|
|
1179
1719
|
return { isDogfood: true, existingPath };
|
|
@@ -1184,33 +1724,34 @@ function isDogfoodMarketplace(targetDir) {
|
|
|
1184
1724
|
}
|
|
1185
1725
|
}
|
|
1186
1726
|
function scaffoldLsMarketplace(targetDir) {
|
|
1187
|
-
const scaffoldsRoot =
|
|
1188
|
-
if (!
|
|
1189
|
-
|
|
1190
|
-
return;
|
|
1727
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
1728
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1729
|
+
warn(`ls-marketplace scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1730
|
+
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1191
1731
|
}
|
|
1192
1732
|
const dogfood = isDogfoodMarketplace(targetDir);
|
|
1193
1733
|
if (dogfood.isDogfood) {
|
|
1194
1734
|
info(`dogfood marketplace pointer detected (${dogfood.existingPath}) \u2014 skipping copy, only refreshing enabledPlugins`);
|
|
1195
1735
|
wireLsSettings(targetDir);
|
|
1196
1736
|
ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
|
|
1197
|
-
return;
|
|
1737
|
+
return { status: "ok", summary: `dogfood pointer (${dogfood.existingPath})` };
|
|
1198
1738
|
}
|
|
1199
|
-
const marketplaceRoot =
|
|
1739
|
+
const marketplaceRoot = path5.join(targetDir, ".claude", "marketplace");
|
|
1200
1740
|
info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
|
|
1201
1741
|
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
1202
1742
|
wireLsSettings(targetDir);
|
|
1203
1743
|
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`);
|
|
1744
|
+
return { status: "ok", summary: "marketplace tree + settings refreshed" };
|
|
1204
1745
|
}
|
|
1205
1746
|
function wireLsSettings(targetDir) {
|
|
1206
|
-
const p =
|
|
1207
|
-
const hadExisting =
|
|
1747
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1748
|
+
const hadExisting = fs5.existsSync(p);
|
|
1208
1749
|
let existing = {};
|
|
1209
1750
|
if (hadExisting) {
|
|
1210
1751
|
try {
|
|
1211
|
-
existing = JSON.parse(
|
|
1752
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1212
1753
|
} catch (err) {
|
|
1213
|
-
|
|
1754
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1214
1755
|
}
|
|
1215
1756
|
}
|
|
1216
1757
|
const merged = { ...existing };
|
|
@@ -1238,32 +1779,33 @@ function wireLsSettings(targetDir) {
|
|
|
1238
1779
|
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
1239
1780
|
return;
|
|
1240
1781
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1782
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
1783
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1243
1784
|
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
|
|
1244
1785
|
}
|
|
1245
1786
|
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
1246
1787
|
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
1247
1788
|
function scaffoldRecallHook(targetDir) {
|
|
1248
|
-
const scaffoldsRoot =
|
|
1249
|
-
if (!
|
|
1250
|
-
|
|
1251
|
-
return;
|
|
1789
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
1790
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1791
|
+
warn(`recall-hook scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1792
|
+
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1252
1793
|
}
|
|
1253
1794
|
info("scaffolding recall-hook (SessionStart watcher-respawn hook + ensure-recall.sh) \u2026");
|
|
1254
1795
|
copyScaffoldDirAlways(scaffoldsRoot, targetDir, "");
|
|
1255
|
-
wireRecallHook(targetDir);
|
|
1796
|
+
const wired = wireRecallHook(targetDir);
|
|
1256
1797
|
ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
|
|
1798
|
+
return wired ? { status: "ok", summary: "ensure-recall.sh refreshed + SessionStart hook appended" } : { status: "ok", summary: "ensure-recall.sh refreshed (hook already wired)" };
|
|
1257
1799
|
}
|
|
1258
1800
|
function wireRecallHook(targetDir) {
|
|
1259
|
-
const p =
|
|
1260
|
-
const hadExisting =
|
|
1801
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1802
|
+
const hadExisting = fs5.existsSync(p);
|
|
1261
1803
|
let existing = {};
|
|
1262
1804
|
if (hadExisting) {
|
|
1263
1805
|
try {
|
|
1264
|
-
existing = JSON.parse(
|
|
1806
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1265
1807
|
} catch (err) {
|
|
1266
|
-
|
|
1808
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1267
1809
|
}
|
|
1268
1810
|
}
|
|
1269
1811
|
const hooks = existing.hooks ?? {};
|
|
@@ -1273,7 +1815,7 @@ function wireRecallHook(targetDir) {
|
|
|
1273
1815
|
);
|
|
1274
1816
|
if (alreadyWired) {
|
|
1275
1817
|
info(".claude/settings.json SessionStart hook already references ensure-recall.sh \u2014 leaving alone");
|
|
1276
|
-
return;
|
|
1818
|
+
return false;
|
|
1277
1819
|
}
|
|
1278
1820
|
const newGroup = {
|
|
1279
1821
|
hooks: [{ type: "command", command: RECALL_HOOK_COMMAND }]
|
|
@@ -1287,26 +1829,58 @@ function wireRecallHook(targetDir) {
|
|
|
1287
1829
|
};
|
|
1288
1830
|
if (DRY_RUN) {
|
|
1289
1831
|
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
1290
|
-
return;
|
|
1832
|
+
return true;
|
|
1291
1833
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1834
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
1835
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1294
1836
|
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
1837
|
+
return true;
|
|
1295
1838
|
}
|
|
1296
1839
|
function tryActivateStatusline() {
|
|
1297
1840
|
if (DRY_RUN) {
|
|
1298
1841
|
dryNote(`would wrap ~/.claude/settings.json statusLine.command with launch-kit's MCP chip wrapper (skips silently if no statusline configured)`);
|
|
1299
|
-
return;
|
|
1842
|
+
return { status: "skipped", summary: "(dry-run)" };
|
|
1300
1843
|
}
|
|
1301
1844
|
const res = activateStatusline();
|
|
1302
1845
|
if (res.ok && res.outcome === "activated") {
|
|
1303
1846
|
ok(`statusline wrapped \u2014 MCP chips will render alongside your existing statusline next session`);
|
|
1304
|
-
|
|
1847
|
+
return { status: "ok", summary: "wrapped \u2014 MCP chips render next session" };
|
|
1848
|
+
}
|
|
1849
|
+
if (res.ok && res.outcome === "refreshed") {
|
|
1305
1850
|
info(`statusline already wrapped \u2014 refreshed chip scripts`);
|
|
1851
|
+
return { status: "ok", summary: "chip scripts refreshed" };
|
|
1306
1852
|
}
|
|
1853
|
+
return null;
|
|
1307
1854
|
}
|
|
1308
|
-
async function
|
|
1855
|
+
async function main2() {
|
|
1309
1856
|
const subcommand = process.argv[2];
|
|
1857
|
+
if (subcommand === "radar-docker-init") {
|
|
1858
|
+
await Promise.resolve().then(() => (init_radar_docker_init_entry(), radar_docker_init_entry_exports));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (subcommand === "setup-git") {
|
|
1862
|
+
if (process.argv[3] === "--help" || process.argv[3] === "-h") {
|
|
1863
|
+
console.log('usage: launch-kit setup-git --identity="Name <email>"');
|
|
1864
|
+
console.log("");
|
|
1865
|
+
console.log(" Runs `gh auth setup-git` when GH_TOKEN is set, then sets the");
|
|
1866
|
+
console.log(" global git user.name, user.email, init.defaultBranch=main, and");
|
|
1867
|
+
console.log(" pull.rebase=false. Idempotent; safe to re-run.");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
let identityVal = null;
|
|
1871
|
+
for (const a of process.argv.slice(3)) {
|
|
1872
|
+
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
1873
|
+
else fail2(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
1874
|
+
}
|
|
1875
|
+
if (!identityVal) fail2(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
1876
|
+
try {
|
|
1877
|
+
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
1878
|
+
configureGitForBot(identity);
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1881
|
+
}
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1310
1884
|
if (subcommand === "statusline") {
|
|
1311
1885
|
const action = process.argv[3];
|
|
1312
1886
|
if (!action || action === "--help" || action === "-h") {
|
|
@@ -1323,17 +1897,50 @@ async function main() {
|
|
|
1323
1897
|
for (const a of process.argv.slice(4)) {
|
|
1324
1898
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
1325
1899
|
else if (a === "--compact") compactArg = true;
|
|
1326
|
-
else
|
|
1900
|
+
else fail2(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
1327
1901
|
}
|
|
1328
1902
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
1329
1903
|
let res;
|
|
1330
1904
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
1331
1905
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
1332
|
-
else
|
|
1906
|
+
else fail2(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
1333
1907
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1334
1908
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1335
1909
|
return;
|
|
1336
1910
|
}
|
|
1911
|
+
if (subcommand === "secrets") {
|
|
1912
|
+
const action = process.argv[3];
|
|
1913
|
+
if (!action || action === "--help" || action === "-h") {
|
|
1914
|
+
console.log("usage: launch-kit secrets pull [--env=<slug>] [--dir=<path>] [--file=<name>]");
|
|
1915
|
+
console.log("");
|
|
1916
|
+
console.log(" --env=<slug> env to pull from. Overrides $LS_ENV and the project-level default.");
|
|
1917
|
+
console.log(" --dir=<path> project root (default: cwd). Must contain a launch-kit cred file.");
|
|
1918
|
+
console.log(" --file=<name> output file relative to --dir (default: .env.local).");
|
|
1919
|
+
console.log("");
|
|
1920
|
+
console.log(" Resolution order: --env \u2192 $LS_ENV \u2192 server-side default \u2192 single-env auto-pick.");
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (action !== "pull") {
|
|
1924
|
+
fail2(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
1925
|
+
}
|
|
1926
|
+
let envOverride = null;
|
|
1927
|
+
let dirArg = null;
|
|
1928
|
+
let fileArg = ".env.local";
|
|
1929
|
+
for (const a of process.argv.slice(4)) {
|
|
1930
|
+
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
1931
|
+
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
1932
|
+
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
1933
|
+
else fail2(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
1934
|
+
}
|
|
1935
|
+
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
1936
|
+
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
1937
|
+
try {
|
|
1938
|
+
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1337
1944
|
const args = parseArgs(process.argv.slice(2));
|
|
1338
1945
|
if (args.help) {
|
|
1339
1946
|
if (subcommand === "refresh") printRefreshHelp();
|
|
@@ -1341,25 +1948,26 @@ async function main() {
|
|
|
1341
1948
|
return;
|
|
1342
1949
|
}
|
|
1343
1950
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
1344
|
-
|
|
1951
|
+
fail2(`missing subcommand. Usage: launch-kit <init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
1345
1952
|
}
|
|
1346
1953
|
if (subcommand !== "init" && subcommand !== "refresh") {
|
|
1347
|
-
|
|
1954
|
+
fail2(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
1348
1955
|
}
|
|
1349
1956
|
DRY_RUN = args.dryRun;
|
|
1957
|
+
VERBOSE = args.verbose || DRY_RUN;
|
|
1350
1958
|
if (DRY_RUN) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1959
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1960
|
+
console.log(c.bold("DRY RUN") + c.dim(" \u2014 no files will be written, no commands will run."));
|
|
1961
|
+
console.log(c.dim("Lines tagged (dry-run) show what would happen."));
|
|
1962
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1355
1963
|
}
|
|
1356
1964
|
if (subcommand === "refresh") return mainRefresh(args);
|
|
1357
1965
|
return mainInit(args);
|
|
1358
1966
|
}
|
|
1359
1967
|
async function mainRefresh(args) {
|
|
1360
1968
|
const cwd = process.cwd();
|
|
1361
|
-
const targetDir =
|
|
1362
|
-
if (!
|
|
1969
|
+
const targetDir = path5.resolve(args.targetDir ?? cwd);
|
|
1970
|
+
if (!fs5.existsSync(targetDir)) fail2(`target dir does not exist: ${targetDir}`);
|
|
1363
1971
|
let cred;
|
|
1364
1972
|
let source;
|
|
1365
1973
|
try {
|
|
@@ -1367,7 +1975,7 @@ async function mainRefresh(args) {
|
|
|
1367
1975
|
cred = recovery.cred;
|
|
1368
1976
|
source = recovery.source;
|
|
1369
1977
|
} catch (err) {
|
|
1370
|
-
|
|
1978
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1371
1979
|
}
|
|
1372
1980
|
if (cred && source === "mcp") {
|
|
1373
1981
|
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
@@ -1376,44 +1984,53 @@ async function mainRefresh(args) {
|
|
|
1376
1984
|
if (DRY_RUN) {
|
|
1377
1985
|
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
1378
1986
|
} else {
|
|
1379
|
-
writeJsonAtomic(
|
|
1987
|
+
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
1380
1988
|
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
1381
1989
|
}
|
|
1382
1990
|
}
|
|
1383
1991
|
if (!cred) {
|
|
1384
|
-
|
|
1385
|
-
`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=${
|
|
1992
|
+
fail2(
|
|
1993
|
+
`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=${path5.relative(cwd, targetDir) || "."}\` first.`
|
|
1386
1994
|
);
|
|
1387
1995
|
}
|
|
1388
1996
|
const nested = toNested(cred);
|
|
1389
|
-
if (!nested)
|
|
1997
|
+
if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
1390
1998
|
const active = nested.profiles[nested.active];
|
|
1391
|
-
if (!active)
|
|
1999
|
+
if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
1392
2000
|
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
2001
|
+
header("launch-kit refresh", [
|
|
2002
|
+
["course", nested.active],
|
|
2003
|
+
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
2004
|
+
["dir", path5.relative(cwd, targetDir) || "."]
|
|
2005
|
+
]);
|
|
1393
2006
|
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
1394
|
-
mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
|
|
2007
|
+
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
1395
2008
|
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
1396
|
-
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir, args.refreshScaffolds);
|
|
1397
|
-
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
1398
|
-
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
1399
|
-
tryActivateStatusline();
|
|
1400
|
-
|
|
2009
|
+
if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
|
|
2010
|
+
if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
|
|
2011
|
+
if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
|
|
2012
|
+
const slR = tryActivateStatusline();
|
|
2013
|
+
if (slR) phase("statusline", slR);
|
|
1401
2014
|
if (DRY_RUN) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
2015
|
+
console.log("");
|
|
2016
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2017
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
|
|
2018
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1405
2019
|
return;
|
|
1406
2020
|
}
|
|
1407
2021
|
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2022
|
+
const showGuide = args.quiet ? false : args.guide === true;
|
|
2023
|
+
const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
|
|
2024
|
+
if (!showGuide && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
|
|
2025
|
+
footer("Refresh complete.", hints);
|
|
2026
|
+
if (showGuide) {
|
|
2027
|
+
console.log(c.dim(`(refresh never runs these: clone, dependency install, onboard script, launch-recall init \u2014 use \`launch-kit init\` for a full bootstrap)`));
|
|
2028
|
+
console.log(getLaunchKitToolsGuide());
|
|
1412
2029
|
}
|
|
1413
2030
|
}
|
|
1414
2031
|
async function mainInit(args) {
|
|
1415
|
-
const probeDir =
|
|
1416
|
-
if (!args.force &&
|
|
2032
|
+
const probeDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2033
|
+
if (!args.force && fs5.existsSync(probeDir)) {
|
|
1417
2034
|
const detection = detectExistingBootstrap(probeDir);
|
|
1418
2035
|
if (detection.bootstrapped) {
|
|
1419
2036
|
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
@@ -1422,8 +2039,8 @@ async function mainInit(args) {
|
|
|
1422
2039
|
}
|
|
1423
2040
|
}
|
|
1424
2041
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
1425
|
-
const recoveryDir =
|
|
1426
|
-
if (
|
|
2042
|
+
const recoveryDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2043
|
+
if (fs5.existsSync(recoveryDir)) {
|
|
1427
2044
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
1428
2045
|
const nested = cred ? toNested(cred) : null;
|
|
1429
2046
|
const recovered = nested ? nested.profiles[nested.active] : cred;
|
|
@@ -1450,43 +2067,61 @@ async function mainInit(args) {
|
|
|
1450
2067
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
1451
2068
|
args.token = t || null;
|
|
1452
2069
|
}
|
|
1453
|
-
if (!args.token)
|
|
1454
|
-
if (!/^ls_pat_/.test(args.token))
|
|
1455
|
-
if (!args.orgSlug)
|
|
1456
|
-
if (!args.projectSlug)
|
|
2070
|
+
if (!args.token) fail2("--token (or LS_PAT env) is required.");
|
|
2071
|
+
if (!/^ls_pat_/.test(args.token)) fail2("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
2072
|
+
if (!args.orgSlug) fail2("--org=<orgSlug> is required.");
|
|
2073
|
+
if (!args.projectSlug) fail2("--project=<projectSlug> is required.");
|
|
2074
|
+
header("launch-kit init", [
|
|
2075
|
+
["org", args.orgSlug],
|
|
2076
|
+
["project", args.projectSlug],
|
|
2077
|
+
["server", args.serverUrl]
|
|
2078
|
+
]);
|
|
1457
2079
|
const { hasGh } = preflight();
|
|
2080
|
+
phase("preflight", { status: "ok", summary: `node ${process.versions.node}${hasGh ? " \xB7 git \xB7 gh" : " \xB7 git (gh not found \u2014 will use git for clone)"}` });
|
|
2081
|
+
if (args.gitIdentity) {
|
|
2082
|
+
configureGitForBot(args.gitIdentity);
|
|
2083
|
+
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 gh credential helper wired" : ""}` });
|
|
2084
|
+
}
|
|
1458
2085
|
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
1459
2086
|
let resolved;
|
|
1460
2087
|
try {
|
|
1461
2088
|
resolved = await callProjectInfo(args);
|
|
1462
2089
|
} catch (err) {
|
|
1463
|
-
|
|
2090
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1464
2091
|
}
|
|
1465
2092
|
ok(`resolved "${resolved.projectName}"`);
|
|
2093
|
+
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
1466
2094
|
if (!resolved.repositoryUrl) {
|
|
1467
|
-
|
|
2095
|
+
fail2(
|
|
1468
2096
|
`Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
|
|
1469
2097
|
);
|
|
1470
2098
|
}
|
|
1471
2099
|
const repoUrl = resolved.repositoryUrl;
|
|
1472
2100
|
const cwd = process.cwd();
|
|
1473
|
-
const targetDir =
|
|
2101
|
+
const targetDir = path5.resolve(args.targetDir ?? path5.join(cwd, resolved.projectSlug));
|
|
1474
2102
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
1475
2103
|
let skipClone = false;
|
|
1476
|
-
if (
|
|
2104
|
+
if (fs5.existsSync(targetDir)) {
|
|
1477
2105
|
if (isGitRepo(targetDir)) {
|
|
1478
2106
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
1479
2107
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
1480
2108
|
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
|
|
1481
2109
|
skipClone = true;
|
|
1482
2110
|
} else {
|
|
1483
|
-
|
|
2111
|
+
fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
1484
2112
|
}
|
|
1485
2113
|
} else if (!dirIsEmpty(targetDir)) {
|
|
1486
|
-
|
|
2114
|
+
fail2(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
1487
2115
|
}
|
|
1488
2116
|
}
|
|
1489
|
-
|
|
2117
|
+
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2118
|
+
if (!skipClone) {
|
|
2119
|
+
section(`Cloning ${repoUrl}`);
|
|
2120
|
+
cloneRepo(repoUrl, targetDir, hasGh);
|
|
2121
|
+
phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
|
|
2122
|
+
} else {
|
|
2123
|
+
phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
|
|
2124
|
+
}
|
|
1490
2125
|
const cfg = {
|
|
1491
2126
|
pat: args.token,
|
|
1492
2127
|
orgSlug: resolved.orgSlug,
|
|
@@ -1495,7 +2130,8 @@ async function mainInit(args) {
|
|
|
1495
2130
|
};
|
|
1496
2131
|
const courseName = args.course ?? inferCourseName(cfg.serverUrl);
|
|
1497
2132
|
writeConfigFile(targetDir, cfg, courseName);
|
|
1498
|
-
|
|
2133
|
+
phase("cred file", { status: "ok", summary: `course=${courseName}, active` });
|
|
2134
|
+
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
1499
2135
|
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
1500
2136
|
let installSkippedReason = null;
|
|
1501
2137
|
const detected = detectPackageManager(targetDir);
|
|
@@ -1505,45 +2141,71 @@ async function mainInit(args) {
|
|
|
1505
2141
|
} else if (!detected) {
|
|
1506
2142
|
installSkippedReason = "no package.json found";
|
|
1507
2143
|
} else {
|
|
2144
|
+
section(`Installing dependencies (${detected.pm.name})`);
|
|
1508
2145
|
runInstall(targetDir, detected);
|
|
1509
|
-
|
|
2146
|
+
phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
|
|
2147
|
+
if (!args.noOnboard) {
|
|
2148
|
+
if (hasOnboardScript(targetDir)) {
|
|
2149
|
+
section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
|
|
2150
|
+
runOnboard(targetDir, detected.pm);
|
|
2151
|
+
phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
|
|
2152
|
+
} else {
|
|
2153
|
+
section("Pulling environment secrets");
|
|
2154
|
+
info("running launch-kit secrets pull \u2026");
|
|
2155
|
+
if (DRY_RUN) {
|
|
2156
|
+
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2157
|
+
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2158
|
+
} else {
|
|
2159
|
+
try {
|
|
2160
|
+
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2161
|
+
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2164
|
+
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2165
|
+
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
1510
2170
|
}
|
|
1511
2171
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
1512
|
-
if (
|
|
1513
|
-
if (!args.
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
2172
|
+
if (installSkippedReason) phase("install", { status: "skipped", summary: installSkippedReason });
|
|
2173
|
+
if (!args.noRecall) {
|
|
2174
|
+
section("Initializing launch-recall (shadow git backup)");
|
|
2175
|
+
runRecallInit(targetDir);
|
|
2176
|
+
phase("launch-recall", { status: "ok", summary: "shadow git ready" });
|
|
2177
|
+
}
|
|
2178
|
+
if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
|
|
2179
|
+
if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
|
|
2180
|
+
if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
|
|
2181
|
+
const slR = tryActivateStatusline();
|
|
2182
|
+
if (slR) phase("statusline", slR);
|
|
1519
2183
|
if (DRY_RUN) {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2184
|
+
console.log("");
|
|
2185
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2186
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(` \u2014 no files were modified, no commands ran.`));
|
|
2187
|
+
console.log(c.dim(`Target: ${targetDir}`));
|
|
2188
|
+
console.log(c.dim(`Re-run without --dry-run to apply the changes shown above.`));
|
|
2189
|
+
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1525
2190
|
return;
|
|
1526
2191
|
}
|
|
1527
2192
|
ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
|
|
2193
|
+
const showGuide = args.quiet ? false : args.guide ?? true;
|
|
2194
|
+
const nextSteps = [`cd ${relTarget}`];
|
|
1528
2195
|
if (installSkippedReason) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
claude # launch Claude Code (5 MCPs wired)
|
|
1543
|
-
${getLaunchKitToolsGuide()}`);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
main().catch((err) => {
|
|
2196
|
+
nextSteps.push(detected ? `${detected.pm.binary} ${detected.pm.installArgs.join(" ")} # install skipped: ${installSkippedReason}` : `npm install # install skipped: ${installSkippedReason}`);
|
|
2197
|
+
if (hasOnboard && detected && !args.noOnboard) {
|
|
2198
|
+
nextSteps.push(`${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
nextSteps.push("claude # launch Claude Code (5 MCPs wired)");
|
|
2202
|
+
footer(`${resolved.projectName} is ready at ${relTarget}.`, [
|
|
2203
|
+
"Next:",
|
|
2204
|
+
...nextSteps.map((s) => " " + s)
|
|
2205
|
+
]);
|
|
2206
|
+
if (showGuide) console.log(getLaunchKitToolsGuide());
|
|
2207
|
+
}
|
|
2208
|
+
main2().catch((err) => {
|
|
1547
2209
|
console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
1548
2210
|
process.exit(1);
|
|
1549
2211
|
});
|