@launchsecure/launch-kit 0.0.32 → 0.0.34
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/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-C7GsHvgg.js → _baseUniq-BiVx0WO_.js} +1 -1
- package/dist/deck-client/assets/{arc-CSrZRINY.js → arc-DGMkiEzS.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-zoB-G17J.js → architectureDiagram-Q4EWVU46-Y2WRmHtk.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BRjjtYH6.js → blockDiagram-DXYQGD6D-_Lbfu5BQ.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-C3D3sd2U.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-DhpDMOPO.js → chunk-4BX2VUAB-liEIbPHs.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-BIRgPXRl.js → chunk-4TB4RGXK-CCc6lYvL.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-BF24dwDZ.js → chunk-55IACEB6-D02jJUR2.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-CW75Y61B.js → chunk-EDXVE4YY-BFmGMbLD.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-B5-oyL79.js → chunk-FMBD7UC4-6wFLOVcJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-BB2bHe_Q.js → chunk-OYMX7WX6-Bnr8RiBf.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-D80eZO4B.js → chunk-QZHKN3VN-Ct82MksJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-Dz9787p_.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-MQjiZLcL.js → cose-bilkent-S5V4N54A-CmQCT-mH.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-DG4EcLpJ.js → dagre-KV5264BT-DDdSa9EX.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-1n7hM3Gc.js → diagram-5BDNPKRD-Bccks2xJ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-CYMarncV.js → diagram-G4DWMVQ6-CPPNgxmQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-DSisoipe.js → diagram-MMDJMWI5-KrD300pS.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-Btnq49OJ.js → diagram-TYMM5635-DefnLuQf.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-Cu2Hb_Tz.js → erDiagram-SMLLAGMA-DI9FfnFP.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-CGJzUzsO.js → flowDiagram-DWJPFMVM-twKyd3Fx.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-D9sqGUBT.js → ganttDiagram-T4ZO3ILL-Wau3jhBr.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-C0QwX2od.js → gitGraphDiagram-UUTBAWPF-D9GgYXwb.js} +1 -1
- package/dist/deck-client/assets/{graph-CcBjOQCl.js → graph-BhNLzyXS.js} +1 -1
- package/dist/deck-client/assets/index-B-YQq5b5.css +1 -0
- package/dist/deck-client/assets/{index-0arwoc0z.js → index-BtQBaQ7s.js} +3 -3
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-DTimhhhS.js → infoDiagram-42DDH7IO-TylGlSG-.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DxOxg_B4.js → ishikawaDiagram-UXIWVN3A-DAT8icpg.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-Bpq0qa4j.js → journeyDiagram-VCZTEJTY-D3v_XL72.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-aTIrpcVO.js → kanban-definition-6JOO6SKY-DNUOBiNr.js} +1 -1
- package/dist/deck-client/assets/{layout-DqglLR2E.js → layout-COfodgwF.js} +1 -1
- package/dist/deck-client/assets/{linear-D5GxehPc.js → linear-DmTsuIvK.js} +1 -1
- package/dist/deck-client/assets/{min-DXLfSREq.js → min-BW1F7i1D.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-mO5Vys7I.js → mindmap-definition-QFDTVHPH-CErFzKWl.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-Dm0gzdAr.js → pieDiagram-DEJITSTG-DW5F757o.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-Daq7j3qD.js → quadrantDiagram-34T5L4WZ-B1S2-TfI.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CmwV95um.js → requirementDiagram-MS252O5E-BY5BAR-5.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BOYl3Nkf.js → sankeyDiagram-XADWPNL6-CE1Cp9HS.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-BuUjhIcW.js → sequenceDiagram-FGHM5R23-IaHnbKye.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-LUZ_uwio.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-CDUxCCAW.js → timeline-definition-GMOUNBTQ-DVFGGSgN.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-BRb24Tf7.js → vennDiagram-DHZGUBPP-C1194MJi.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-BLGlYrQz.js → wardleyDiagram-NUSXRM2D-hpwdFfGj.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-De31MSnk.js → xychartDiagram-5P7HB3ND-DYkotwy8.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/chart-serve.js +167 -2
- package/dist/server/cli.js +328 -42
- package/dist/server/course-entry.js +1 -1
- package/dist/server/graph-mcp-entry.js +180 -4
- package/dist/server/init-entry.js +1133 -219
- package/dist/server/launch-radar-entry.js +45 -0
- package/dist/server/parse-worker-entry.js +167 -2
- package/dist/server/radar-docker-init-entry.js +644 -0
- package/dist/server/radar-entrypoint-entry.js +99 -0
- package/dist/server/radar-teardown-entry.js +478 -0
- package/dist/server/recall-entry.js +4 -1
- package/dist/server/rover-entry.js +20122 -0
- package/package.json +7 -5
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
- package/scaffolds/ls-marketplace/plugins/kit/commands/standup.md +6 -6
- package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +6 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +40 -48
- package/scaffolds/ls-marketplace/plugins/kit/skills/debug/SKILL.md +45 -20
- package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +76 -67
- package/scaffolds/ls-marketplace/plugins/kit/skills/handoff/SKILL.md +132 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +290 -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-8ReQnQfH.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-cRxTeGkK.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-cRxTeGkK.js +0 -1
- package/dist/deck-client/assets/clone-LSHZ3K6R.js +0 -1
- package/dist/deck-client/assets/index-BlTlhxFW.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CnnRwE5D.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-B0BYyVBY.js +0 -162
|
@@ -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,708 @@ 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
|
-
|
|
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;
|
|
387
|
+
}
|
|
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
|
+
};
|
|
167
468
|
}
|
|
168
|
-
|
|
169
|
-
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/server/launch-kit-services.ts
|
|
472
|
+
function defaultServices() {
|
|
473
|
+
return [expandShorthand("radar")];
|
|
474
|
+
}
|
|
475
|
+
function expandShorthand(name) {
|
|
476
|
+
const def = SHORTHANDS[name];
|
|
477
|
+
if (!def) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
483
|
+
}
|
|
484
|
+
function coerceEntry(raw, index) {
|
|
485
|
+
if (typeof raw === "string") {
|
|
486
|
+
return expandShorthand(raw);
|
|
487
|
+
}
|
|
488
|
+
if (typeof raw !== "object" || raw === null) {
|
|
489
|
+
throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
|
|
490
|
+
}
|
|
491
|
+
const r = raw;
|
|
492
|
+
if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
|
|
493
|
+
throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
|
|
494
|
+
}
|
|
495
|
+
if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
|
|
496
|
+
throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
|
|
170
497
|
}
|
|
171
|
-
const name = inferCourseName(cred.serverUrl);
|
|
172
498
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
499
|
+
name: r.name,
|
|
500
|
+
port: r.port,
|
|
501
|
+
bin: r.bin,
|
|
502
|
+
args: r.args ?? []
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function validate(services) {
|
|
506
|
+
if (services.length === 0) {
|
|
507
|
+
throw new Error(`[launch-kit-services] resolved an empty service list`);
|
|
508
|
+
}
|
|
509
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
510
|
+
const seenPorts = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const s of services) {
|
|
512
|
+
if (!DNS_NAME_RE.test(s.name)) {
|
|
513
|
+
throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
|
|
514
|
+
}
|
|
515
|
+
if (seenNames.has(s.name)) {
|
|
516
|
+
throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
|
|
517
|
+
}
|
|
518
|
+
seenNames.add(s.name);
|
|
519
|
+
if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
|
|
520
|
+
throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
|
|
521
|
+
}
|
|
522
|
+
if (seenPorts.has(s.port)) {
|
|
523
|
+
throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
|
|
524
|
+
}
|
|
525
|
+
seenPorts.add(s.port);
|
|
526
|
+
}
|
|
527
|
+
return services;
|
|
528
|
+
}
|
|
529
|
+
function resolveServices(opts = {}) {
|
|
530
|
+
const env = opts.env ?? process.env;
|
|
531
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
532
|
+
const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
|
|
533
|
+
if (rawEnv) {
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(rawEnv);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
539
|
+
}
|
|
540
|
+
if (!Array.isArray(parsed)) {
|
|
541
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
|
|
542
|
+
}
|
|
543
|
+
return validate(parsed.map(coerceEntry));
|
|
544
|
+
}
|
|
545
|
+
const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
|
|
546
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
547
|
+
let parsed;
|
|
548
|
+
try {
|
|
549
|
+
parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
550
|
+
} catch (err) {
|
|
551
|
+
throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
552
|
+
}
|
|
553
|
+
if (!Array.isArray(parsed)) {
|
|
554
|
+
throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
|
|
555
|
+
}
|
|
556
|
+
return validate(parsed.map(coerceEntry));
|
|
557
|
+
}
|
|
558
|
+
return validate(defaultServices());
|
|
559
|
+
}
|
|
560
|
+
var import_node_fs, import_node_path, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
|
|
561
|
+
var init_launch_kit_services = __esm({
|
|
562
|
+
"src/server/launch-kit-services.ts"() {
|
|
563
|
+
"use strict";
|
|
564
|
+
import_node_fs = require("node:fs");
|
|
565
|
+
import_node_path = require("node:path");
|
|
566
|
+
SHORTHANDS = {
|
|
567
|
+
radar: { port: 3517, bin: "launch-radar", args: [] },
|
|
568
|
+
sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
|
|
569
|
+
chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
|
|
570
|
+
deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
|
|
571
|
+
council: { port: 52839, bin: "launch-council", args: ["serve"] }
|
|
572
|
+
};
|
|
573
|
+
DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
574
|
+
SHORTHAND_NAMES = Object.keys(SHORTHANDS);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// src/server/cf-ingress.ts
|
|
579
|
+
async function cf(opts) {
|
|
580
|
+
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
581
|
+
method: opts.method,
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
584
|
+
"Content-Type": "application/json",
|
|
585
|
+
Accept: "application/json",
|
|
586
|
+
"User-Agent": "launch-kit/cf-ingress"
|
|
587
|
+
},
|
|
588
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
589
|
+
signal: AbortSignal.timeout(15e3)
|
|
590
|
+
});
|
|
591
|
+
const text = await res.text();
|
|
592
|
+
let parsed;
|
|
593
|
+
try {
|
|
594
|
+
parsed = text ? JSON.parse(text) : { success: false };
|
|
595
|
+
} catch {
|
|
596
|
+
throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
|
|
597
|
+
}
|
|
598
|
+
return parsed;
|
|
599
|
+
}
|
|
600
|
+
function isNotFound(env) {
|
|
601
|
+
return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
|
|
602
|
+
}
|
|
603
|
+
function loadState(path6) {
|
|
604
|
+
if (!(0, import_node_fs2.existsSync)(path6)) return null;
|
|
605
|
+
try {
|
|
606
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path6, "utf8"));
|
|
607
|
+
if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
|
|
608
|
+
return parsed;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
} catch {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function saveState(path6, state) {
|
|
616
|
+
const dir = (0, import_node_path2.dirname)(path6);
|
|
617
|
+
if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
618
|
+
(0, import_node_fs2.writeFileSync)(path6, JSON.stringify(state, null, 2));
|
|
619
|
+
}
|
|
620
|
+
async function ensureTunnel(input, knownTunnelId) {
|
|
621
|
+
if (knownTunnelId) {
|
|
622
|
+
const got = await cf({
|
|
623
|
+
apiToken: input.apiToken,
|
|
624
|
+
method: "GET",
|
|
625
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
|
|
626
|
+
});
|
|
627
|
+
if (got.success && got.result && !got.result.deleted_at) {
|
|
628
|
+
return knownTunnelId;
|
|
629
|
+
}
|
|
630
|
+
if (!isNotFound(got) && !got.success) {
|
|
631
|
+
throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const created = await cf({
|
|
635
|
+
apiToken: input.apiToken,
|
|
636
|
+
method: "POST",
|
|
637
|
+
path: `/accounts/${input.accountId}/cfd_tunnel`,
|
|
638
|
+
body: { name: input.tunnelName, config_src: "cloudflare" }
|
|
639
|
+
});
|
|
640
|
+
if (!created.success || !created.result) {
|
|
641
|
+
throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
|
|
642
|
+
}
|
|
643
|
+
return created.result.id;
|
|
644
|
+
}
|
|
645
|
+
async function fetchConnectorToken(input, tunnelId) {
|
|
646
|
+
const res = await cf({
|
|
647
|
+
apiToken: input.apiToken,
|
|
648
|
+
method: "GET",
|
|
649
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
|
|
650
|
+
});
|
|
651
|
+
if (!res.success || typeof res.result !== "string") {
|
|
652
|
+
throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
|
|
653
|
+
}
|
|
654
|
+
return res.result;
|
|
655
|
+
}
|
|
656
|
+
async function setIngressConfig(input, tunnelId) {
|
|
657
|
+
const ingress = input.services.map((s) => ({
|
|
658
|
+
hostname: `${s.name}.${input.zone.name}`,
|
|
659
|
+
service: `http://localhost:${s.port}`
|
|
660
|
+
}));
|
|
661
|
+
ingress.push({ service: "http_status:404" });
|
|
662
|
+
const res = await cf({
|
|
663
|
+
apiToken: input.apiToken,
|
|
664
|
+
method: "PUT",
|
|
665
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
666
|
+
body: { config: { ingress } }
|
|
667
|
+
});
|
|
668
|
+
if (!res.success) {
|
|
669
|
+
throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function ensureDnsRecord(input, tunnelId, service) {
|
|
673
|
+
const fqdn = `${service.name}.${input.zone.name}`;
|
|
674
|
+
const target = `${tunnelId}.cfargotunnel.com`;
|
|
675
|
+
const existing = await cf({
|
|
676
|
+
apiToken: input.apiToken,
|
|
677
|
+
method: "GET",
|
|
678
|
+
path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
|
|
679
|
+
});
|
|
680
|
+
if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
|
|
681
|
+
const rec = existing.result[0];
|
|
682
|
+
if (rec.content === target) return;
|
|
683
|
+
const upd = await cf({
|
|
684
|
+
apiToken: input.apiToken,
|
|
685
|
+
method: "PUT",
|
|
686
|
+
path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
|
|
687
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
688
|
+
});
|
|
689
|
+
if (!upd.success) {
|
|
690
|
+
throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const created = await cf({
|
|
695
|
+
apiToken: input.apiToken,
|
|
696
|
+
method: "POST",
|
|
697
|
+
path: `/zones/${input.zone.id}/dns_records`,
|
|
698
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
699
|
+
});
|
|
700
|
+
if (created.success) return;
|
|
701
|
+
if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
|
|
702
|
+
throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
|
|
703
|
+
}
|
|
704
|
+
async function provisionIngress(input) {
|
|
705
|
+
const prior = loadState(input.stateFile);
|
|
706
|
+
const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
|
|
707
|
+
saveState(input.stateFile, {
|
|
708
|
+
tunnelId,
|
|
709
|
+
accountId: input.accountId,
|
|
710
|
+
tunnelName: input.tunnelName,
|
|
711
|
+
zoneId: input.zone.id
|
|
712
|
+
});
|
|
713
|
+
const connectorToken = await fetchConnectorToken(input, tunnelId);
|
|
714
|
+
await setIngressConfig(input, tunnelId);
|
|
715
|
+
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
716
|
+
const hostnames = {};
|
|
717
|
+
for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
|
|
718
|
+
return { tunnelId, connectorToken, hostnames };
|
|
719
|
+
}
|
|
720
|
+
var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
|
|
721
|
+
var init_cf_ingress = __esm({
|
|
722
|
+
"src/server/cf-ingress.ts"() {
|
|
723
|
+
"use strict";
|
|
724
|
+
import_node_fs2 = require("node:fs");
|
|
725
|
+
import_node_path2 = require("node:path");
|
|
726
|
+
CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
727
|
+
CF_ERR_DNS_RECORD_EXISTS = 81053;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// src/server/radar-docker-init-entry.ts
|
|
732
|
+
var radar_docker_init_entry_exports = {};
|
|
733
|
+
__export(radar_docker_init_entry_exports, {
|
|
734
|
+
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
735
|
+
spawnServiceGroup: () => spawnServiceGroup
|
|
736
|
+
});
|
|
737
|
+
function fail(message) {
|
|
738
|
+
console.error(message);
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
function requireEnv(name) {
|
|
742
|
+
const v = process.env[name];
|
|
743
|
+
if (!v) fail(`ERROR: ${name} is required but not set`);
|
|
744
|
+
return v;
|
|
745
|
+
}
|
|
746
|
+
function run2(cmd, args, stdio = "inherit") {
|
|
747
|
+
const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
|
|
748
|
+
return r.status ?? 1;
|
|
749
|
+
}
|
|
750
|
+
async function setupFromCloud() {
|
|
751
|
+
const pat = requireEnv("LS_PAT");
|
|
752
|
+
const orgSlug = requireEnv("LS_ORG_SLUG");
|
|
753
|
+
const projectSlug = requireEnv("LS_PROJECT_SLUG");
|
|
754
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
755
|
+
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
756
|
+
let bundle;
|
|
757
|
+
try {
|
|
758
|
+
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
759
|
+
} catch (err) {
|
|
760
|
+
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
|
|
761
|
+
}
|
|
762
|
+
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
763
|
+
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
764
|
+
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
765
|
+
if (!process.env.GH_TOKEN) {
|
|
766
|
+
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.`);
|
|
767
|
+
}
|
|
768
|
+
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
769
|
+
console.log(`[entrypoint] bundle from cloud: org=${orgSlug} project=${projectSlug} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()} ${cfNote}`);
|
|
770
|
+
return bundle;
|
|
771
|
+
}
|
|
772
|
+
function setupClaudeCredentials() {
|
|
773
|
+
const home = process.env.HOME ?? "/home/launchpod";
|
|
774
|
+
const claudeDir = (0, import_node_path3.join)(home, ".claude");
|
|
775
|
+
(0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
|
|
776
|
+
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
777
|
+
const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
|
|
778
|
+
(0, import_node_fs3.writeFileSync)(credsPath, decoded);
|
|
779
|
+
(0, import_node_fs3.chmodSync)(credsPath, 384);
|
|
780
|
+
const configPath = (0, import_node_path3.join)(home, ".claude.json");
|
|
781
|
+
let cfg = {};
|
|
782
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
783
|
+
try {
|
|
784
|
+
cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
|
|
785
|
+
} catch {
|
|
786
|
+
cfg = {};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
cfg.hasCompletedOnboarding = true;
|
|
790
|
+
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
791
|
+
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
792
|
+
cfg.installMethod = cfg.installMethod ?? "global";
|
|
793
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
794
|
+
(0, import_node_fs3.chmodSync)(configPath, 384);
|
|
795
|
+
}
|
|
796
|
+
function setupGitAndGh() {
|
|
797
|
+
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
798
|
+
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
799
|
+
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
800
|
+
if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
801
|
+
}
|
|
802
|
+
function initWorkspaceIfEmpty() {
|
|
803
|
+
process.chdir("/workspace");
|
|
804
|
+
if ((0, import_node_fs3.existsSync)(".git")) {
|
|
805
|
+
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
console.log("[entrypoint] /workspace is empty \u2014 running launch-kit init");
|
|
809
|
+
const status = run2("launch-kit", [
|
|
810
|
+
"init",
|
|
811
|
+
`--token=${requireEnv("LS_PAT")}`,
|
|
812
|
+
`--org=${requireEnv("LS_ORG_SLUG")}`,
|
|
813
|
+
`--project=${requireEnv("LS_PROJECT_SLUG")}`,
|
|
814
|
+
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
815
|
+
`--dir=/workspace`
|
|
816
|
+
]);
|
|
817
|
+
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
818
|
+
}
|
|
819
|
+
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
820
|
+
const token = bundle.cloudflareToken ?? null;
|
|
821
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
822
|
+
const zones = bundle.cloudflareZones ?? [];
|
|
823
|
+
if (!token && !accountId && zones.length === 0) return null;
|
|
824
|
+
if (!token || !accountId) {
|
|
825
|
+
fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
826
|
+
}
|
|
827
|
+
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
828
|
+
let chosen = null;
|
|
829
|
+
if (baseDomain) {
|
|
830
|
+
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
831
|
+
if (!chosen) {
|
|
832
|
+
fail(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
|
|
833
|
+
}
|
|
834
|
+
} else if (zones.length === 1) {
|
|
835
|
+
chosen = { id: zones[0].id, name: zones[0].name };
|
|
836
|
+
} else {
|
|
837
|
+
fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
838
|
+
}
|
|
839
|
+
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
840
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
|
|
841
|
+
const result = await provisionIngress({
|
|
842
|
+
apiToken: token,
|
|
843
|
+
accountId,
|
|
844
|
+
zone: chosen,
|
|
845
|
+
tunnelName: `launch-kit-${projectSlug}`,
|
|
846
|
+
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
847
|
+
stateFile
|
|
848
|
+
});
|
|
849
|
+
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
850
|
+
console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
function spawnServiceGroup(services) {
|
|
855
|
+
const children = [];
|
|
856
|
+
let shuttingDown = false;
|
|
857
|
+
const killAll = (signal = "SIGTERM") => {
|
|
858
|
+
if (shuttingDown) return;
|
|
859
|
+
shuttingDown = true;
|
|
860
|
+
for (const c2 of children) {
|
|
861
|
+
try {
|
|
862
|
+
c2.proc.kill(signal);
|
|
863
|
+
} catch {
|
|
180
864
|
}
|
|
181
865
|
}
|
|
182
866
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
867
|
+
const prefixStream = (name, stream, sink) => {
|
|
868
|
+
let buf = "";
|
|
869
|
+
stream.setEncoding("utf8");
|
|
870
|
+
stream.on("data", (chunk) => {
|
|
871
|
+
buf += chunk;
|
|
872
|
+
const lines = buf.split("\n");
|
|
873
|
+
buf = lines.pop() ?? "";
|
|
874
|
+
for (const line of lines) sink.write(`[${name}] ${line}
|
|
875
|
+
`);
|
|
876
|
+
});
|
|
877
|
+
stream.on("end", () => {
|
|
878
|
+
if (buf) sink.write(`[${name}] ${buf}
|
|
879
|
+
`);
|
|
880
|
+
});
|
|
881
|
+
};
|
|
882
|
+
const signalHandlers = [];
|
|
883
|
+
const installSignals = () => {
|
|
884
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
885
|
+
const fn = () => {
|
|
886
|
+
console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
|
|
887
|
+
killAll(sig);
|
|
888
|
+
};
|
|
889
|
+
process.on(sig, fn);
|
|
890
|
+
signalHandlers.push({ sig, fn });
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
const removeSignals = () => {
|
|
894
|
+
for (const h of signalHandlers) process.off(h.sig, h.fn);
|
|
895
|
+
signalHandlers.length = 0;
|
|
189
896
|
};
|
|
897
|
+
return new Promise((resolve3, reject) => {
|
|
898
|
+
let exitedCount = 0;
|
|
899
|
+
let firstFailure = null;
|
|
900
|
+
for (const spec of services) {
|
|
901
|
+
const args = [...spec.args, "--port", String(spec.port)];
|
|
902
|
+
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
903
|
+
const proc = (0, import_node_child_process2.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
904
|
+
children.push({ spec, proc });
|
|
905
|
+
if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
|
|
906
|
+
if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
|
|
907
|
+
proc.on("exit", (code, signal) => {
|
|
908
|
+
exitedCount += 1;
|
|
909
|
+
const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
|
|
910
|
+
if (!shuttingDown && code !== 0) {
|
|
911
|
+
console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
|
|
912
|
+
if (!firstFailure) firstFailure = { name: spec.name, code, signal };
|
|
913
|
+
killAll();
|
|
914
|
+
} else {
|
|
915
|
+
console.log(`[entrypoint] ${label}`);
|
|
916
|
+
}
|
|
917
|
+
if (exitedCount === children.length) {
|
|
918
|
+
if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
|
|
919
|
+
else resolve3();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
proc.on("error", (err) => {
|
|
923
|
+
console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
|
|
924
|
+
if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
|
|
925
|
+
killAll();
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
installSignals();
|
|
929
|
+
}).finally(removeSignals);
|
|
190
930
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
931
|
+
async function main() {
|
|
932
|
+
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
933
|
+
const bundle = await setupFromCloud();
|
|
934
|
+
setupClaudeCredentials();
|
|
935
|
+
setupGitAndGh();
|
|
936
|
+
initWorkspaceIfEmpty();
|
|
937
|
+
let services;
|
|
194
938
|
try {
|
|
195
|
-
|
|
939
|
+
services = resolveServices();
|
|
196
940
|
} catch (err) {
|
|
197
|
-
|
|
941
|
+
fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
942
|
+
}
|
|
943
|
+
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
944
|
+
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
945
|
+
if (ingress) {
|
|
946
|
+
process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
|
|
947
|
+
const radarFqdn = ingress.hostnames.radar;
|
|
948
|
+
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
949
|
+
else if (services.some((s) => s.name === "radar")) {
|
|
950
|
+
fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
951
|
+
}
|
|
952
|
+
} else if (services.length > 1) {
|
|
953
|
+
const first = services[0];
|
|
954
|
+
console.warn(
|
|
955
|
+
`[entrypoint] \u26A0 quick mode \u2014 only the first service "${first.name}" (port ${first.port}) will be exposed via the ephemeral *.trycloudflare.com URL. Other service(s) [${services.slice(1).map((s) => s.name).join(", ")}] will run on localhost inside the container only. Connect a Cloudflare provider in LS and set LAUNCHKIT_CF_BASE_DOMAIN to expose all services with stable subdomains.`
|
|
956
|
+
);
|
|
957
|
+
if (first.name !== "radar") {
|
|
958
|
+
console.warn(`[entrypoint] \u26A0 first service is "${first.name}", not "radar" \u2014 quick tunneling is owned by the radar agent today, so NO external URL will be available.`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
await spawnServiceGroup(services);
|
|
963
|
+
process.exit(0);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
966
|
+
process.exit(1);
|
|
198
967
|
}
|
|
199
968
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
969
|
+
var import_node_child_process2, import_node_fs3, import_node_path3, REQUIRED_ENV;
|
|
970
|
+
var init_radar_docker_init_entry = __esm({
|
|
971
|
+
"src/server/radar-docker-init-entry.ts"() {
|
|
972
|
+
"use strict";
|
|
973
|
+
import_node_child_process2 = require("node:child_process");
|
|
974
|
+
import_node_fs3 = require("node:fs");
|
|
975
|
+
import_node_path3 = require("node:path");
|
|
976
|
+
init_mcp();
|
|
977
|
+
init_launch_kit_services();
|
|
978
|
+
init_cf_ingress();
|
|
979
|
+
REQUIRED_ENV = [
|
|
980
|
+
"CLAUDE_CREDENTIALS_B64",
|
|
981
|
+
"LS_PAT",
|
|
982
|
+
"LS_ORG_SLUG",
|
|
983
|
+
"LS_PROJECT_SLUG"
|
|
984
|
+
];
|
|
985
|
+
if (!process.env.VITEST) {
|
|
986
|
+
main().catch((err) => {
|
|
987
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
});
|
|
207
990
|
}
|
|
208
991
|
}
|
|
209
|
-
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// src/server/init-entry.ts
|
|
995
|
+
var import_node_child_process3 = require("node:child_process");
|
|
996
|
+
var crypto = __toESM(require("node:crypto"));
|
|
997
|
+
var fs5 = __toESM(require("node:fs"));
|
|
998
|
+
var import_node_http3 = require("node:http");
|
|
999
|
+
var import_node_https3 = require("node:https");
|
|
1000
|
+
var path5 = __toESM(require("node:path"));
|
|
1001
|
+
var readline = __toESM(require("node:readline"));
|
|
1002
|
+
var import_node_url2 = require("node:url");
|
|
1003
|
+
init_cred_shape();
|
|
1004
|
+
|
|
1005
|
+
// src/server/git-bot-config.ts
|
|
1006
|
+
var import_node_child_process = require("node:child_process");
|
|
1007
|
+
function run(cmd, args, stdio = "inherit") {
|
|
1008
|
+
return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
|
|
1009
|
+
}
|
|
1010
|
+
function configureGitForBot(identity) {
|
|
1011
|
+
if (process.env.GH_TOKEN) {
|
|
1012
|
+
run("gh", ["auth", "setup-git"]);
|
|
1013
|
+
}
|
|
1014
|
+
run("git", ["config", "--global", "user.name", identity.name]);
|
|
1015
|
+
run("git", ["config", "--global", "user.email", identity.email]);
|
|
1016
|
+
run("git", ["config", "--global", "init.defaultBranch", "main"]);
|
|
1017
|
+
run("git", ["config", "--global", "pull.rebase", "false"]);
|
|
1018
|
+
}
|
|
1019
|
+
function parseGitIdentityFlag(value, flagName = "--git-identity") {
|
|
1020
|
+
const m = value.match(/^\s*(.+?)\s*<\s*([^>]+?)\s*>\s*$/);
|
|
1021
|
+
if (!m) {
|
|
1022
|
+
throw new Error(`${flagName} must be in the form "Name <email>" (got: ${value})`);
|
|
1023
|
+
}
|
|
1024
|
+
return { name: m[1], email: m[2] };
|
|
210
1025
|
}
|
|
211
1026
|
|
|
212
1027
|
// src/server/cred-recovery.ts
|
|
213
1028
|
var fs2 = __toESM(require("node:fs"));
|
|
214
1029
|
var path2 = __toESM(require("node:path"));
|
|
1030
|
+
init_cred_shape();
|
|
215
1031
|
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
216
1032
|
function migrateLegacyCredFile(targetDir, opts) {
|
|
217
1033
|
const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
@@ -292,6 +1108,7 @@ function recoverCred(targetDir, opts) {
|
|
|
292
1108
|
}
|
|
293
1109
|
|
|
294
1110
|
// src/server/init-entry.ts
|
|
1111
|
+
init_secrets_pull();
|
|
295
1112
|
init_statusline_install();
|
|
296
1113
|
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
297
1114
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
@@ -349,8 +1166,8 @@ Wired in Claude Code (.mcp.json):
|
|
|
349
1166
|
launch-recall \u2014 restore deleted/modified files from shadow git
|
|
350
1167
|
|
|
351
1168
|
Other tools (run on demand via npx):
|
|
352
|
-
npx launch-
|
|
353
|
-
npx launch-
|
|
1169
|
+
npx launch-radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
|
|
1170
|
+
npx launch-sequencer \u2014 full pipeline UI (separate sequencer login)
|
|
354
1171
|
npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
|
|
355
1172
|
in-browser monitor. Paste the printed URL into
|
|
356
1173
|
the beacon debug panel; events stream to
|
|
@@ -363,12 +1180,12 @@ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
|
|
|
363
1180
|
`;
|
|
364
1181
|
function listEntries(dir, kind) {
|
|
365
1182
|
if (kind === "commands") {
|
|
366
|
-
return
|
|
1183
|
+
return fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path5.join(dir, f) }));
|
|
367
1184
|
}
|
|
368
|
-
return
|
|
1185
|
+
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") }));
|
|
369
1186
|
}
|
|
370
1187
|
function renderEntries(dir, kind) {
|
|
371
|
-
if (!
|
|
1188
|
+
if (!fs5.existsSync(dir)) return `
|
|
372
1189
|
LS slash ${kind}: (scaffold dir not bundled \u2014 this is a packaging bug)
|
|
373
1190
|
`;
|
|
374
1191
|
const entries = listEntries(dir, kind);
|
|
@@ -378,7 +1195,7 @@ LS slash ${kind}: (none defined)
|
|
|
378
1195
|
const names = entries.map((e) => `/${PLUGIN_ID}:${e.name}`);
|
|
379
1196
|
const colWidth = Math.max(26, ...names.map((n) => n.length + 2));
|
|
380
1197
|
const lines = entries.map((entry, i) => {
|
|
381
|
-
const text =
|
|
1198
|
+
const text = fs5.readFileSync(entry.file, "utf-8");
|
|
382
1199
|
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
383
1200
|
const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
384
1201
|
return ` ${names[i].padEnd(colWidth)} \u2014 ${desc}`;
|
|
@@ -389,8 +1206,8 @@ ${lines.join("\n")}
|
|
|
389
1206
|
`;
|
|
390
1207
|
}
|
|
391
1208
|
function renderLsCommandsSection() {
|
|
392
|
-
const base =
|
|
393
|
-
return renderEntries(
|
|
1209
|
+
const base = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
|
|
1210
|
+
return renderEntries(path5.join(base, "commands"), "commands") + renderEntries(path5.join(base, "skills"), "skills");
|
|
394
1211
|
}
|
|
395
1212
|
function getLaunchKitToolsGuide() {
|
|
396
1213
|
return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
|
|
@@ -418,7 +1235,7 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
418
1235
|
"--guide",
|
|
419
1236
|
"--no-guide"
|
|
420
1237
|
]);
|
|
421
|
-
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
|
|
1238
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
|
|
422
1239
|
function parseArgs(argv) {
|
|
423
1240
|
const args = {
|
|
424
1241
|
token: process.env.LS_PAT ?? null,
|
|
@@ -426,6 +1243,7 @@ function parseArgs(argv) {
|
|
|
426
1243
|
projectSlug: null,
|
|
427
1244
|
serverUrl: DEFAULT_SERVER_URL,
|
|
428
1245
|
targetDir: null,
|
|
1246
|
+
gitIdentity: null,
|
|
429
1247
|
course: null,
|
|
430
1248
|
noInstall: false,
|
|
431
1249
|
noOnboard: false,
|
|
@@ -529,6 +1347,14 @@ function parseArgs(argv) {
|
|
|
529
1347
|
args.course = val;
|
|
530
1348
|
continue;
|
|
531
1349
|
}
|
|
1350
|
+
if (key === "git-identity") {
|
|
1351
|
+
try {
|
|
1352
|
+
args.gitIdentity = parseGitIdentityFlag(val);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
532
1358
|
unknown.push(raw);
|
|
533
1359
|
continue;
|
|
534
1360
|
}
|
|
@@ -541,7 +1367,7 @@ function parseArgs(argv) {
|
|
|
541
1367
|
if (unknown.length > 0) {
|
|
542
1368
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
543
1369
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
544
|
-
|
|
1370
|
+
fail2(`Unknown argument(s): ${unknown.join(" ")}
|
|
545
1371
|
Known boolean flags: ${knownBool}
|
|
546
1372
|
Known key=value flags: ${knownKv}`);
|
|
547
1373
|
}
|
|
@@ -594,6 +1420,12 @@ Subcommands:
|
|
|
594
1420
|
init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
|
|
595
1421
|
refresh Re-apply scaffolds + MCP entries in an already-initialized project
|
|
596
1422
|
(no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
|
|
1423
|
+
setup-git Configure git identity + gh credential helper in one
|
|
1424
|
+
shot. Use in containers / CI where init isn't needed.
|
|
1425
|
+
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
1426
|
+
secrets pull Fetch decrypted secrets from cloud LS and write a .env file.
|
|
1427
|
+
Env resolved from --env \u2192 $LS_ENV \u2192 server-side project default \u2192
|
|
1428
|
+
single-env auto-pick. See \`launch-kit secrets --help\`.
|
|
597
1429
|
statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
|
|
598
1430
|
chips (recall, chart, deck, council) get appended. Refuses to
|
|
599
1431
|
create one if none exists \u2014 additive only.
|
|
@@ -618,6 +1450,12 @@ Options:
|
|
|
618
1450
|
becomes active; re-run with a different --course
|
|
619
1451
|
and --url to add another (e.g. local + staging).
|
|
620
1452
|
Use \`launch-course set <name>\` to switch later.
|
|
1453
|
+
--git-identity="N <e>" Non-interactive git identity for service-account /
|
|
1454
|
+
CI / Docker runs. Configures git user.name, user.email,
|
|
1455
|
+
init.defaultBranch=main, pull.rebase=false; also
|
|
1456
|
+
wires GH_TOKEN into git's credential helper via
|
|
1457
|
+
\`gh auth setup-git\` when GH_TOKEN is set. Example:
|
|
1458
|
+
--git-identity="Radar Bot <radar@launchpod.local>".
|
|
621
1459
|
--no-install Skip dependency install step (also skips the onboard
|
|
622
1460
|
script \u2014 install is its prerequisite).
|
|
623
1461
|
--no-onboard Skip the onboard script even when install runs.
|
|
@@ -695,7 +1533,7 @@ async function prompt(question) {
|
|
|
695
1533
|
resolve3(answer.trim());
|
|
696
1534
|
}));
|
|
697
1535
|
}
|
|
698
|
-
function
|
|
1536
|
+
function fail2(msg) {
|
|
699
1537
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
700
1538
|
process.exit(1);
|
|
701
1539
|
}
|
|
@@ -711,14 +1549,14 @@ function dryNote(msg) {
|
|
|
711
1549
|
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
712
1550
|
}
|
|
713
1551
|
function which(bin) {
|
|
714
|
-
const res = (0,
|
|
1552
|
+
const res = (0, import_node_child_process3.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
715
1553
|
if (res.status !== 0) return null;
|
|
716
1554
|
return res.stdout.split(/\r?\n/)[0]?.trim() || null;
|
|
717
1555
|
}
|
|
718
1556
|
function preflight() {
|
|
719
1557
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
720
|
-
if (nodeMajor < 18)
|
|
721
|
-
if (!which("git"))
|
|
1558
|
+
if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
1559
|
+
if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
722
1560
|
const hasGh = which("gh") !== null;
|
|
723
1561
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
724
1562
|
return { hasGh };
|
|
@@ -734,7 +1572,7 @@ var ProjectInfoHttpError = class extends Error {
|
|
|
734
1572
|
};
|
|
735
1573
|
function attemptProjectInfo(args) {
|
|
736
1574
|
return new Promise((resolve3, reject) => {
|
|
737
|
-
const mcpUrl = new
|
|
1575
|
+
const mcpUrl = new import_node_url2.URL("/api/mcp/project", args.serverUrl);
|
|
738
1576
|
const body = JSON.stringify({
|
|
739
1577
|
jsonrpc: "2.0",
|
|
740
1578
|
id: 1,
|
|
@@ -744,7 +1582,7 @@ function attemptProjectInfo(args) {
|
|
|
744
1582
|
arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
|
|
745
1583
|
}
|
|
746
1584
|
});
|
|
747
|
-
const requester = mcpUrl.protocol === "https:" ?
|
|
1585
|
+
const requester = mcpUrl.protocol === "https:" ? import_node_https3.request : import_node_http3.request;
|
|
748
1586
|
const req = requester(
|
|
749
1587
|
{
|
|
750
1588
|
host: mcpUrl.hostname,
|
|
@@ -852,7 +1690,7 @@ async function callProjectInfo(args) {
|
|
|
852
1690
|
throw lastErr;
|
|
853
1691
|
}
|
|
854
1692
|
function gitRemoteUrl(dir) {
|
|
855
|
-
const res = (0,
|
|
1693
|
+
const res = (0, import_node_child_process3.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
856
1694
|
if (res.status !== 0) return null;
|
|
857
1695
|
return res.stdout.trim() || null;
|
|
858
1696
|
}
|
|
@@ -861,18 +1699,18 @@ function normalizeRepoUrl(url) {
|
|
|
861
1699
|
const sshMatch = u.match(/^git@([^:]+):(.+)$/);
|
|
862
1700
|
if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
863
1701
|
try {
|
|
864
|
-
const parsed = new
|
|
1702
|
+
const parsed = new import_node_url2.URL(u);
|
|
865
1703
|
return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
|
|
866
1704
|
} catch {
|
|
867
1705
|
return u;
|
|
868
1706
|
}
|
|
869
1707
|
}
|
|
870
1708
|
function isGitRepo(dir) {
|
|
871
|
-
return
|
|
1709
|
+
return fs5.existsSync(path5.join(dir, ".git"));
|
|
872
1710
|
}
|
|
873
1711
|
function dirIsEmpty(dir) {
|
|
874
|
-
if (!
|
|
875
|
-
return
|
|
1712
|
+
if (!fs5.existsSync(dir)) return true;
|
|
1713
|
+
return fs5.readdirSync(dir).length === 0;
|
|
876
1714
|
}
|
|
877
1715
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
878
1716
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -891,20 +1729,20 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
891
1729
|
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
892
1730
|
return;
|
|
893
1731
|
}
|
|
894
|
-
const res = (0,
|
|
1732
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
895
1733
|
if (res.status !== 0) {
|
|
896
|
-
|
|
1734
|
+
fail2(
|
|
897
1735
|
`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.`
|
|
898
1736
|
);
|
|
899
1737
|
}
|
|
900
|
-
if (!
|
|
901
|
-
|
|
1738
|
+
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1739
|
+
fail2(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
902
1740
|
}
|
|
903
1741
|
ok(`cloned to ${targetDir}`);
|
|
904
1742
|
}
|
|
905
1743
|
function writeConfigFile(targetDir, cfg, courseName) {
|
|
906
1744
|
recoverCred(targetDir, getRecoveryOptions());
|
|
907
|
-
const p =
|
|
1745
|
+
const p = path5.join(targetDir, CONFIG_FILENAME);
|
|
908
1746
|
const existing = readCredFile(targetDir);
|
|
909
1747
|
const isNew = existing === null;
|
|
910
1748
|
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
@@ -923,15 +1761,15 @@ function getRecoveryOptions() {
|
|
|
923
1761
|
return { dryRun: DRY_RUN, log: recoveryLog };
|
|
924
1762
|
}
|
|
925
1763
|
function detectExistingBootstrap(targetDir) {
|
|
926
|
-
if (!
|
|
1764
|
+
if (!fs5.existsSync(path5.join(targetDir, CONFIG_FILENAME))) {
|
|
927
1765
|
return { bootstrapped: false };
|
|
928
1766
|
}
|
|
929
|
-
const mcpPath =
|
|
930
|
-
if (!
|
|
1767
|
+
const mcpPath = path5.join(targetDir, ".mcp.json");
|
|
1768
|
+
if (!fs5.existsSync(mcpPath)) {
|
|
931
1769
|
return { bootstrapped: false };
|
|
932
1770
|
}
|
|
933
1771
|
try {
|
|
934
|
-
const mcp = JSON.parse(
|
|
1772
|
+
const mcp = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
935
1773
|
if (mcp.mcpServers?.["launch-secure"]) {
|
|
936
1774
|
return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
|
|
937
1775
|
}
|
|
@@ -949,13 +1787,7 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
949
1787
|
},
|
|
950
1788
|
"launch-chart": {
|
|
951
1789
|
command: "npx",
|
|
952
|
-
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
|
|
953
|
-
// Tells launch-chart to also start its HTTP UI alongside the MCP, so
|
|
954
|
-
// users can open the chart viewer at localhost:<port> while queries
|
|
955
|
-
// hit the MCP. Without this, the MCP runs passively (queries work,
|
|
956
|
-
// no UI). I4 deep-merge preserves user-added env keys; this default
|
|
957
|
-
// ensures the auto-serve UX ships out of the box.
|
|
958
|
-
env: { LAUNCH_CHART_AUTOSERVE: "1" }
|
|
1790
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
|
|
959
1791
|
},
|
|
960
1792
|
"launch-deck": {
|
|
961
1793
|
command: "npx",
|
|
@@ -988,14 +1820,14 @@ function mergeMcpEntry(existing, ours) {
|
|
|
988
1820
|
return merged;
|
|
989
1821
|
}
|
|
990
1822
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
991
|
-
const p =
|
|
992
|
-
const hadExisting =
|
|
1823
|
+
const p = path5.join(targetDir, ".mcp.json");
|
|
1824
|
+
const hadExisting = fs5.existsSync(p);
|
|
993
1825
|
let existing = {};
|
|
994
1826
|
if (hadExisting) {
|
|
995
1827
|
try {
|
|
996
|
-
existing = JSON.parse(
|
|
1828
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
997
1829
|
} catch (err) {
|
|
998
|
-
|
|
1830
|
+
fail2(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
999
1831
|
}
|
|
1000
1832
|
}
|
|
1001
1833
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -1017,17 +1849,17 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
1017
1849
|
dryNote(`${action} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
1018
1850
|
return { status: "skipped", summary: "(dry-run)" };
|
|
1019
1851
|
}
|
|
1020
|
-
|
|
1852
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1021
1853
|
const verb = hadExisting && existingServerCount > 0 ? "merged" : "wrote";
|
|
1022
1854
|
ok(`${verb === "merged" ? "merged into" : "wrote"} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
1023
1855
|
const total = Object.keys(launchKitEntries).length;
|
|
1024
1856
|
return { status: "ok", summary: `${verb} ${total} entries` };
|
|
1025
1857
|
}
|
|
1026
1858
|
function detectPackageManager(repoDir) {
|
|
1027
|
-
const pkgPath =
|
|
1028
|
-
if (!
|
|
1859
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1860
|
+
if (!fs5.existsSync(pkgPath)) return null;
|
|
1029
1861
|
try {
|
|
1030
|
-
const pkg = JSON.parse(
|
|
1862
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1031
1863
|
if (typeof pkg.packageManager === "string") {
|
|
1032
1864
|
const name = pkg.packageManager.split("@")[0];
|
|
1033
1865
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -1036,7 +1868,7 @@ function detectPackageManager(repoDir) {
|
|
|
1036
1868
|
}
|
|
1037
1869
|
} catch {
|
|
1038
1870
|
}
|
|
1039
|
-
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) =>
|
|
1871
|
+
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs5.existsSync(path5.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
|
|
1040
1872
|
if (matches.length === 1) {
|
|
1041
1873
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
1042
1874
|
}
|
|
@@ -1045,8 +1877,8 @@ function detectPackageManager(repoDir) {
|
|
|
1045
1877
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
1046
1878
|
}
|
|
1047
1879
|
for (const pm of PACKAGE_MANAGERS) {
|
|
1048
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
1049
|
-
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) =>
|
|
1880
|
+
if (pm.workspaceFiles?.some((wf) => fs5.existsSync(path5.join(repoDir, wf)))) {
|
|
1881
|
+
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs5.existsSync(path5.join(repoDir, wf)))})` };
|
|
1050
1882
|
}
|
|
1051
1883
|
}
|
|
1052
1884
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -1055,8 +1887,8 @@ function detectPackageManager(repoDir) {
|
|
|
1055
1887
|
function runInstall(repoDir, detected) {
|
|
1056
1888
|
const { pm } = detected;
|
|
1057
1889
|
if (!which(pm.binary)) {
|
|
1058
|
-
|
|
1059
|
-
`${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 ${
|
|
1890
|
+
fail2(
|
|
1891
|
+
`${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(" ")}`
|
|
1060
1892
|
);
|
|
1061
1893
|
}
|
|
1062
1894
|
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
@@ -1064,24 +1896,24 @@ function runInstall(repoDir, detected) {
|
|
|
1064
1896
|
dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
|
|
1065
1897
|
return;
|
|
1066
1898
|
}
|
|
1067
|
-
const res = (0,
|
|
1899
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
1068
1900
|
if (res.status !== 0) {
|
|
1069
|
-
|
|
1901
|
+
fail2(
|
|
1070
1902
|
`${pm.name} install failed (exit ${res.status}).
|
|
1071
1903
|
|
|
1072
1904
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
1073
|
-
- ${
|
|
1074
|
-
- ${
|
|
1075
|
-
- ${
|
|
1905
|
+
- ${path5.join(repoDir, CONFIG_FILENAME)} (cred file)
|
|
1906
|
+
- ${path5.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
|
|
1907
|
+
- ${path5.join(repoDir, ".gitignore")} (cred line appended)
|
|
1076
1908
|
- clone at ${repoDir}
|
|
1077
1909
|
|
|
1078
1910
|
Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
|
|
1079
1911
|
|
|
1080
1912
|
To retry install only:
|
|
1081
|
-
cd ${
|
|
1913
|
+
cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
|
|
1082
1914
|
|
|
1083
1915
|
To re-run init after fixing the install error:
|
|
1084
|
-
cd ${
|
|
1916
|
+
cd ${path5.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
|
|
1085
1917
|
|
|
1086
1918
|
To fully reset: delete the files listed above and the clone, then re-init.`
|
|
1087
1919
|
);
|
|
@@ -1089,10 +1921,10 @@ To fully reset: delete the files listed above and the clone, then re-init.`
|
|
|
1089
1921
|
ok(`${pm.name} install complete`);
|
|
1090
1922
|
}
|
|
1091
1923
|
function hasOnboardScript(repoDir) {
|
|
1092
|
-
const pkgPath =
|
|
1093
|
-
if (!
|
|
1924
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1925
|
+
if (!fs5.existsSync(pkgPath)) return false;
|
|
1094
1926
|
try {
|
|
1095
|
-
const pkg = JSON.parse(
|
|
1927
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1096
1928
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
1097
1929
|
} catch {
|
|
1098
1930
|
return false;
|
|
@@ -1100,17 +1932,17 @@ function hasOnboardScript(repoDir) {
|
|
|
1100
1932
|
}
|
|
1101
1933
|
function runRecallInit(repoDir) {
|
|
1102
1934
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
1103
|
-
const recallEntry =
|
|
1104
|
-
const useSibling =
|
|
1935
|
+
const recallEntry = path5.resolve(__dirname, "recall-entry.js");
|
|
1936
|
+
const useSibling = fs5.existsSync(recallEntry);
|
|
1105
1937
|
const cmd = useSibling ? process.execPath : "npx";
|
|
1106
1938
|
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
1107
1939
|
if (DRY_RUN) {
|
|
1108
1940
|
dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
|
|
1109
1941
|
return;
|
|
1110
1942
|
}
|
|
1111
|
-
const res = (0,
|
|
1943
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
1112
1944
|
if (res.status !== 0) {
|
|
1113
|
-
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${
|
|
1945
|
+
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`);
|
|
1114
1946
|
return;
|
|
1115
1947
|
}
|
|
1116
1948
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
@@ -1121,17 +1953,17 @@ function runOnboard(repoDir, pm) {
|
|
|
1121
1953
|
dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
|
|
1122
1954
|
return;
|
|
1123
1955
|
}
|
|
1124
|
-
const res = (0,
|
|
1956
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
1125
1957
|
if (res.status !== 0) {
|
|
1126
|
-
|
|
1127
|
-
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${
|
|
1958
|
+
fail2(
|
|
1959
|
+
`${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}`
|
|
1128
1960
|
);
|
|
1129
1961
|
}
|
|
1130
1962
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
1131
1963
|
}
|
|
1132
1964
|
function ensureGitignoreLine(targetDir, line) {
|
|
1133
|
-
const p =
|
|
1134
|
-
let content =
|
|
1965
|
+
const p = path5.join(targetDir, ".gitignore");
|
|
1966
|
+
let content = fs5.existsSync(p) ? fs5.readFileSync(p, "utf-8") : "";
|
|
1135
1967
|
const lines = content.split(/\r?\n/);
|
|
1136
1968
|
if (lines.some((l) => l.trim() === line)) return;
|
|
1137
1969
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
@@ -1141,28 +1973,28 @@ function ensureGitignoreLine(targetDir, line) {
|
|
|
1141
1973
|
dryNote(`would append "${line}" to .gitignore`);
|
|
1142
1974
|
return;
|
|
1143
1975
|
}
|
|
1144
|
-
|
|
1976
|
+
fs5.writeFileSync(p, content, "utf-8");
|
|
1145
1977
|
ok(`appended ${line} to .gitignore`);
|
|
1146
1978
|
}
|
|
1147
1979
|
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
1148
|
-
if (!
|
|
1149
|
-
for (const entry of
|
|
1150
|
-
const srcPath =
|
|
1151
|
-
const destPath =
|
|
1980
|
+
if (!fs5.existsSync(srcDir)) return;
|
|
1981
|
+
for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) {
|
|
1982
|
+
const srcPath = path5.join(srcDir, entry.name);
|
|
1983
|
+
const destPath = path5.join(destDir, entry.name);
|
|
1152
1984
|
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
1153
1985
|
if (entry.isDirectory()) {
|
|
1154
1986
|
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
1155
1987
|
} else if (entry.isFile()) {
|
|
1156
|
-
const existed =
|
|
1988
|
+
const existed = fs5.existsSync(destPath);
|
|
1157
1989
|
if (DRY_RUN) {
|
|
1158
1990
|
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
1159
1991
|
continue;
|
|
1160
1992
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1993
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
1994
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1163
1995
|
try {
|
|
1164
|
-
const srcMode =
|
|
1165
|
-
|
|
1996
|
+
const srcMode = fs5.statSync(srcPath).mode;
|
|
1997
|
+
fs5.chmodSync(destPath, srcMode);
|
|
1166
1998
|
} catch {
|
|
1167
1999
|
}
|
|
1168
2000
|
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
@@ -1170,25 +2002,25 @@ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
|
1170
2002
|
}
|
|
1171
2003
|
}
|
|
1172
2004
|
function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
|
|
1173
|
-
const scaffoldsRoot =
|
|
1174
|
-
if (!
|
|
2005
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
2006
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1175
2007
|
warn(`migrate-safety scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug; main onboarding unaffected`);
|
|
1176
2008
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1177
2009
|
}
|
|
1178
2010
|
const files = [
|
|
1179
2011
|
{
|
|
1180
|
-
src:
|
|
1181
|
-
dest:
|
|
2012
|
+
src: path5.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
2013
|
+
dest: path5.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
1182
2014
|
label: ".github/workflows/backup-on-migration.yml"
|
|
1183
2015
|
},
|
|
1184
2016
|
{
|
|
1185
|
-
src:
|
|
1186
|
-
dest:
|
|
2017
|
+
src: path5.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
2018
|
+
dest: path5.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
1187
2019
|
label: "scripts/migrate-with-backup.sh"
|
|
1188
2020
|
},
|
|
1189
2021
|
{
|
|
1190
|
-
src:
|
|
1191
|
-
dest:
|
|
2022
|
+
src: path5.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
2023
|
+
dest: path5.join(targetDir, "docs", "migrations-runbook.md"),
|
|
1192
2024
|
label: "docs/migrations-runbook.md"
|
|
1193
2025
|
}
|
|
1194
2026
|
];
|
|
@@ -1223,25 +2055,25 @@ function summarizeFileCounts(counts) {
|
|
|
1223
2055
|
}
|
|
1224
2056
|
function hashFile(p) {
|
|
1225
2057
|
try {
|
|
1226
|
-
return crypto.createHash("sha256").update(
|
|
2058
|
+
return crypto.createHash("sha256").update(fs5.readFileSync(p)).digest("hex");
|
|
1227
2059
|
} catch {
|
|
1228
2060
|
return null;
|
|
1229
2061
|
}
|
|
1230
2062
|
}
|
|
1231
2063
|
function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
1232
|
-
if (!
|
|
2064
|
+
if (!fs5.existsSync(srcPath)) {
|
|
1233
2065
|
info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
|
|
1234
2066
|
return "missing-src";
|
|
1235
2067
|
}
|
|
1236
|
-
if (!
|
|
2068
|
+
if (!fs5.existsSync(destPath)) {
|
|
1237
2069
|
if (DRY_RUN) {
|
|
1238
2070
|
dryNote(`would write ${label}`);
|
|
1239
2071
|
return "wrote";
|
|
1240
2072
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
2073
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
2074
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1243
2075
|
try {
|
|
1244
|
-
|
|
2076
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1245
2077
|
} catch {
|
|
1246
2078
|
}
|
|
1247
2079
|
ok(`wrote ${label}`);
|
|
@@ -1259,9 +2091,9 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1259
2091
|
dryNote(`would refresh ${label} (overrides local edits)`);
|
|
1260
2092
|
return "drifted-refreshed";
|
|
1261
2093
|
}
|
|
1262
|
-
|
|
2094
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1263
2095
|
try {
|
|
1264
|
-
|
|
2096
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1265
2097
|
} catch {
|
|
1266
2098
|
}
|
|
1267
2099
|
ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
|
|
@@ -1273,10 +2105,10 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1273
2105
|
var MARKETPLACE_ID = "launch-secure";
|
|
1274
2106
|
var PLUGIN_ID = "kit";
|
|
1275
2107
|
function isDogfoodMarketplace(targetDir) {
|
|
1276
|
-
const p =
|
|
1277
|
-
if (!
|
|
2108
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
2109
|
+
if (!fs5.existsSync(p)) return { isDogfood: false };
|
|
1278
2110
|
try {
|
|
1279
|
-
const settings = JSON.parse(
|
|
2111
|
+
const settings = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1280
2112
|
const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
|
|
1281
2113
|
if (existingPath && existingPath !== "./.claude/marketplace") {
|
|
1282
2114
|
return { isDogfood: true, existingPath };
|
|
@@ -1287,8 +2119,8 @@ function isDogfoodMarketplace(targetDir) {
|
|
|
1287
2119
|
}
|
|
1288
2120
|
}
|
|
1289
2121
|
function scaffoldLsMarketplace(targetDir) {
|
|
1290
|
-
const scaffoldsRoot =
|
|
1291
|
-
if (!
|
|
2122
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
2123
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1292
2124
|
warn(`ls-marketplace scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1293
2125
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1294
2126
|
}
|
|
@@ -1299,7 +2131,7 @@ function scaffoldLsMarketplace(targetDir) {
|
|
|
1299
2131
|
ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
|
|
1300
2132
|
return { status: "ok", summary: `dogfood pointer (${dogfood.existingPath})` };
|
|
1301
2133
|
}
|
|
1302
|
-
const marketplaceRoot =
|
|
2134
|
+
const marketplaceRoot = path5.join(targetDir, ".claude", "marketplace");
|
|
1303
2135
|
info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
|
|
1304
2136
|
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
1305
2137
|
wireLsSettings(targetDir);
|
|
@@ -1307,14 +2139,14 @@ function scaffoldLsMarketplace(targetDir) {
|
|
|
1307
2139
|
return { status: "ok", summary: "marketplace tree + settings refreshed" };
|
|
1308
2140
|
}
|
|
1309
2141
|
function wireLsSettings(targetDir) {
|
|
1310
|
-
const p =
|
|
1311
|
-
const hadExisting =
|
|
2142
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
2143
|
+
const hadExisting = fs5.existsSync(p);
|
|
1312
2144
|
let existing = {};
|
|
1313
2145
|
if (hadExisting) {
|
|
1314
2146
|
try {
|
|
1315
|
-
existing = JSON.parse(
|
|
2147
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1316
2148
|
} catch (err) {
|
|
1317
|
-
|
|
2149
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1318
2150
|
}
|
|
1319
2151
|
}
|
|
1320
2152
|
const merged = { ...existing };
|
|
@@ -1342,15 +2174,15 @@ function wireLsSettings(targetDir) {
|
|
|
1342
2174
|
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
1343
2175
|
return;
|
|
1344
2176
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
2177
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
2178
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1347
2179
|
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
|
|
1348
2180
|
}
|
|
1349
2181
|
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
1350
2182
|
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
1351
2183
|
function scaffoldRecallHook(targetDir) {
|
|
1352
|
-
const scaffoldsRoot =
|
|
1353
|
-
if (!
|
|
2184
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
2185
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1354
2186
|
warn(`recall-hook scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1355
2187
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1356
2188
|
}
|
|
@@ -1361,14 +2193,14 @@ function scaffoldRecallHook(targetDir) {
|
|
|
1361
2193
|
return wired ? { status: "ok", summary: "ensure-recall.sh refreshed + SessionStart hook appended" } : { status: "ok", summary: "ensure-recall.sh refreshed (hook already wired)" };
|
|
1362
2194
|
}
|
|
1363
2195
|
function wireRecallHook(targetDir) {
|
|
1364
|
-
const p =
|
|
1365
|
-
const hadExisting =
|
|
2196
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
2197
|
+
const hadExisting = fs5.existsSync(p);
|
|
1366
2198
|
let existing = {};
|
|
1367
2199
|
if (hadExisting) {
|
|
1368
2200
|
try {
|
|
1369
|
-
existing = JSON.parse(
|
|
2201
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1370
2202
|
} catch (err) {
|
|
1371
|
-
|
|
2203
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1372
2204
|
}
|
|
1373
2205
|
}
|
|
1374
2206
|
const hooks = existing.hooks ?? {};
|
|
@@ -1394,8 +2226,8 @@ function wireRecallHook(targetDir) {
|
|
|
1394
2226
|
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
1395
2227
|
return true;
|
|
1396
2228
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
2229
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
2230
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1399
2231
|
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
1400
2232
|
return true;
|
|
1401
2233
|
}
|
|
@@ -1415,8 +2247,35 @@ function tryActivateStatusline() {
|
|
|
1415
2247
|
}
|
|
1416
2248
|
return null;
|
|
1417
2249
|
}
|
|
1418
|
-
async function
|
|
2250
|
+
async function main2() {
|
|
1419
2251
|
const subcommand = process.argv[2];
|
|
2252
|
+
if (subcommand === "radar-docker-init") {
|
|
2253
|
+
await Promise.resolve().then(() => (init_radar_docker_init_entry(), radar_docker_init_entry_exports));
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (subcommand === "setup-git") {
|
|
2257
|
+
if (process.argv[3] === "--help" || process.argv[3] === "-h") {
|
|
2258
|
+
console.log('usage: launch-kit setup-git --identity="Name <email>"');
|
|
2259
|
+
console.log("");
|
|
2260
|
+
console.log(" Runs `gh auth setup-git` when GH_TOKEN is set, then sets the");
|
|
2261
|
+
console.log(" global git user.name, user.email, init.defaultBranch=main, and");
|
|
2262
|
+
console.log(" pull.rebase=false. Idempotent; safe to re-run.");
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
let identityVal = null;
|
|
2266
|
+
for (const a of process.argv.slice(3)) {
|
|
2267
|
+
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
2268
|
+
else fail2(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
2269
|
+
}
|
|
2270
|
+
if (!identityVal) fail2(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
2271
|
+
try {
|
|
2272
|
+
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
2273
|
+
configureGitForBot(identity);
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
2276
|
+
}
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
1420
2279
|
if (subcommand === "statusline") {
|
|
1421
2280
|
const action = process.argv[3];
|
|
1422
2281
|
if (!action || action === "--help" || action === "-h") {
|
|
@@ -1433,17 +2292,50 @@ async function main() {
|
|
|
1433
2292
|
for (const a of process.argv.slice(4)) {
|
|
1434
2293
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
1435
2294
|
else if (a === "--compact") compactArg = true;
|
|
1436
|
-
else
|
|
2295
|
+
else fail2(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
1437
2296
|
}
|
|
1438
2297
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
1439
2298
|
let res;
|
|
1440
2299
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
1441
2300
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
1442
|
-
else
|
|
2301
|
+
else fail2(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
1443
2302
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1444
2303
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1445
2304
|
return;
|
|
1446
2305
|
}
|
|
2306
|
+
if (subcommand === "secrets") {
|
|
2307
|
+
const action = process.argv[3];
|
|
2308
|
+
if (!action || action === "--help" || action === "-h") {
|
|
2309
|
+
console.log("usage: launch-kit secrets pull [--env=<slug>] [--dir=<path>] [--file=<name>]");
|
|
2310
|
+
console.log("");
|
|
2311
|
+
console.log(" --env=<slug> env to pull from. Overrides $LS_ENV and the project-level default.");
|
|
2312
|
+
console.log(" --dir=<path> project root (default: cwd). Must contain a launch-kit cred file.");
|
|
2313
|
+
console.log(" --file=<name> output file relative to --dir (default: .env.local).");
|
|
2314
|
+
console.log("");
|
|
2315
|
+
console.log(" Resolution order: --env \u2192 $LS_ENV \u2192 server-side default \u2192 single-env auto-pick.");
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (action !== "pull") {
|
|
2319
|
+
fail2(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
2320
|
+
}
|
|
2321
|
+
let envOverride = null;
|
|
2322
|
+
let dirArg = null;
|
|
2323
|
+
let fileArg = ".env.local";
|
|
2324
|
+
for (const a of process.argv.slice(4)) {
|
|
2325
|
+
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
2326
|
+
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
2327
|
+
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
2328
|
+
else fail2(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
2329
|
+
}
|
|
2330
|
+
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
2331
|
+
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
2332
|
+
try {
|
|
2333
|
+
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
2336
|
+
}
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
1447
2339
|
const args = parseArgs(process.argv.slice(2));
|
|
1448
2340
|
if (args.help) {
|
|
1449
2341
|
if (subcommand === "refresh") printRefreshHelp();
|
|
@@ -1451,10 +2343,10 @@ async function main() {
|
|
|
1451
2343
|
return;
|
|
1452
2344
|
}
|
|
1453
2345
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
1454
|
-
|
|
2346
|
+
fail2(`missing subcommand. Usage: launch-kit <init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
1455
2347
|
}
|
|
1456
2348
|
if (subcommand !== "init" && subcommand !== "refresh") {
|
|
1457
|
-
|
|
2349
|
+
fail2(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
1458
2350
|
}
|
|
1459
2351
|
DRY_RUN = args.dryRun;
|
|
1460
2352
|
VERBOSE = args.verbose || DRY_RUN;
|
|
@@ -1469,8 +2361,8 @@ async function main() {
|
|
|
1469
2361
|
}
|
|
1470
2362
|
async function mainRefresh(args) {
|
|
1471
2363
|
const cwd = process.cwd();
|
|
1472
|
-
const targetDir =
|
|
1473
|
-
if (!
|
|
2364
|
+
const targetDir = path5.resolve(args.targetDir ?? cwd);
|
|
2365
|
+
if (!fs5.existsSync(targetDir)) fail2(`target dir does not exist: ${targetDir}`);
|
|
1474
2366
|
let cred;
|
|
1475
2367
|
let source;
|
|
1476
2368
|
try {
|
|
@@ -1478,7 +2370,7 @@ async function mainRefresh(args) {
|
|
|
1478
2370
|
cred = recovery.cred;
|
|
1479
2371
|
source = recovery.source;
|
|
1480
2372
|
} catch (err) {
|
|
1481
|
-
|
|
2373
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1482
2374
|
}
|
|
1483
2375
|
if (cred && source === "mcp") {
|
|
1484
2376
|
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
@@ -1487,24 +2379,24 @@ async function mainRefresh(args) {
|
|
|
1487
2379
|
if (DRY_RUN) {
|
|
1488
2380
|
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
1489
2381
|
} else {
|
|
1490
|
-
writeJsonAtomic(
|
|
2382
|
+
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
1491
2383
|
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
1492
2384
|
}
|
|
1493
2385
|
}
|
|
1494
2386
|
if (!cred) {
|
|
1495
|
-
|
|
1496
|
-
`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=${
|
|
2387
|
+
fail2(
|
|
2388
|
+
`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.`
|
|
1497
2389
|
);
|
|
1498
2390
|
}
|
|
1499
2391
|
const nested = toNested(cred);
|
|
1500
|
-
if (!nested)
|
|
2392
|
+
if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
1501
2393
|
const active = nested.profiles[nested.active];
|
|
1502
|
-
if (!active)
|
|
2394
|
+
if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
1503
2395
|
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
1504
2396
|
header("launch-kit refresh", [
|
|
1505
2397
|
["course", nested.active],
|
|
1506
2398
|
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
1507
|
-
["dir",
|
|
2399
|
+
["dir", path5.relative(cwd, targetDir) || "."]
|
|
1508
2400
|
]);
|
|
1509
2401
|
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
1510
2402
|
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
@@ -1532,8 +2424,8 @@ async function mainRefresh(args) {
|
|
|
1532
2424
|
}
|
|
1533
2425
|
}
|
|
1534
2426
|
async function mainInit(args) {
|
|
1535
|
-
const probeDir =
|
|
1536
|
-
if (!args.force &&
|
|
2427
|
+
const probeDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2428
|
+
if (!args.force && fs5.existsSync(probeDir)) {
|
|
1537
2429
|
const detection = detectExistingBootstrap(probeDir);
|
|
1538
2430
|
if (detection.bootstrapped) {
|
|
1539
2431
|
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
@@ -1542,8 +2434,8 @@ async function mainInit(args) {
|
|
|
1542
2434
|
}
|
|
1543
2435
|
}
|
|
1544
2436
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
1545
|
-
const recoveryDir =
|
|
1546
|
-
if (
|
|
2437
|
+
const recoveryDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2438
|
+
if (fs5.existsSync(recoveryDir)) {
|
|
1547
2439
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
1548
2440
|
const nested = cred ? toNested(cred) : null;
|
|
1549
2441
|
const recovered = nested ? nested.profiles[nested.active] : cred;
|
|
@@ -1570,10 +2462,10 @@ async function mainInit(args) {
|
|
|
1570
2462
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
1571
2463
|
args.token = t || null;
|
|
1572
2464
|
}
|
|
1573
|
-
if (!args.token)
|
|
1574
|
-
if (!/^ls_pat_/.test(args.token))
|
|
1575
|
-
if (!args.orgSlug)
|
|
1576
|
-
if (!args.projectSlug)
|
|
2465
|
+
if (!args.token) fail2("--token (or LS_PAT env) is required.");
|
|
2466
|
+
if (!/^ls_pat_/.test(args.token)) fail2("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
2467
|
+
if (!args.orgSlug) fail2("--org=<orgSlug> is required.");
|
|
2468
|
+
if (!args.projectSlug) fail2("--project=<projectSlug> is required.");
|
|
1577
2469
|
header("launch-kit init", [
|
|
1578
2470
|
["org", args.orgSlug],
|
|
1579
2471
|
["project", args.projectSlug],
|
|
@@ -1581,39 +2473,43 @@ async function mainInit(args) {
|
|
|
1581
2473
|
]);
|
|
1582
2474
|
const { hasGh } = preflight();
|
|
1583
2475
|
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)"}` });
|
|
2476
|
+
if (args.gitIdentity) {
|
|
2477
|
+
configureGitForBot(args.gitIdentity);
|
|
2478
|
+
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 gh credential helper wired" : ""}` });
|
|
2479
|
+
}
|
|
1584
2480
|
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
1585
2481
|
let resolved;
|
|
1586
2482
|
try {
|
|
1587
2483
|
resolved = await callProjectInfo(args);
|
|
1588
2484
|
} catch (err) {
|
|
1589
|
-
|
|
2485
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1590
2486
|
}
|
|
1591
2487
|
ok(`resolved "${resolved.projectName}"`);
|
|
1592
2488
|
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
1593
2489
|
if (!resolved.repositoryUrl) {
|
|
1594
|
-
|
|
2490
|
+
fail2(
|
|
1595
2491
|
`Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
|
|
1596
2492
|
);
|
|
1597
2493
|
}
|
|
1598
2494
|
const repoUrl = resolved.repositoryUrl;
|
|
1599
2495
|
const cwd = process.cwd();
|
|
1600
|
-
const targetDir =
|
|
2496
|
+
const targetDir = path5.resolve(args.targetDir ?? path5.join(cwd, resolved.projectSlug));
|
|
1601
2497
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
1602
2498
|
let skipClone = false;
|
|
1603
|
-
if (
|
|
2499
|
+
if (fs5.existsSync(targetDir)) {
|
|
1604
2500
|
if (isGitRepo(targetDir)) {
|
|
1605
2501
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
1606
2502
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
1607
2503
|
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
|
|
1608
2504
|
skipClone = true;
|
|
1609
2505
|
} else {
|
|
1610
|
-
|
|
2506
|
+
fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
1611
2507
|
}
|
|
1612
2508
|
} else if (!dirIsEmpty(targetDir)) {
|
|
1613
|
-
|
|
2509
|
+
fail2(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
1614
2510
|
}
|
|
1615
2511
|
}
|
|
1616
|
-
const relTarget =
|
|
2512
|
+
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
1617
2513
|
if (!skipClone) {
|
|
1618
2514
|
section(`Cloning ${repoUrl}`);
|
|
1619
2515
|
cloneRepo(repoUrl, targetDir, hasGh);
|
|
@@ -1643,10 +2539,28 @@ async function mainInit(args) {
|
|
|
1643
2539
|
section(`Installing dependencies (${detected.pm.name})`);
|
|
1644
2540
|
runInstall(targetDir, detected);
|
|
1645
2541
|
phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
|
|
1646
|
-
if (!args.noOnboard
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2542
|
+
if (!args.noOnboard) {
|
|
2543
|
+
if (hasOnboardScript(targetDir)) {
|
|
2544
|
+
section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
|
|
2545
|
+
runOnboard(targetDir, detected.pm);
|
|
2546
|
+
phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
|
|
2547
|
+
} else {
|
|
2548
|
+
section("Pulling environment secrets");
|
|
2549
|
+
info("running launch-kit secrets pull \u2026");
|
|
2550
|
+
if (DRY_RUN) {
|
|
2551
|
+
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2552
|
+
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2553
|
+
} else {
|
|
2554
|
+
try {
|
|
2555
|
+
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2556
|
+
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2559
|
+
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2560
|
+
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
1650
2564
|
}
|
|
1651
2565
|
}
|
|
1652
2566
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
@@ -1686,7 +2600,7 @@ async function mainInit(args) {
|
|
|
1686
2600
|
]);
|
|
1687
2601
|
if (showGuide) console.log(getLaunchKitToolsGuide());
|
|
1688
2602
|
}
|
|
1689
|
-
|
|
2603
|
+
main2().catch((err) => {
|
|
1690
2604
|
console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
1691
2605
|
process.exit(1);
|
|
1692
2606
|
});
|