@launchsecure/launch-kit 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/cli.js +91 -13
- package/dist/server/council-entry.js +0 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +740 -221
- package/dist/server/radar-docker-init-entry.js +239 -0
- package/dist/server/radar-entrypoint-entry.js +99 -0
- package/dist/server/radar-teardown-entry.js +477 -0
- package/dist/server/recall-entry.js +4 -1
- package/package.json +22 -23
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +274 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
- package/scaffolds/statusline/statusline-mcp.sh +82 -2
- package/scaffolds/statusline/statusline-wrapper.sh +8 -1
- package/dist/chart-client/assets/index-CDIhdgWg.css +0 -1
- package/dist/client/assets/index-CfW4n40I.css +0 -32
- package/dist/council-client/assets/index-CZim6x1u.css +0 -1
- package/dist/deck-client/assets/channel-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,313 @@ function deactivateStatusline() {
|
|
|
115
326
|
writeSettings(restored);
|
|
116
327
|
for (const p of [WRAPPER_PATH, CHIP_PATH]) {
|
|
117
328
|
try {
|
|
118
|
-
|
|
329
|
+
fs4.unlinkSync(p);
|
|
119
330
|
} catch {
|
|
120
331
|
}
|
|
121
332
|
}
|
|
122
333
|
return { ok: true, outcome: "deactivated", message: "restored original statusLine.command" };
|
|
123
334
|
}
|
|
124
|
-
var
|
|
335
|
+
var fs4, path4, import_node_os, LK_DIR, WRAPPER_PATH, CHIP_PATH, SETTINGS_PATH, ORIGINAL_KEY;
|
|
125
336
|
var init_statusline_install = __esm({
|
|
126
337
|
"src/server/statusline-install.ts"() {
|
|
127
338
|
"use strict";
|
|
128
|
-
|
|
129
|
-
|
|
339
|
+
fs4 = __toESM(require("node:fs"));
|
|
340
|
+
path4 = __toESM(require("node:path"));
|
|
130
341
|
import_node_os = require("node:os");
|
|
131
|
-
LK_DIR =
|
|
132
|
-
WRAPPER_PATH =
|
|
133
|
-
CHIP_PATH =
|
|
134
|
-
SETTINGS_PATH =
|
|
342
|
+
LK_DIR = path4.join((0, import_node_os.homedir)(), ".launchsecure");
|
|
343
|
+
WRAPPER_PATH = path4.join(LK_DIR, "statusline-wrapper.sh");
|
|
344
|
+
CHIP_PATH = path4.join(LK_DIR, "statusline-mcp.sh");
|
|
345
|
+
SETTINGS_PATH = path4.join((0, import_node_os.homedir)(), ".claude", "settings.json");
|
|
135
346
|
ORIGINAL_KEY = "_launchKitStatuslineOriginal";
|
|
136
347
|
}
|
|
137
348
|
});
|
|
138
349
|
|
|
139
|
-
// src/server/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
var CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
153
|
-
function inferCourseName(serverUrl) {
|
|
350
|
+
// src/server/radar/mcp.ts
|
|
351
|
+
function parseBody(text) {
|
|
352
|
+
if (!text) return {};
|
|
353
|
+
if (text.startsWith("event:") || text.includes("\ndata: ")) {
|
|
354
|
+
for (const line of text.split("\n")) {
|
|
355
|
+
if (line.startsWith("data: ")) {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(line.slice(6));
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
154
363
|
try {
|
|
155
|
-
|
|
156
|
-
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
|
|
157
|
-
if (host.includes("staging")) return "staging";
|
|
158
|
-
if (host.endsWith(".vercel.app")) return "prod";
|
|
159
|
-
return host.split(".")[0] || "default";
|
|
364
|
+
return JSON.parse(text);
|
|
160
365
|
} catch {
|
|
161
|
-
return
|
|
366
|
+
return {};
|
|
162
367
|
}
|
|
163
368
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
369
|
+
var import_node_https2, import_node_http2, ProjectMcpClient;
|
|
370
|
+
var init_mcp = __esm({
|
|
371
|
+
"src/server/radar/mcp.ts"() {
|
|
372
|
+
"use strict";
|
|
373
|
+
import_node_https2 = require("node:https");
|
|
374
|
+
import_node_http2 = require("node:http");
|
|
375
|
+
ProjectMcpClient = class {
|
|
376
|
+
constructor(opts) {
|
|
377
|
+
this.initialized = false;
|
|
378
|
+
this.callId = 1;
|
|
379
|
+
this.mcpUrl = new URL("/api/mcp/project", opts.serverUrl);
|
|
380
|
+
this.headers = {
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
"Accept": "application/json, text/event-stream",
|
|
383
|
+
"Authorization": `Bearer ${opts.pat}`
|
|
384
|
+
};
|
|
385
|
+
if (opts.orgSlug) this.headers["X-Org-Slug"] = opts.orgSlug;
|
|
386
|
+
if (opts.projectSlug) this.headers["X-Project-Slug"] = opts.projectSlug;
|
|
180
387
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
388
|
+
async call(toolName, args) {
|
|
389
|
+
await this.ensureInitialized();
|
|
390
|
+
const body = JSON.stringify({
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
id: this.callId++,
|
|
393
|
+
method: "tools/call",
|
|
394
|
+
params: { name: toolName, arguments: args }
|
|
395
|
+
});
|
|
396
|
+
const resp = await this.send(body);
|
|
397
|
+
const parsed = parseBody(resp.body);
|
|
398
|
+
if (parsed.error) {
|
|
399
|
+
throw new Error(`MCP ${toolName} failed: ${parsed.error.message ?? JSON.stringify(parsed.error)}`);
|
|
400
|
+
}
|
|
401
|
+
const text = parsed.result?.content?.[0]?.text;
|
|
402
|
+
if (!text) return parsed.result;
|
|
403
|
+
if (text.startsWith("\u2500\u2500 Error")) {
|
|
404
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
405
|
+
const reason = lines.find((l) => !l.startsWith("\u2500\u2500")) ?? text;
|
|
406
|
+
throw new Error(`MCP ${toolName} failed: ${reason}`);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(text);
|
|
410
|
+
} catch {
|
|
411
|
+
return text;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async ensureInitialized() {
|
|
415
|
+
if (this.initialized) return;
|
|
416
|
+
const initBody = JSON.stringify({
|
|
417
|
+
jsonrpc: "2.0",
|
|
418
|
+
id: this.callId++,
|
|
419
|
+
method: "initialize",
|
|
420
|
+
params: {
|
|
421
|
+
protocolVersion: "2025-03-26",
|
|
422
|
+
capabilities: {},
|
|
423
|
+
clientInfo: { name: "launchpod-feedback-agent", version: "0.0.1" }
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const initResp = await this.send(initBody);
|
|
427
|
+
if (initResp.sessionId) this.sessionId = initResp.sessionId;
|
|
428
|
+
const notifBody = JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
429
|
+
await this.send(notifBody);
|
|
430
|
+
this.initialized = true;
|
|
431
|
+
}
|
|
432
|
+
send(body) {
|
|
433
|
+
return new Promise((resolve3, reject) => {
|
|
434
|
+
const headers = {
|
|
435
|
+
...this.headers,
|
|
436
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
437
|
+
};
|
|
438
|
+
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
439
|
+
const requester = this.mcpUrl.protocol === "https:" ? import_node_https2.request : import_node_http2.request;
|
|
440
|
+
const req = requester(
|
|
441
|
+
{
|
|
442
|
+
host: this.mcpUrl.hostname,
|
|
443
|
+
port: this.mcpUrl.port || (this.mcpUrl.protocol === "https:" ? 443 : 80),
|
|
444
|
+
path: this.mcpUrl.pathname + this.mcpUrl.search,
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers
|
|
447
|
+
},
|
|
448
|
+
(res) => {
|
|
449
|
+
const chunks = [];
|
|
450
|
+
res.on("data", (c2) => chunks.push(c2));
|
|
451
|
+
res.on("end", () => {
|
|
452
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
453
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
454
|
+
reject(new Error(`MCP HTTP ${res.statusCode}: ${text}`));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const sid = res.headers["mcp-session-id"];
|
|
458
|
+
resolve3({ body: text, sessionId: typeof sid === "string" ? sid : void 0 });
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
req.on("error", reject);
|
|
463
|
+
req.write(body);
|
|
464
|
+
req.end();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/server/radar-docker-init-entry.ts
|
|
472
|
+
var radar_docker_init_entry_exports = {};
|
|
473
|
+
function fail(message) {
|
|
474
|
+
console.error(message);
|
|
475
|
+
process.exit(1);
|
|
190
476
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
477
|
+
function requireEnv(name) {
|
|
478
|
+
const v = process.env[name];
|
|
479
|
+
if (!v) fail(`ERROR: ${name} is required but not set`);
|
|
480
|
+
return v;
|
|
481
|
+
}
|
|
482
|
+
function run2(cmd, args, stdio = "inherit") {
|
|
483
|
+
const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
|
|
484
|
+
return r.status ?? 1;
|
|
485
|
+
}
|
|
486
|
+
async function setupFromCloud() {
|
|
487
|
+
const pat = requireEnv("LS_PAT");
|
|
488
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
489
|
+
const orgSlug = process.env.LS_ORG_SLUG;
|
|
490
|
+
const projectSlug = process.env.LS_PROJECT_SLUG;
|
|
491
|
+
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
492
|
+
let bundle;
|
|
194
493
|
try {
|
|
195
|
-
|
|
494
|
+
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
196
495
|
} catch (err) {
|
|
197
|
-
|
|
496
|
+
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and is scoped to the right org/project.`);
|
|
497
|
+
}
|
|
498
|
+
if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
|
|
499
|
+
if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
|
|
500
|
+
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
501
|
+
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
502
|
+
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
503
|
+
if (!process.env.GH_TOKEN) {
|
|
504
|
+
fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
|
|
505
|
+
}
|
|
506
|
+
console.log(`[entrypoint] bundle from cloud: org=${process.env.LS_ORG_SLUG} project=${process.env.LS_PROJECT_SLUG} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()}`);
|
|
507
|
+
}
|
|
508
|
+
function setupClaudeCredentials() {
|
|
509
|
+
const home = process.env.HOME ?? "/home/launchpod";
|
|
510
|
+
const claudeDir = (0, import_node_path.join)(home, ".claude");
|
|
511
|
+
(0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
|
|
512
|
+
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
513
|
+
const credsPath = (0, import_node_path.join)(claudeDir, ".credentials.json");
|
|
514
|
+
(0, import_node_fs.writeFileSync)(credsPath, decoded);
|
|
515
|
+
(0, import_node_fs.chmodSync)(credsPath, 384);
|
|
516
|
+
const configPath = (0, import_node_path.join)(home, ".claude.json");
|
|
517
|
+
let cfg = {};
|
|
518
|
+
if ((0, import_node_fs.existsSync)(configPath)) {
|
|
519
|
+
try {
|
|
520
|
+
cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
|
|
521
|
+
} catch {
|
|
522
|
+
cfg = {};
|
|
523
|
+
}
|
|
198
524
|
}
|
|
525
|
+
cfg.hasCompletedOnboarding = true;
|
|
526
|
+
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
527
|
+
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
528
|
+
cfg.installMethod = cfg.installMethod ?? "global";
|
|
529
|
+
(0, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
530
|
+
(0, import_node_fs.chmodSync)(configPath, 384);
|
|
531
|
+
}
|
|
532
|
+
function setupGitAndGh() {
|
|
533
|
+
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
534
|
+
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
535
|
+
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
536
|
+
if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
537
|
+
}
|
|
538
|
+
function initWorkspaceIfEmpty() {
|
|
539
|
+
process.chdir("/workspace");
|
|
540
|
+
if ((0, import_node_fs.existsSync)(".git")) {
|
|
541
|
+
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
console.log("[entrypoint] /workspace is empty \u2014 running launch-kit init");
|
|
545
|
+
const status = run2("launch-kit", [
|
|
546
|
+
"init",
|
|
547
|
+
`--token=${requireEnv("LS_PAT")}`,
|
|
548
|
+
`--org=${requireEnv("LS_ORG_SLUG")}`,
|
|
549
|
+
`--project=${requireEnv("LS_PROJECT_SLUG")}`,
|
|
550
|
+
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
551
|
+
`--dir=/workspace`
|
|
552
|
+
]);
|
|
553
|
+
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
199
554
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
555
|
+
function execLaunchPodRadar() {
|
|
556
|
+
console.log("[entrypoint] starting launch-pod radar");
|
|
557
|
+
const child = (0, import_node_child_process2.spawn)("launch-pod", ["radar"], { stdio: "inherit" });
|
|
558
|
+
const forward = (sig) => () => {
|
|
204
559
|
try {
|
|
205
|
-
|
|
560
|
+
child.kill(sig);
|
|
206
561
|
} catch {
|
|
207
562
|
}
|
|
563
|
+
};
|
|
564
|
+
process.on("SIGTERM", forward("SIGTERM"));
|
|
565
|
+
process.on("SIGINT", forward("SIGINT"));
|
|
566
|
+
process.on("SIGHUP", forward("SIGHUP"));
|
|
567
|
+
child.on("exit", (code, signal) => {
|
|
568
|
+
if (signal) process.kill(process.pid, signal);
|
|
569
|
+
else process.exit(code ?? 0);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function main() {
|
|
573
|
+
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
574
|
+
await setupFromCloud();
|
|
575
|
+
setupClaudeCredentials();
|
|
576
|
+
setupGitAndGh();
|
|
577
|
+
initWorkspaceIfEmpty();
|
|
578
|
+
execLaunchPodRadar();
|
|
579
|
+
}
|
|
580
|
+
var import_node_child_process2, import_node_fs, import_node_path, REQUIRED_ENV;
|
|
581
|
+
var init_radar_docker_init_entry = __esm({
|
|
582
|
+
"src/server/radar-docker-init-entry.ts"() {
|
|
583
|
+
"use strict";
|
|
584
|
+
import_node_child_process2 = require("node:child_process");
|
|
585
|
+
import_node_fs = require("node:fs");
|
|
586
|
+
import_node_path = require("node:path");
|
|
587
|
+
init_mcp();
|
|
588
|
+
REQUIRED_ENV = [
|
|
589
|
+
"CLAUDE_CREDENTIALS_B64",
|
|
590
|
+
"LS_PAT"
|
|
591
|
+
];
|
|
592
|
+
main().catch((err) => {
|
|
593
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
});
|
|
208
596
|
}
|
|
209
|
-
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// src/server/init-entry.ts
|
|
600
|
+
var import_node_child_process3 = require("node:child_process");
|
|
601
|
+
var crypto = __toESM(require("node:crypto"));
|
|
602
|
+
var fs5 = __toESM(require("node:fs"));
|
|
603
|
+
var import_node_http3 = require("node:http");
|
|
604
|
+
var import_node_https3 = require("node:https");
|
|
605
|
+
var path5 = __toESM(require("node:path"));
|
|
606
|
+
var readline = __toESM(require("node:readline"));
|
|
607
|
+
var import_node_url2 = require("node:url");
|
|
608
|
+
init_cred_shape();
|
|
609
|
+
|
|
610
|
+
// src/server/git-bot-config.ts
|
|
611
|
+
var import_node_child_process = require("node:child_process");
|
|
612
|
+
function run(cmd, args, stdio = "inherit") {
|
|
613
|
+
return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
|
|
614
|
+
}
|
|
615
|
+
function configureGitForBot(identity) {
|
|
616
|
+
if (process.env.GH_TOKEN) {
|
|
617
|
+
run("gh", ["auth", "setup-git"]);
|
|
618
|
+
}
|
|
619
|
+
run("git", ["config", "--global", "user.name", identity.name]);
|
|
620
|
+
run("git", ["config", "--global", "user.email", identity.email]);
|
|
621
|
+
run("git", ["config", "--global", "init.defaultBranch", "main"]);
|
|
622
|
+
run("git", ["config", "--global", "pull.rebase", "false"]);
|
|
623
|
+
}
|
|
624
|
+
function parseGitIdentityFlag(value, flagName = "--git-identity") {
|
|
625
|
+
const m = value.match(/^\s*(.+?)\s*<\s*([^>]+?)\s*>\s*$/);
|
|
626
|
+
if (!m) {
|
|
627
|
+
throw new Error(`${flagName} must be in the form "Name <email>" (got: ${value})`);
|
|
628
|
+
}
|
|
629
|
+
return { name: m[1], email: m[2] };
|
|
210
630
|
}
|
|
211
631
|
|
|
212
632
|
// src/server/cred-recovery.ts
|
|
213
633
|
var fs2 = __toESM(require("node:fs"));
|
|
214
634
|
var path2 = __toESM(require("node:path"));
|
|
635
|
+
init_cred_shape();
|
|
215
636
|
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
216
637
|
function migrateLegacyCredFile(targetDir, opts) {
|
|
217
638
|
const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
@@ -292,6 +713,7 @@ function recoverCred(targetDir, opts) {
|
|
|
292
713
|
}
|
|
293
714
|
|
|
294
715
|
// src/server/init-entry.ts
|
|
716
|
+
init_secrets_pull();
|
|
295
717
|
init_statusline_install();
|
|
296
718
|
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
297
719
|
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
@@ -363,12 +785,12 @@ var LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL = `
|
|
|
363
785
|
`;
|
|
364
786
|
function listEntries(dir, kind) {
|
|
365
787
|
if (kind === "commands") {
|
|
366
|
-
return
|
|
788
|
+
return fs5.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => ({ name: f.replace(/\.md$/, ""), file: path5.join(dir, f) }));
|
|
367
789
|
}
|
|
368
|
-
return
|
|
790
|
+
return fs5.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs5.existsSync(path5.join(dir, e.name, "SKILL.md"))).map((e) => e.name).sort().map((name) => ({ name, file: path5.join(dir, name, "SKILL.md") }));
|
|
369
791
|
}
|
|
370
792
|
function renderEntries(dir, kind) {
|
|
371
|
-
if (!
|
|
793
|
+
if (!fs5.existsSync(dir)) return `
|
|
372
794
|
LS slash ${kind}: (scaffold dir not bundled \u2014 this is a packaging bug)
|
|
373
795
|
`;
|
|
374
796
|
const entries = listEntries(dir, kind);
|
|
@@ -378,7 +800,7 @@ LS slash ${kind}: (none defined)
|
|
|
378
800
|
const names = entries.map((e) => `/${PLUGIN_ID}:${e.name}`);
|
|
379
801
|
const colWidth = Math.max(26, ...names.map((n) => n.length + 2));
|
|
380
802
|
const lines = entries.map((entry, i) => {
|
|
381
|
-
const text =
|
|
803
|
+
const text = fs5.readFileSync(entry.file, "utf-8");
|
|
382
804
|
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
383
805
|
const desc = fmMatch?.[1].match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
384
806
|
return ` ${names[i].padEnd(colWidth)} \u2014 ${desc}`;
|
|
@@ -389,8 +811,8 @@ ${lines.join("\n")}
|
|
|
389
811
|
`;
|
|
390
812
|
}
|
|
391
813
|
function renderLsCommandsSection() {
|
|
392
|
-
const base =
|
|
393
|
-
return renderEntries(
|
|
814
|
+
const base = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace", "plugins", "kit");
|
|
815
|
+
return renderEntries(path5.join(base, "commands"), "commands") + renderEntries(path5.join(base, "skills"), "skills");
|
|
394
816
|
}
|
|
395
817
|
function getLaunchKitToolsGuide() {
|
|
396
818
|
return `${LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD}${renderLsCommandsSection()}${LAUNCH_KIT_TOOLS_GUIDE_STATIC_TAIL}`;
|
|
@@ -418,7 +840,7 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
418
840
|
"--guide",
|
|
419
841
|
"--no-guide"
|
|
420
842
|
]);
|
|
421
|
-
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course"]);
|
|
843
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
|
|
422
844
|
function parseArgs(argv) {
|
|
423
845
|
const args = {
|
|
424
846
|
token: process.env.LS_PAT ?? null,
|
|
@@ -426,6 +848,7 @@ function parseArgs(argv) {
|
|
|
426
848
|
projectSlug: null,
|
|
427
849
|
serverUrl: DEFAULT_SERVER_URL,
|
|
428
850
|
targetDir: null,
|
|
851
|
+
gitIdentity: null,
|
|
429
852
|
course: null,
|
|
430
853
|
noInstall: false,
|
|
431
854
|
noOnboard: false,
|
|
@@ -529,6 +952,14 @@ function parseArgs(argv) {
|
|
|
529
952
|
args.course = val;
|
|
530
953
|
continue;
|
|
531
954
|
}
|
|
955
|
+
if (key === "git-identity") {
|
|
956
|
+
try {
|
|
957
|
+
args.gitIdentity = parseGitIdentityFlag(val);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
960
|
+
}
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
532
963
|
unknown.push(raw);
|
|
533
964
|
continue;
|
|
534
965
|
}
|
|
@@ -541,7 +972,7 @@ function parseArgs(argv) {
|
|
|
541
972
|
if (unknown.length > 0) {
|
|
542
973
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
543
974
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
544
|
-
|
|
975
|
+
fail2(`Unknown argument(s): ${unknown.join(" ")}
|
|
545
976
|
Known boolean flags: ${knownBool}
|
|
546
977
|
Known key=value flags: ${knownKv}`);
|
|
547
978
|
}
|
|
@@ -594,6 +1025,12 @@ Subcommands:
|
|
|
594
1025
|
init Bootstrap a new project (clone, cred file, MCP, scaffolds, install)
|
|
595
1026
|
refresh Re-apply scaffolds + MCP entries in an already-initialized project
|
|
596
1027
|
(no clone, no install, no PAT prompt \u2014 see \`launch-kit refresh --help\`)
|
|
1028
|
+
setup-git Configure git identity + gh credential helper in one
|
|
1029
|
+
shot. Use in containers / CI where init isn't needed.
|
|
1030
|
+
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
1031
|
+
secrets pull Fetch decrypted secrets from cloud LS and write a .env file.
|
|
1032
|
+
Env resolved from --env \u2192 $LS_ENV \u2192 server-side project default \u2192
|
|
1033
|
+
single-env auto-pick. See \`launch-kit secrets --help\`.
|
|
597
1034
|
statusline activate Wrap ~/.claude/settings.json's statusLine.command so MCP daemon
|
|
598
1035
|
chips (recall, chart, deck, council) get appended. Refuses to
|
|
599
1036
|
create one if none exists \u2014 additive only.
|
|
@@ -618,6 +1055,12 @@ Options:
|
|
|
618
1055
|
becomes active; re-run with a different --course
|
|
619
1056
|
and --url to add another (e.g. local + staging).
|
|
620
1057
|
Use \`launch-course set <name>\` to switch later.
|
|
1058
|
+
--git-identity="N <e>" Non-interactive git identity for service-account /
|
|
1059
|
+
CI / Docker runs. Configures git user.name, user.email,
|
|
1060
|
+
init.defaultBranch=main, pull.rebase=false; also
|
|
1061
|
+
wires GH_TOKEN into git's credential helper via
|
|
1062
|
+
\`gh auth setup-git\` when GH_TOKEN is set. Example:
|
|
1063
|
+
--git-identity="Radar Bot <radar@launchpod.local>".
|
|
621
1064
|
--no-install Skip dependency install step (also skips the onboard
|
|
622
1065
|
script \u2014 install is its prerequisite).
|
|
623
1066
|
--no-onboard Skip the onboard script even when install runs.
|
|
@@ -695,7 +1138,7 @@ async function prompt(question) {
|
|
|
695
1138
|
resolve3(answer.trim());
|
|
696
1139
|
}));
|
|
697
1140
|
}
|
|
698
|
-
function
|
|
1141
|
+
function fail2(msg) {
|
|
699
1142
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
700
1143
|
process.exit(1);
|
|
701
1144
|
}
|
|
@@ -711,14 +1154,14 @@ function dryNote(msg) {
|
|
|
711
1154
|
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
712
1155
|
}
|
|
713
1156
|
function which(bin) {
|
|
714
|
-
const res = (0,
|
|
1157
|
+
const res = (0, import_node_child_process3.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
715
1158
|
if (res.status !== 0) return null;
|
|
716
1159
|
return res.stdout.split(/\r?\n/)[0]?.trim() || null;
|
|
717
1160
|
}
|
|
718
1161
|
function preflight() {
|
|
719
1162
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
720
|
-
if (nodeMajor < 18)
|
|
721
|
-
if (!which("git"))
|
|
1163
|
+
if (nodeMajor < 18) fail2(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
1164
|
+
if (!which("git")) fail2("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
722
1165
|
const hasGh = which("gh") !== null;
|
|
723
1166
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
724
1167
|
return { hasGh };
|
|
@@ -734,7 +1177,7 @@ var ProjectInfoHttpError = class extends Error {
|
|
|
734
1177
|
};
|
|
735
1178
|
function attemptProjectInfo(args) {
|
|
736
1179
|
return new Promise((resolve3, reject) => {
|
|
737
|
-
const mcpUrl = new
|
|
1180
|
+
const mcpUrl = new import_node_url2.URL("/api/mcp/project", args.serverUrl);
|
|
738
1181
|
const body = JSON.stringify({
|
|
739
1182
|
jsonrpc: "2.0",
|
|
740
1183
|
id: 1,
|
|
@@ -744,7 +1187,7 @@ function attemptProjectInfo(args) {
|
|
|
744
1187
|
arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
|
|
745
1188
|
}
|
|
746
1189
|
});
|
|
747
|
-
const requester = mcpUrl.protocol === "https:" ?
|
|
1190
|
+
const requester = mcpUrl.protocol === "https:" ? import_node_https3.request : import_node_http3.request;
|
|
748
1191
|
const req = requester(
|
|
749
1192
|
{
|
|
750
1193
|
host: mcpUrl.hostname,
|
|
@@ -852,7 +1295,7 @@ async function callProjectInfo(args) {
|
|
|
852
1295
|
throw lastErr;
|
|
853
1296
|
}
|
|
854
1297
|
function gitRemoteUrl(dir) {
|
|
855
|
-
const res = (0,
|
|
1298
|
+
const res = (0, import_node_child_process3.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
856
1299
|
if (res.status !== 0) return null;
|
|
857
1300
|
return res.stdout.trim() || null;
|
|
858
1301
|
}
|
|
@@ -861,18 +1304,18 @@ function normalizeRepoUrl(url) {
|
|
|
861
1304
|
const sshMatch = u.match(/^git@([^:]+):(.+)$/);
|
|
862
1305
|
if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
863
1306
|
try {
|
|
864
|
-
const parsed = new
|
|
1307
|
+
const parsed = new import_node_url2.URL(u);
|
|
865
1308
|
return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
|
|
866
1309
|
} catch {
|
|
867
1310
|
return u;
|
|
868
1311
|
}
|
|
869
1312
|
}
|
|
870
1313
|
function isGitRepo(dir) {
|
|
871
|
-
return
|
|
1314
|
+
return fs5.existsSync(path5.join(dir, ".git"));
|
|
872
1315
|
}
|
|
873
1316
|
function dirIsEmpty(dir) {
|
|
874
|
-
if (!
|
|
875
|
-
return
|
|
1317
|
+
if (!fs5.existsSync(dir)) return true;
|
|
1318
|
+
return fs5.readdirSync(dir).length === 0;
|
|
876
1319
|
}
|
|
877
1320
|
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
878
1321
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
@@ -891,20 +1334,20 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
891
1334
|
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
892
1335
|
return;
|
|
893
1336
|
}
|
|
894
|
-
const res = (0,
|
|
1337
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
895
1338
|
if (res.status !== 0) {
|
|
896
|
-
|
|
1339
|
+
fail2(
|
|
897
1340
|
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\` or an SSH key on your GitHub account.`
|
|
898
1341
|
);
|
|
899
1342
|
}
|
|
900
|
-
if (!
|
|
901
|
-
|
|
1343
|
+
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1344
|
+
fail2(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
902
1345
|
}
|
|
903
1346
|
ok(`cloned to ${targetDir}`);
|
|
904
1347
|
}
|
|
905
1348
|
function writeConfigFile(targetDir, cfg, courseName) {
|
|
906
1349
|
recoverCred(targetDir, getRecoveryOptions());
|
|
907
|
-
const p =
|
|
1350
|
+
const p = path5.join(targetDir, CONFIG_FILENAME);
|
|
908
1351
|
const existing = readCredFile(targetDir);
|
|
909
1352
|
const isNew = existing === null;
|
|
910
1353
|
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
@@ -923,15 +1366,15 @@ function getRecoveryOptions() {
|
|
|
923
1366
|
return { dryRun: DRY_RUN, log: recoveryLog };
|
|
924
1367
|
}
|
|
925
1368
|
function detectExistingBootstrap(targetDir) {
|
|
926
|
-
if (!
|
|
1369
|
+
if (!fs5.existsSync(path5.join(targetDir, CONFIG_FILENAME))) {
|
|
927
1370
|
return { bootstrapped: false };
|
|
928
1371
|
}
|
|
929
|
-
const mcpPath =
|
|
930
|
-
if (!
|
|
1372
|
+
const mcpPath = path5.join(targetDir, ".mcp.json");
|
|
1373
|
+
if (!fs5.existsSync(mcpPath)) {
|
|
931
1374
|
return { bootstrapped: false };
|
|
932
1375
|
}
|
|
933
1376
|
try {
|
|
934
|
-
const mcp = JSON.parse(
|
|
1377
|
+
const mcp = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
935
1378
|
if (mcp.mcpServers?.["launch-secure"]) {
|
|
936
1379
|
return { bootstrapped: true, reason: `${CONFIG_FILENAME} present + launch-secure MCP entry in .mcp.json` };
|
|
937
1380
|
}
|
|
@@ -949,13 +1392,7 @@ function buildLaunchKitMcpEntries(cfg) {
|
|
|
949
1392
|
},
|
|
950
1393
|
"launch-chart": {
|
|
951
1394
|
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" }
|
|
1395
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"]
|
|
959
1396
|
},
|
|
960
1397
|
"launch-deck": {
|
|
961
1398
|
command: "npx",
|
|
@@ -988,14 +1425,14 @@ function mergeMcpEntry(existing, ours) {
|
|
|
988
1425
|
return merged;
|
|
989
1426
|
}
|
|
990
1427
|
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
991
|
-
const p =
|
|
992
|
-
const hadExisting =
|
|
1428
|
+
const p = path5.join(targetDir, ".mcp.json");
|
|
1429
|
+
const hadExisting = fs5.existsSync(p);
|
|
993
1430
|
let existing = {};
|
|
994
1431
|
if (hadExisting) {
|
|
995
1432
|
try {
|
|
996
|
-
existing = JSON.parse(
|
|
1433
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
997
1434
|
} catch (err) {
|
|
998
|
-
|
|
1435
|
+
fail2(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
999
1436
|
}
|
|
1000
1437
|
}
|
|
1001
1438
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -1017,17 +1454,17 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
1017
1454
|
dryNote(`${action} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
1018
1455
|
return { status: "skipped", summary: "(dry-run)" };
|
|
1019
1456
|
}
|
|
1020
|
-
|
|
1457
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1021
1458
|
const verb = hadExisting && existingServerCount > 0 ? "merged" : "wrote";
|
|
1022
1459
|
ok(`${verb === "merged" ? "merged into" : "wrote"} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
1023
1460
|
const total = Object.keys(launchKitEntries).length;
|
|
1024
1461
|
return { status: "ok", summary: `${verb} ${total} entries` };
|
|
1025
1462
|
}
|
|
1026
1463
|
function detectPackageManager(repoDir) {
|
|
1027
|
-
const pkgPath =
|
|
1028
|
-
if (!
|
|
1464
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1465
|
+
if (!fs5.existsSync(pkgPath)) return null;
|
|
1029
1466
|
try {
|
|
1030
|
-
const pkg = JSON.parse(
|
|
1467
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1031
1468
|
if (typeof pkg.packageManager === "string") {
|
|
1032
1469
|
const name = pkg.packageManager.split("@")[0];
|
|
1033
1470
|
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
@@ -1036,7 +1473,7 @@ function detectPackageManager(repoDir) {
|
|
|
1036
1473
|
}
|
|
1037
1474
|
} catch {
|
|
1038
1475
|
}
|
|
1039
|
-
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) =>
|
|
1476
|
+
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs5.existsSync(path5.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
|
|
1040
1477
|
if (matches.length === 1) {
|
|
1041
1478
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
1042
1479
|
}
|
|
@@ -1045,8 +1482,8 @@ function detectPackageManager(repoDir) {
|
|
|
1045
1482
|
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
1046
1483
|
}
|
|
1047
1484
|
for (const pm of PACKAGE_MANAGERS) {
|
|
1048
|
-
if (pm.workspaceFiles?.some((wf) =>
|
|
1049
|
-
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) =>
|
|
1485
|
+
if (pm.workspaceFiles?.some((wf) => fs5.existsSync(path5.join(repoDir, wf)))) {
|
|
1486
|
+
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs5.existsSync(path5.join(repoDir, wf)))})` };
|
|
1050
1487
|
}
|
|
1051
1488
|
}
|
|
1052
1489
|
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
@@ -1055,8 +1492,8 @@ function detectPackageManager(repoDir) {
|
|
|
1055
1492
|
function runInstall(repoDir, detected) {
|
|
1056
1493
|
const { pm } = detected;
|
|
1057
1494
|
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 ${
|
|
1495
|
+
fail2(
|
|
1496
|
+
`${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
1060
1497
|
);
|
|
1061
1498
|
}
|
|
1062
1499
|
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
@@ -1064,24 +1501,24 @@ function runInstall(repoDir, detected) {
|
|
|
1064
1501
|
dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
|
|
1065
1502
|
return;
|
|
1066
1503
|
}
|
|
1067
|
-
const res = (0,
|
|
1504
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
1068
1505
|
if (res.status !== 0) {
|
|
1069
|
-
|
|
1506
|
+
fail2(
|
|
1070
1507
|
`${pm.name} install failed (exit ${res.status}).
|
|
1071
1508
|
|
|
1072
1509
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
1073
|
-
- ${
|
|
1074
|
-
- ${
|
|
1075
|
-
- ${
|
|
1510
|
+
- ${path5.join(repoDir, CONFIG_FILENAME)} (cred file)
|
|
1511
|
+
- ${path5.join(repoDir, ".mcp.json")} (5 launch-kit MCP entries merged)
|
|
1512
|
+
- ${path5.join(repoDir, ".gitignore")} (cred line appended)
|
|
1076
1513
|
- clone at ${repoDir}
|
|
1077
1514
|
|
|
1078
1515
|
Scaffolds (recall, migrate-safety, marketplace, recall-hook) were NOT yet written.
|
|
1079
1516
|
|
|
1080
1517
|
To retry install only:
|
|
1081
|
-
cd ${
|
|
1518
|
+
cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}
|
|
1082
1519
|
|
|
1083
1520
|
To re-run init after fixing the install error:
|
|
1084
|
-
cd ${
|
|
1521
|
+
cd ${path5.basename(repoDir)} && npx @launchsecure/launch-kit init --dir=.
|
|
1085
1522
|
|
|
1086
1523
|
To fully reset: delete the files listed above and the clone, then re-init.`
|
|
1087
1524
|
);
|
|
@@ -1089,10 +1526,10 @@ To fully reset: delete the files listed above and the clone, then re-init.`
|
|
|
1089
1526
|
ok(`${pm.name} install complete`);
|
|
1090
1527
|
}
|
|
1091
1528
|
function hasOnboardScript(repoDir) {
|
|
1092
|
-
const pkgPath =
|
|
1093
|
-
if (!
|
|
1529
|
+
const pkgPath = path5.join(repoDir, "package.json");
|
|
1530
|
+
if (!fs5.existsSync(pkgPath)) return false;
|
|
1094
1531
|
try {
|
|
1095
|
-
const pkg = JSON.parse(
|
|
1532
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1096
1533
|
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
1097
1534
|
} catch {
|
|
1098
1535
|
return false;
|
|
@@ -1100,17 +1537,17 @@ function hasOnboardScript(repoDir) {
|
|
|
1100
1537
|
}
|
|
1101
1538
|
function runRecallInit(repoDir) {
|
|
1102
1539
|
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
1103
|
-
const recallEntry =
|
|
1104
|
-
const useSibling =
|
|
1540
|
+
const recallEntry = path5.resolve(__dirname, "recall-entry.js");
|
|
1541
|
+
const useSibling = fs5.existsSync(recallEntry);
|
|
1105
1542
|
const cmd = useSibling ? process.execPath : "npx";
|
|
1106
1543
|
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
1107
1544
|
if (DRY_RUN) {
|
|
1108
1545
|
dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
|
|
1109
1546
|
return;
|
|
1110
1547
|
}
|
|
1111
|
-
const res = (0,
|
|
1548
|
+
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
1112
1549
|
if (res.status !== 0) {
|
|
1113
|
-
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${
|
|
1550
|
+
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path5.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
|
|
1114
1551
|
return;
|
|
1115
1552
|
}
|
|
1116
1553
|
ok(`launch-recall ready (shadow git initialized)`);
|
|
@@ -1121,17 +1558,17 @@ function runOnboard(repoDir, pm) {
|
|
|
1121
1558
|
dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
|
|
1122
1559
|
return;
|
|
1123
1560
|
}
|
|
1124
|
-
const res = (0,
|
|
1561
|
+
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
1125
1562
|
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 ${
|
|
1563
|
+
fail2(
|
|
1564
|
+
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path5.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
|
|
1128
1565
|
);
|
|
1129
1566
|
}
|
|
1130
1567
|
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
1131
1568
|
}
|
|
1132
1569
|
function ensureGitignoreLine(targetDir, line) {
|
|
1133
|
-
const p =
|
|
1134
|
-
let content =
|
|
1570
|
+
const p = path5.join(targetDir, ".gitignore");
|
|
1571
|
+
let content = fs5.existsSync(p) ? fs5.readFileSync(p, "utf-8") : "";
|
|
1135
1572
|
const lines = content.split(/\r?\n/);
|
|
1136
1573
|
if (lines.some((l) => l.trim() === line)) return;
|
|
1137
1574
|
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
@@ -1141,28 +1578,28 @@ function ensureGitignoreLine(targetDir, line) {
|
|
|
1141
1578
|
dryNote(`would append "${line}" to .gitignore`);
|
|
1142
1579
|
return;
|
|
1143
1580
|
}
|
|
1144
|
-
|
|
1581
|
+
fs5.writeFileSync(p, content, "utf-8");
|
|
1145
1582
|
ok(`appended ${line} to .gitignore`);
|
|
1146
1583
|
}
|
|
1147
1584
|
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
1148
|
-
if (!
|
|
1149
|
-
for (const entry of
|
|
1150
|
-
const srcPath =
|
|
1151
|
-
const destPath =
|
|
1585
|
+
if (!fs5.existsSync(srcDir)) return;
|
|
1586
|
+
for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) {
|
|
1587
|
+
const srcPath = path5.join(srcDir, entry.name);
|
|
1588
|
+
const destPath = path5.join(destDir, entry.name);
|
|
1152
1589
|
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
1153
1590
|
if (entry.isDirectory()) {
|
|
1154
1591
|
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
1155
1592
|
} else if (entry.isFile()) {
|
|
1156
|
-
const existed =
|
|
1593
|
+
const existed = fs5.existsSync(destPath);
|
|
1157
1594
|
if (DRY_RUN) {
|
|
1158
1595
|
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
1159
1596
|
continue;
|
|
1160
1597
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1598
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
1599
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1163
1600
|
try {
|
|
1164
|
-
const srcMode =
|
|
1165
|
-
|
|
1601
|
+
const srcMode = fs5.statSync(srcPath).mode;
|
|
1602
|
+
fs5.chmodSync(destPath, srcMode);
|
|
1166
1603
|
} catch {
|
|
1167
1604
|
}
|
|
1168
1605
|
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
@@ -1170,25 +1607,25 @@ function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
|
1170
1607
|
}
|
|
1171
1608
|
}
|
|
1172
1609
|
function scaffoldMigrateSafety(targetDir, refreshScaffolds = false) {
|
|
1173
|
-
const scaffoldsRoot =
|
|
1174
|
-
if (!
|
|
1610
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
1611
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1175
1612
|
warn(`migrate-safety scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug; main onboarding unaffected`);
|
|
1176
1613
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1177
1614
|
}
|
|
1178
1615
|
const files = [
|
|
1179
1616
|
{
|
|
1180
|
-
src:
|
|
1181
|
-
dest:
|
|
1617
|
+
src: path5.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
1618
|
+
dest: path5.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
1182
1619
|
label: ".github/workflows/backup-on-migration.yml"
|
|
1183
1620
|
},
|
|
1184
1621
|
{
|
|
1185
|
-
src:
|
|
1186
|
-
dest:
|
|
1622
|
+
src: path5.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
1623
|
+
dest: path5.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
1187
1624
|
label: "scripts/migrate-with-backup.sh"
|
|
1188
1625
|
},
|
|
1189
1626
|
{
|
|
1190
|
-
src:
|
|
1191
|
-
dest:
|
|
1627
|
+
src: path5.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
1628
|
+
dest: path5.join(targetDir, "docs", "migrations-runbook.md"),
|
|
1192
1629
|
label: "docs/migrations-runbook.md"
|
|
1193
1630
|
}
|
|
1194
1631
|
];
|
|
@@ -1223,25 +1660,25 @@ function summarizeFileCounts(counts) {
|
|
|
1223
1660
|
}
|
|
1224
1661
|
function hashFile(p) {
|
|
1225
1662
|
try {
|
|
1226
|
-
return crypto.createHash("sha256").update(
|
|
1663
|
+
return crypto.createHash("sha256").update(fs5.readFileSync(p)).digest("hex");
|
|
1227
1664
|
} catch {
|
|
1228
1665
|
return null;
|
|
1229
1666
|
}
|
|
1230
1667
|
}
|
|
1231
1668
|
function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
1232
|
-
if (!
|
|
1669
|
+
if (!fs5.existsSync(srcPath)) {
|
|
1233
1670
|
info(`\u26A0 scaffold src missing for ${label} \u2014 skipping (packaging bug)`);
|
|
1234
1671
|
return "missing-src";
|
|
1235
1672
|
}
|
|
1236
|
-
if (!
|
|
1673
|
+
if (!fs5.existsSync(destPath)) {
|
|
1237
1674
|
if (DRY_RUN) {
|
|
1238
1675
|
dryNote(`would write ${label}`);
|
|
1239
1676
|
return "wrote";
|
|
1240
1677
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1678
|
+
fs5.mkdirSync(path5.dirname(destPath), { recursive: true });
|
|
1679
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1243
1680
|
try {
|
|
1244
|
-
|
|
1681
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1245
1682
|
} catch {
|
|
1246
1683
|
}
|
|
1247
1684
|
ok(`wrote ${label}`);
|
|
@@ -1259,9 +1696,9 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1259
1696
|
dryNote(`would refresh ${label} (overrides local edits)`);
|
|
1260
1697
|
return "drifted-refreshed";
|
|
1261
1698
|
}
|
|
1262
|
-
|
|
1699
|
+
fs5.copyFileSync(srcPath, destPath);
|
|
1263
1700
|
try {
|
|
1264
|
-
|
|
1701
|
+
fs5.chmodSync(destPath, fs5.statSync(srcPath).mode);
|
|
1265
1702
|
} catch {
|
|
1266
1703
|
}
|
|
1267
1704
|
ok(`refreshed ${label} (overrode local edits \u2014 drift detected before write)`);
|
|
@@ -1273,10 +1710,10 @@ function copyScaffoldDriftAware(srcPath, destPath, label, refreshScaffolds) {
|
|
|
1273
1710
|
var MARKETPLACE_ID = "launch-secure";
|
|
1274
1711
|
var PLUGIN_ID = "kit";
|
|
1275
1712
|
function isDogfoodMarketplace(targetDir) {
|
|
1276
|
-
const p =
|
|
1277
|
-
if (!
|
|
1713
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1714
|
+
if (!fs5.existsSync(p)) return { isDogfood: false };
|
|
1278
1715
|
try {
|
|
1279
|
-
const settings = JSON.parse(
|
|
1716
|
+
const settings = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1280
1717
|
const existingPath = settings.extraKnownMarketplaces?.[MARKETPLACE_ID]?.source?.path;
|
|
1281
1718
|
if (existingPath && existingPath !== "./.claude/marketplace") {
|
|
1282
1719
|
return { isDogfood: true, existingPath };
|
|
@@ -1287,8 +1724,8 @@ function isDogfoodMarketplace(targetDir) {
|
|
|
1287
1724
|
}
|
|
1288
1725
|
}
|
|
1289
1726
|
function scaffoldLsMarketplace(targetDir) {
|
|
1290
|
-
const scaffoldsRoot =
|
|
1291
|
-
if (!
|
|
1727
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
1728
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1292
1729
|
warn(`ls-marketplace scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1293
1730
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1294
1731
|
}
|
|
@@ -1299,7 +1736,7 @@ function scaffoldLsMarketplace(targetDir) {
|
|
|
1299
1736
|
ok(`launch-secure marketplace (dogfood) \u2014 Claude Code loads commands from ${dogfood.existingPath}`);
|
|
1300
1737
|
return { status: "ok", summary: `dogfood pointer (${dogfood.existingPath})` };
|
|
1301
1738
|
}
|
|
1302
|
-
const marketplaceRoot =
|
|
1739
|
+
const marketplaceRoot = path5.join(targetDir, ".claude", "marketplace");
|
|
1303
1740
|
info("scaffolding launch-secure marketplace (Claude Code /kit: namespace \u2014 refreshes every /kit:* command found in the scaffold) \u2026");
|
|
1304
1741
|
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
1305
1742
|
wireLsSettings(targetDir);
|
|
@@ -1307,14 +1744,14 @@ function scaffoldLsMarketplace(targetDir) {
|
|
|
1307
1744
|
return { status: "ok", summary: "marketplace tree + settings refreshed" };
|
|
1308
1745
|
}
|
|
1309
1746
|
function wireLsSettings(targetDir) {
|
|
1310
|
-
const p =
|
|
1311
|
-
const hadExisting =
|
|
1747
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1748
|
+
const hadExisting = fs5.existsSync(p);
|
|
1312
1749
|
let existing = {};
|
|
1313
1750
|
if (hadExisting) {
|
|
1314
1751
|
try {
|
|
1315
|
-
existing = JSON.parse(
|
|
1752
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1316
1753
|
} catch (err) {
|
|
1317
|
-
|
|
1754
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1318
1755
|
}
|
|
1319
1756
|
}
|
|
1320
1757
|
const merged = { ...existing };
|
|
@@ -1342,15 +1779,15 @@ function wireLsSettings(targetDir) {
|
|
|
1342
1779
|
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
1343
1780
|
return;
|
|
1344
1781
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1782
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
1783
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1347
1784
|
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${PLUGIN_ID}@${MARKETPLACE_ID})`);
|
|
1348
1785
|
}
|
|
1349
1786
|
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
1350
1787
|
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
1351
1788
|
function scaffoldRecallHook(targetDir) {
|
|
1352
|
-
const scaffoldsRoot =
|
|
1353
|
-
if (!
|
|
1789
|
+
const scaffoldsRoot = path5.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
1790
|
+
if (!fs5.existsSync(scaffoldsRoot)) {
|
|
1354
1791
|
warn(`recall-hook scaffolds missing at ${scaffoldsRoot} \u2014 packaging bug`);
|
|
1355
1792
|
return { status: "warn", summary: "scaffolds missing (packaging bug)" };
|
|
1356
1793
|
}
|
|
@@ -1361,14 +1798,14 @@ function scaffoldRecallHook(targetDir) {
|
|
|
1361
1798
|
return wired ? { status: "ok", summary: "ensure-recall.sh refreshed + SessionStart hook appended" } : { status: "ok", summary: "ensure-recall.sh refreshed (hook already wired)" };
|
|
1362
1799
|
}
|
|
1363
1800
|
function wireRecallHook(targetDir) {
|
|
1364
|
-
const p =
|
|
1365
|
-
const hadExisting =
|
|
1801
|
+
const p = path5.join(targetDir, ".claude", "settings.json");
|
|
1802
|
+
const hadExisting = fs5.existsSync(p);
|
|
1366
1803
|
let existing = {};
|
|
1367
1804
|
if (hadExisting) {
|
|
1368
1805
|
try {
|
|
1369
|
-
existing = JSON.parse(
|
|
1806
|
+
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1370
1807
|
} catch (err) {
|
|
1371
|
-
|
|
1808
|
+
fail2(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1372
1809
|
}
|
|
1373
1810
|
}
|
|
1374
1811
|
const hooks = existing.hooks ?? {};
|
|
@@ -1394,8 +1831,8 @@ function wireRecallHook(targetDir) {
|
|
|
1394
1831
|
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
1395
1832
|
return true;
|
|
1396
1833
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1834
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
1835
|
+
fs5.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1399
1836
|
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
1400
1837
|
return true;
|
|
1401
1838
|
}
|
|
@@ -1415,8 +1852,35 @@ function tryActivateStatusline() {
|
|
|
1415
1852
|
}
|
|
1416
1853
|
return null;
|
|
1417
1854
|
}
|
|
1418
|
-
async function
|
|
1855
|
+
async function main2() {
|
|
1419
1856
|
const subcommand = process.argv[2];
|
|
1857
|
+
if (subcommand === "radar-docker-init") {
|
|
1858
|
+
await Promise.resolve().then(() => (init_radar_docker_init_entry(), radar_docker_init_entry_exports));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (subcommand === "setup-git") {
|
|
1862
|
+
if (process.argv[3] === "--help" || process.argv[3] === "-h") {
|
|
1863
|
+
console.log('usage: launch-kit setup-git --identity="Name <email>"');
|
|
1864
|
+
console.log("");
|
|
1865
|
+
console.log(" Runs `gh auth setup-git` when GH_TOKEN is set, then sets the");
|
|
1866
|
+
console.log(" global git user.name, user.email, init.defaultBranch=main, and");
|
|
1867
|
+
console.log(" pull.rebase=false. Idempotent; safe to re-run.");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
let identityVal = null;
|
|
1871
|
+
for (const a of process.argv.slice(3)) {
|
|
1872
|
+
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
1873
|
+
else fail2(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
1874
|
+
}
|
|
1875
|
+
if (!identityVal) fail2(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
1876
|
+
try {
|
|
1877
|
+
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
1878
|
+
configureGitForBot(identity);
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1881
|
+
}
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1420
1884
|
if (subcommand === "statusline") {
|
|
1421
1885
|
const action = process.argv[3];
|
|
1422
1886
|
if (!action || action === "--help" || action === "-h") {
|
|
@@ -1433,17 +1897,50 @@ async function main() {
|
|
|
1433
1897
|
for (const a of process.argv.slice(4)) {
|
|
1434
1898
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
1435
1899
|
else if (a === "--compact") compactArg = true;
|
|
1436
|
-
else
|
|
1900
|
+
else fail2(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
1437
1901
|
}
|
|
1438
1902
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
1439
1903
|
let res;
|
|
1440
1904
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
1441
1905
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
1442
|
-
else
|
|
1906
|
+
else fail2(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
1443
1907
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1444
1908
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
1445
1909
|
return;
|
|
1446
1910
|
}
|
|
1911
|
+
if (subcommand === "secrets") {
|
|
1912
|
+
const action = process.argv[3];
|
|
1913
|
+
if (!action || action === "--help" || action === "-h") {
|
|
1914
|
+
console.log("usage: launch-kit secrets pull [--env=<slug>] [--dir=<path>] [--file=<name>]");
|
|
1915
|
+
console.log("");
|
|
1916
|
+
console.log(" --env=<slug> env to pull from. Overrides $LS_ENV and the project-level default.");
|
|
1917
|
+
console.log(" --dir=<path> project root (default: cwd). Must contain a launch-kit cred file.");
|
|
1918
|
+
console.log(" --file=<name> output file relative to --dir (default: .env.local).");
|
|
1919
|
+
console.log("");
|
|
1920
|
+
console.log(" Resolution order: --env \u2192 $LS_ENV \u2192 server-side default \u2192 single-env auto-pick.");
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (action !== "pull") {
|
|
1924
|
+
fail2(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
1925
|
+
}
|
|
1926
|
+
let envOverride = null;
|
|
1927
|
+
let dirArg = null;
|
|
1928
|
+
let fileArg = ".env.local";
|
|
1929
|
+
for (const a of process.argv.slice(4)) {
|
|
1930
|
+
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
1931
|
+
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
1932
|
+
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
1933
|
+
else fail2(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
1934
|
+
}
|
|
1935
|
+
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
1936
|
+
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
1937
|
+
try {
|
|
1938
|
+
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1447
1944
|
const args = parseArgs(process.argv.slice(2));
|
|
1448
1945
|
if (args.help) {
|
|
1449
1946
|
if (subcommand === "refresh") printRefreshHelp();
|
|
@@ -1451,10 +1948,10 @@ async function main() {
|
|
|
1451
1948
|
return;
|
|
1452
1949
|
}
|
|
1453
1950
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
1454
|
-
|
|
1951
|
+
fail2(`missing subcommand. Usage: launch-kit <init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
1455
1952
|
}
|
|
1456
1953
|
if (subcommand !== "init" && subcommand !== "refresh") {
|
|
1457
|
-
|
|
1954
|
+
fail2(`Unknown subcommand "${subcommand}". Supported: init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
1458
1955
|
}
|
|
1459
1956
|
DRY_RUN = args.dryRun;
|
|
1460
1957
|
VERBOSE = args.verbose || DRY_RUN;
|
|
@@ -1469,8 +1966,8 @@ async function main() {
|
|
|
1469
1966
|
}
|
|
1470
1967
|
async function mainRefresh(args) {
|
|
1471
1968
|
const cwd = process.cwd();
|
|
1472
|
-
const targetDir =
|
|
1473
|
-
if (!
|
|
1969
|
+
const targetDir = path5.resolve(args.targetDir ?? cwd);
|
|
1970
|
+
if (!fs5.existsSync(targetDir)) fail2(`target dir does not exist: ${targetDir}`);
|
|
1474
1971
|
let cred;
|
|
1475
1972
|
let source;
|
|
1476
1973
|
try {
|
|
@@ -1478,7 +1975,7 @@ async function mainRefresh(args) {
|
|
|
1478
1975
|
cred = recovery.cred;
|
|
1479
1976
|
source = recovery.source;
|
|
1480
1977
|
} catch (err) {
|
|
1481
|
-
|
|
1978
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1482
1979
|
}
|
|
1483
1980
|
if (cred && source === "mcp") {
|
|
1484
1981
|
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
@@ -1487,24 +1984,24 @@ async function mainRefresh(args) {
|
|
|
1487
1984
|
if (DRY_RUN) {
|
|
1488
1985
|
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
1489
1986
|
} else {
|
|
1490
|
-
writeJsonAtomic(
|
|
1987
|
+
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
1491
1988
|
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
1492
1989
|
}
|
|
1493
1990
|
}
|
|
1494
1991
|
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=${
|
|
1992
|
+
fail2(
|
|
1993
|
+
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json. Refresh requires an existing cred or a hardcoded launch-secure MCP entry \u2014 run \`npx @launchsecure/launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}\` first.`
|
|
1497
1994
|
);
|
|
1498
1995
|
}
|
|
1499
1996
|
const nested = toNested(cred);
|
|
1500
|
-
if (!nested)
|
|
1997
|
+
if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
1501
1998
|
const active = nested.profiles[nested.active];
|
|
1502
|
-
if (!active)
|
|
1999
|
+
if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
1503
2000
|
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
1504
2001
|
header("launch-kit refresh", [
|
|
1505
2002
|
["course", nested.active],
|
|
1506
2003
|
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
1507
|
-
["dir",
|
|
2004
|
+
["dir", path5.relative(cwd, targetDir) || "."]
|
|
1508
2005
|
]);
|
|
1509
2006
|
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
1510
2007
|
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
@@ -1532,8 +2029,8 @@ async function mainRefresh(args) {
|
|
|
1532
2029
|
}
|
|
1533
2030
|
}
|
|
1534
2031
|
async function mainInit(args) {
|
|
1535
|
-
const probeDir =
|
|
1536
|
-
if (!args.force &&
|
|
2032
|
+
const probeDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2033
|
+
if (!args.force && fs5.existsSync(probeDir)) {
|
|
1537
2034
|
const detection = detectExistingBootstrap(probeDir);
|
|
1538
2035
|
if (detection.bootstrapped) {
|
|
1539
2036
|
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
@@ -1542,8 +2039,8 @@ async function mainInit(args) {
|
|
|
1542
2039
|
}
|
|
1543
2040
|
}
|
|
1544
2041
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
1545
|
-
const recoveryDir =
|
|
1546
|
-
if (
|
|
2042
|
+
const recoveryDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2043
|
+
if (fs5.existsSync(recoveryDir)) {
|
|
1547
2044
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
1548
2045
|
const nested = cred ? toNested(cred) : null;
|
|
1549
2046
|
const recovered = nested ? nested.profiles[nested.active] : cred;
|
|
@@ -1570,10 +2067,10 @@ async function mainInit(args) {
|
|
|
1570
2067
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
1571
2068
|
args.token = t || null;
|
|
1572
2069
|
}
|
|
1573
|
-
if (!args.token)
|
|
1574
|
-
if (!/^ls_pat_/.test(args.token))
|
|
1575
|
-
if (!args.orgSlug)
|
|
1576
|
-
if (!args.projectSlug)
|
|
2070
|
+
if (!args.token) fail2("--token (or LS_PAT env) is required.");
|
|
2071
|
+
if (!/^ls_pat_/.test(args.token)) fail2("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
2072
|
+
if (!args.orgSlug) fail2("--org=<orgSlug> is required.");
|
|
2073
|
+
if (!args.projectSlug) fail2("--project=<projectSlug> is required.");
|
|
1577
2074
|
header("launch-kit init", [
|
|
1578
2075
|
["org", args.orgSlug],
|
|
1579
2076
|
["project", args.projectSlug],
|
|
@@ -1581,39 +2078,43 @@ async function mainInit(args) {
|
|
|
1581
2078
|
]);
|
|
1582
2079
|
const { hasGh } = preflight();
|
|
1583
2080
|
phase("preflight", { status: "ok", summary: `node ${process.versions.node}${hasGh ? " \xB7 git \xB7 gh" : " \xB7 git (gh not found \u2014 will use git for clone)"}` });
|
|
2081
|
+
if (args.gitIdentity) {
|
|
2082
|
+
configureGitForBot(args.gitIdentity);
|
|
2083
|
+
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 gh credential helper wired" : ""}` });
|
|
2084
|
+
}
|
|
1584
2085
|
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
1585
2086
|
let resolved;
|
|
1586
2087
|
try {
|
|
1587
2088
|
resolved = await callProjectInfo(args);
|
|
1588
2089
|
} catch (err) {
|
|
1589
|
-
|
|
2090
|
+
fail2(err instanceof Error ? err.message : String(err));
|
|
1590
2091
|
}
|
|
1591
2092
|
ok(`resolved "${resolved.projectName}"`);
|
|
1592
2093
|
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
1593
2094
|
if (!resolved.repositoryUrl) {
|
|
1594
|
-
|
|
2095
|
+
fail2(
|
|
1595
2096
|
`Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
|
|
1596
2097
|
);
|
|
1597
2098
|
}
|
|
1598
2099
|
const repoUrl = resolved.repositoryUrl;
|
|
1599
2100
|
const cwd = process.cwd();
|
|
1600
|
-
const targetDir =
|
|
2101
|
+
const targetDir = path5.resolve(args.targetDir ?? path5.join(cwd, resolved.projectSlug));
|
|
1601
2102
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
1602
2103
|
let skipClone = false;
|
|
1603
|
-
if (
|
|
2104
|
+
if (fs5.existsSync(targetDir)) {
|
|
1604
2105
|
if (isGitRepo(targetDir)) {
|
|
1605
2106
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
1606
2107
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
1607
2108
|
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
|
|
1608
2109
|
skipClone = true;
|
|
1609
2110
|
} else {
|
|
1610
|
-
|
|
2111
|
+
fail2(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
1611
2112
|
}
|
|
1612
2113
|
} else if (!dirIsEmpty(targetDir)) {
|
|
1613
|
-
|
|
2114
|
+
fail2(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
1614
2115
|
}
|
|
1615
2116
|
}
|
|
1616
|
-
const relTarget =
|
|
2117
|
+
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
1617
2118
|
if (!skipClone) {
|
|
1618
2119
|
section(`Cloning ${repoUrl}`);
|
|
1619
2120
|
cloneRepo(repoUrl, targetDir, hasGh);
|
|
@@ -1643,10 +2144,28 @@ async function mainInit(args) {
|
|
|
1643
2144
|
section(`Installing dependencies (${detected.pm.name})`);
|
|
1644
2145
|
runInstall(targetDir, detected);
|
|
1645
2146
|
phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
|
|
1646
|
-
if (!args.noOnboard
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2147
|
+
if (!args.noOnboard) {
|
|
2148
|
+
if (hasOnboardScript(targetDir)) {
|
|
2149
|
+
section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
|
|
2150
|
+
runOnboard(targetDir, detected.pm);
|
|
2151
|
+
phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
|
|
2152
|
+
} else {
|
|
2153
|
+
section("Pulling environment secrets");
|
|
2154
|
+
info("running launch-kit secrets pull \u2026");
|
|
2155
|
+
if (DRY_RUN) {
|
|
2156
|
+
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2157
|
+
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2158
|
+
} else {
|
|
2159
|
+
try {
|
|
2160
|
+
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2161
|
+
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2164
|
+
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2165
|
+
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
1650
2169
|
}
|
|
1651
2170
|
}
|
|
1652
2171
|
const hasOnboard = hasOnboardScript(targetDir);
|
|
@@ -1686,7 +2205,7 @@ async function mainInit(args) {
|
|
|
1686
2205
|
]);
|
|
1687
2206
|
if (showGuide) console.log(getLaunchKitToolsGuide());
|
|
1688
2207
|
}
|
|
1689
|
-
|
|
2208
|
+
main2().catch((err) => {
|
|
1690
2209
|
console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
1691
2210
|
process.exit(1);
|
|
1692
2211
|
});
|