@launchsecure/launch-kit 0.0.33 → 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/server/chart-serve.js +167 -2
- package/dist/server/cli.js +249 -41
- package/dist/server/council-entry.js +0 -0
- package/dist/server/course-entry.js +1 -1
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph-mcp-entry.js +180 -4
- package/dist/server/init-entry.js +438 -43
- 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 +444 -39
- package/dist/server/radar-entrypoint-entry.js +0 -0
- package/dist/server/radar-teardown-entry.js +23 -22
- package/dist/server/rover-entry.js +20122 -0
- package/package.json +28 -25
- 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 +149 -133
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
3
20
|
|
|
4
21
|
// src/server/radar-docker-init-entry.ts
|
|
22
|
+
var radar_docker_init_entry_exports = {};
|
|
23
|
+
__export(radar_docker_init_entry_exports, {
|
|
24
|
+
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
25
|
+
spawnServiceGroup: () => spawnServiceGroup
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(radar_docker_init_entry_exports);
|
|
5
28
|
var import_node_child_process = require("node:child_process");
|
|
6
|
-
var
|
|
7
|
-
var
|
|
29
|
+
var import_node_fs3 = require("node:fs");
|
|
30
|
+
var import_node_path3 = require("node:path");
|
|
8
31
|
|
|
9
32
|
// src/server/radar/mcp.ts
|
|
10
33
|
var import_node_https = require("node:https");
|
|
@@ -121,10 +144,260 @@ function parseBody(text) {
|
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
146
|
|
|
147
|
+
// src/server/launch-kit-services.ts
|
|
148
|
+
var import_node_fs = require("node:fs");
|
|
149
|
+
var import_node_path = require("node:path");
|
|
150
|
+
var SHORTHANDS = {
|
|
151
|
+
radar: { port: 3517, bin: "launch-radar", args: [] },
|
|
152
|
+
sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
|
|
153
|
+
chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
|
|
154
|
+
deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
|
|
155
|
+
council: { port: 52839, bin: "launch-council", args: ["serve"] }
|
|
156
|
+
};
|
|
157
|
+
var DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
158
|
+
function defaultServices() {
|
|
159
|
+
return [expandShorthand("radar")];
|
|
160
|
+
}
|
|
161
|
+
function expandShorthand(name) {
|
|
162
|
+
const def = SHORTHANDS[name];
|
|
163
|
+
if (!def) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
169
|
+
}
|
|
170
|
+
function coerceEntry(raw, index) {
|
|
171
|
+
if (typeof raw === "string") {
|
|
172
|
+
return expandShorthand(raw);
|
|
173
|
+
}
|
|
174
|
+
if (typeof raw !== "object" || raw === null) {
|
|
175
|
+
throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
|
|
176
|
+
}
|
|
177
|
+
const r = raw;
|
|
178
|
+
if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
|
|
179
|
+
throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
|
|
180
|
+
}
|
|
181
|
+
if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
|
|
182
|
+
throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
name: r.name,
|
|
186
|
+
port: r.port,
|
|
187
|
+
bin: r.bin,
|
|
188
|
+
args: r.args ?? []
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function validate(services) {
|
|
192
|
+
if (services.length === 0) {
|
|
193
|
+
throw new Error(`[launch-kit-services] resolved an empty service list`);
|
|
194
|
+
}
|
|
195
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
196
|
+
const seenPorts = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const s of services) {
|
|
198
|
+
if (!DNS_NAME_RE.test(s.name)) {
|
|
199
|
+
throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
|
|
200
|
+
}
|
|
201
|
+
if (seenNames.has(s.name)) {
|
|
202
|
+
throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
|
|
203
|
+
}
|
|
204
|
+
seenNames.add(s.name);
|
|
205
|
+
if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
|
|
206
|
+
throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
|
|
207
|
+
}
|
|
208
|
+
if (seenPorts.has(s.port)) {
|
|
209
|
+
throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
|
|
210
|
+
}
|
|
211
|
+
seenPorts.add(s.port);
|
|
212
|
+
}
|
|
213
|
+
return services;
|
|
214
|
+
}
|
|
215
|
+
function resolveServices(opts = {}) {
|
|
216
|
+
const env = opts.env ?? process.env;
|
|
217
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
218
|
+
const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
|
|
219
|
+
if (rawEnv) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(rawEnv);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(parsed)) {
|
|
227
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
|
|
228
|
+
}
|
|
229
|
+
return validate(parsed.map(coerceEntry));
|
|
230
|
+
}
|
|
231
|
+
const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
|
|
232
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
if (!Array.isArray(parsed)) {
|
|
240
|
+
throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
|
|
241
|
+
}
|
|
242
|
+
return validate(parsed.map(coerceEntry));
|
|
243
|
+
}
|
|
244
|
+
return validate(defaultServices());
|
|
245
|
+
}
|
|
246
|
+
var SHORTHAND_NAMES = Object.keys(SHORTHANDS);
|
|
247
|
+
|
|
248
|
+
// src/server/cf-ingress.ts
|
|
249
|
+
var import_node_fs2 = require("node:fs");
|
|
250
|
+
var import_node_path2 = require("node:path");
|
|
251
|
+
var CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
252
|
+
var CF_ERR_DNS_RECORD_EXISTS = 81053;
|
|
253
|
+
async function cf(opts) {
|
|
254
|
+
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
255
|
+
method: opts.method,
|
|
256
|
+
headers: {
|
|
257
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
Accept: "application/json",
|
|
260
|
+
"User-Agent": "launch-kit/cf-ingress"
|
|
261
|
+
},
|
|
262
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
263
|
+
signal: AbortSignal.timeout(15e3)
|
|
264
|
+
});
|
|
265
|
+
const text = await res.text();
|
|
266
|
+
let parsed;
|
|
267
|
+
try {
|
|
268
|
+
parsed = text ? JSON.parse(text) : { success: false };
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
|
|
271
|
+
}
|
|
272
|
+
return parsed;
|
|
273
|
+
}
|
|
274
|
+
function isNotFound(env) {
|
|
275
|
+
return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
|
|
276
|
+
}
|
|
277
|
+
function loadState(path) {
|
|
278
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
281
|
+
if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function saveState(path, state) {
|
|
290
|
+
const dir = (0, import_node_path2.dirname)(path);
|
|
291
|
+
if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
292
|
+
(0, import_node_fs2.writeFileSync)(path, JSON.stringify(state, null, 2));
|
|
293
|
+
}
|
|
294
|
+
async function ensureTunnel(input, knownTunnelId) {
|
|
295
|
+
if (knownTunnelId) {
|
|
296
|
+
const got = await cf({
|
|
297
|
+
apiToken: input.apiToken,
|
|
298
|
+
method: "GET",
|
|
299
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
|
|
300
|
+
});
|
|
301
|
+
if (got.success && got.result && !got.result.deleted_at) {
|
|
302
|
+
return knownTunnelId;
|
|
303
|
+
}
|
|
304
|
+
if (!isNotFound(got) && !got.success) {
|
|
305
|
+
throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const created = await cf({
|
|
309
|
+
apiToken: input.apiToken,
|
|
310
|
+
method: "POST",
|
|
311
|
+
path: `/accounts/${input.accountId}/cfd_tunnel`,
|
|
312
|
+
body: { name: input.tunnelName, config_src: "cloudflare" }
|
|
313
|
+
});
|
|
314
|
+
if (!created.success || !created.result) {
|
|
315
|
+
throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
|
|
316
|
+
}
|
|
317
|
+
return created.result.id;
|
|
318
|
+
}
|
|
319
|
+
async function fetchConnectorToken(input, tunnelId) {
|
|
320
|
+
const res = await cf({
|
|
321
|
+
apiToken: input.apiToken,
|
|
322
|
+
method: "GET",
|
|
323
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
|
|
324
|
+
});
|
|
325
|
+
if (!res.success || typeof res.result !== "string") {
|
|
326
|
+
throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
|
|
327
|
+
}
|
|
328
|
+
return res.result;
|
|
329
|
+
}
|
|
330
|
+
async function setIngressConfig(input, tunnelId) {
|
|
331
|
+
const ingress = input.services.map((s) => ({
|
|
332
|
+
hostname: `${s.name}.${input.zone.name}`,
|
|
333
|
+
service: `http://localhost:${s.port}`
|
|
334
|
+
}));
|
|
335
|
+
ingress.push({ service: "http_status:404" });
|
|
336
|
+
const res = await cf({
|
|
337
|
+
apiToken: input.apiToken,
|
|
338
|
+
method: "PUT",
|
|
339
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
340
|
+
body: { config: { ingress } }
|
|
341
|
+
});
|
|
342
|
+
if (!res.success) {
|
|
343
|
+
throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function ensureDnsRecord(input, tunnelId, service) {
|
|
347
|
+
const fqdn = `${service.name}.${input.zone.name}`;
|
|
348
|
+
const target = `${tunnelId}.cfargotunnel.com`;
|
|
349
|
+
const existing = await cf({
|
|
350
|
+
apiToken: input.apiToken,
|
|
351
|
+
method: "GET",
|
|
352
|
+
path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
|
|
353
|
+
});
|
|
354
|
+
if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
|
|
355
|
+
const rec = existing.result[0];
|
|
356
|
+
if (rec.content === target) return;
|
|
357
|
+
const upd = await cf({
|
|
358
|
+
apiToken: input.apiToken,
|
|
359
|
+
method: "PUT",
|
|
360
|
+
path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
|
|
361
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
362
|
+
});
|
|
363
|
+
if (!upd.success) {
|
|
364
|
+
throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const created = await cf({
|
|
369
|
+
apiToken: input.apiToken,
|
|
370
|
+
method: "POST",
|
|
371
|
+
path: `/zones/${input.zone.id}/dns_records`,
|
|
372
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
373
|
+
});
|
|
374
|
+
if (created.success) return;
|
|
375
|
+
if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
|
|
376
|
+
throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
|
|
377
|
+
}
|
|
378
|
+
async function provisionIngress(input) {
|
|
379
|
+
const prior = loadState(input.stateFile);
|
|
380
|
+
const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
|
|
381
|
+
saveState(input.stateFile, {
|
|
382
|
+
tunnelId,
|
|
383
|
+
accountId: input.accountId,
|
|
384
|
+
tunnelName: input.tunnelName,
|
|
385
|
+
zoneId: input.zone.id
|
|
386
|
+
});
|
|
387
|
+
const connectorToken = await fetchConnectorToken(input, tunnelId);
|
|
388
|
+
await setIngressConfig(input, tunnelId);
|
|
389
|
+
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
390
|
+
const hostnames = {};
|
|
391
|
+
for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
|
|
392
|
+
return { tunnelId, connectorToken, hostnames };
|
|
393
|
+
}
|
|
394
|
+
|
|
124
395
|
// src/server/radar-docker-init-entry.ts
|
|
125
396
|
var REQUIRED_ENV = [
|
|
126
397
|
"CLAUDE_CREDENTIALS_B64",
|
|
127
|
-
"LS_PAT"
|
|
398
|
+
"LS_PAT",
|
|
399
|
+
"LS_ORG_SLUG",
|
|
400
|
+
"LS_PROJECT_SLUG"
|
|
128
401
|
];
|
|
129
402
|
function fail(message) {
|
|
130
403
|
console.error(message);
|
|
@@ -141,39 +414,39 @@ function run(cmd, args, stdio = "inherit") {
|
|
|
141
414
|
}
|
|
142
415
|
async function setupFromCloud() {
|
|
143
416
|
const pat = requireEnv("LS_PAT");
|
|
417
|
+
const orgSlug = requireEnv("LS_ORG_SLUG");
|
|
418
|
+
const projectSlug = requireEnv("LS_PROJECT_SLUG");
|
|
144
419
|
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
145
|
-
const orgSlug = process.env.LS_ORG_SLUG;
|
|
146
|
-
const projectSlug = process.env.LS_PROJECT_SLUG;
|
|
147
420
|
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
148
421
|
let bundle;
|
|
149
422
|
try {
|
|
150
423
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
151
424
|
} catch (err) {
|
|
152
|
-
fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and
|
|
425
|
+
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.`);
|
|
153
426
|
}
|
|
154
|
-
if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
|
|
155
|
-
if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
|
|
156
427
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
157
428
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
158
429
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
159
430
|
if (!process.env.GH_TOKEN) {
|
|
160
431
|
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.`);
|
|
161
432
|
}
|
|
162
|
-
|
|
433
|
+
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
434
|
+
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}`);
|
|
435
|
+
return bundle;
|
|
163
436
|
}
|
|
164
437
|
function setupClaudeCredentials() {
|
|
165
438
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
166
|
-
const claudeDir = (0,
|
|
167
|
-
(0,
|
|
439
|
+
const claudeDir = (0, import_node_path3.join)(home, ".claude");
|
|
440
|
+
(0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
|
|
168
441
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
169
|
-
const credsPath = (0,
|
|
170
|
-
(0,
|
|
171
|
-
(0,
|
|
172
|
-
const configPath = (0,
|
|
442
|
+
const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
|
|
443
|
+
(0, import_node_fs3.writeFileSync)(credsPath, decoded);
|
|
444
|
+
(0, import_node_fs3.chmodSync)(credsPath, 384);
|
|
445
|
+
const configPath = (0, import_node_path3.join)(home, ".claude.json");
|
|
173
446
|
let cfg = {};
|
|
174
|
-
if ((0,
|
|
447
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
175
448
|
try {
|
|
176
|
-
cfg = JSON.parse((0,
|
|
449
|
+
cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
|
|
177
450
|
} catch {
|
|
178
451
|
cfg = {};
|
|
179
452
|
}
|
|
@@ -182,8 +455,8 @@ function setupClaudeCredentials() {
|
|
|
182
455
|
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
183
456
|
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
184
457
|
cfg.installMethod = cfg.installMethod ?? "global";
|
|
185
|
-
(0,
|
|
186
|
-
(0,
|
|
458
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
459
|
+
(0, import_node_fs3.chmodSync)(configPath, 384);
|
|
187
460
|
}
|
|
188
461
|
function setupGitAndGh() {
|
|
189
462
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
@@ -193,7 +466,7 @@ function setupGitAndGh() {
|
|
|
193
466
|
}
|
|
194
467
|
function initWorkspaceIfEmpty() {
|
|
195
468
|
process.chdir("/workspace");
|
|
196
|
-
if ((0,
|
|
469
|
+
if ((0, import_node_fs3.existsSync)(".git")) {
|
|
197
470
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
198
471
|
return;
|
|
199
472
|
}
|
|
@@ -208,32 +481,164 @@ function initWorkspaceIfEmpty() {
|
|
|
208
481
|
]);
|
|
209
482
|
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
210
483
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
484
|
+
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
485
|
+
const token = bundle.cloudflareToken ?? null;
|
|
486
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
487
|
+
const zones = bundle.cloudflareZones ?? [];
|
|
488
|
+
if (!token && !accountId && zones.length === 0) return null;
|
|
489
|
+
if (!token || !accountId) {
|
|
490
|
+
fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
491
|
+
}
|
|
492
|
+
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
493
|
+
let chosen = null;
|
|
494
|
+
if (baseDomain) {
|
|
495
|
+
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
496
|
+
if (!chosen) {
|
|
497
|
+
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.`);
|
|
218
498
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
499
|
+
} else if (zones.length === 1) {
|
|
500
|
+
chosen = { id: zones[0].id, name: zones[0].name };
|
|
501
|
+
} else {
|
|
502
|
+
fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
503
|
+
}
|
|
504
|
+
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
505
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
|
|
506
|
+
const result = await provisionIngress({
|
|
507
|
+
apiToken: token,
|
|
508
|
+
accountId,
|
|
509
|
+
zone: chosen,
|
|
510
|
+
tunnelName: `launch-kit-${projectSlug}`,
|
|
511
|
+
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
512
|
+
stateFile
|
|
226
513
|
});
|
|
514
|
+
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
515
|
+
console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
function spawnServiceGroup(services) {
|
|
520
|
+
const children = [];
|
|
521
|
+
let shuttingDown = false;
|
|
522
|
+
const killAll = (signal = "SIGTERM") => {
|
|
523
|
+
if (shuttingDown) return;
|
|
524
|
+
shuttingDown = true;
|
|
525
|
+
for (const c of children) {
|
|
526
|
+
try {
|
|
527
|
+
c.proc.kill(signal);
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const prefixStream = (name, stream, sink) => {
|
|
533
|
+
let buf = "";
|
|
534
|
+
stream.setEncoding("utf8");
|
|
535
|
+
stream.on("data", (chunk) => {
|
|
536
|
+
buf += chunk;
|
|
537
|
+
const lines = buf.split("\n");
|
|
538
|
+
buf = lines.pop() ?? "";
|
|
539
|
+
for (const line of lines) sink.write(`[${name}] ${line}
|
|
540
|
+
`);
|
|
541
|
+
});
|
|
542
|
+
stream.on("end", () => {
|
|
543
|
+
if (buf) sink.write(`[${name}] ${buf}
|
|
544
|
+
`);
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
const signalHandlers = [];
|
|
548
|
+
const installSignals = () => {
|
|
549
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
550
|
+
const fn = () => {
|
|
551
|
+
console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
|
|
552
|
+
killAll(sig);
|
|
553
|
+
};
|
|
554
|
+
process.on(sig, fn);
|
|
555
|
+
signalHandlers.push({ sig, fn });
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const removeSignals = () => {
|
|
559
|
+
for (const h of signalHandlers) process.off(h.sig, h.fn);
|
|
560
|
+
signalHandlers.length = 0;
|
|
561
|
+
};
|
|
562
|
+
return new Promise((resolve, reject) => {
|
|
563
|
+
let exitedCount = 0;
|
|
564
|
+
let firstFailure = null;
|
|
565
|
+
for (const spec of services) {
|
|
566
|
+
const args = [...spec.args, "--port", String(spec.port)];
|
|
567
|
+
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
568
|
+
const proc = (0, import_node_child_process.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
569
|
+
children.push({ spec, proc });
|
|
570
|
+
if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
|
|
571
|
+
if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
|
|
572
|
+
proc.on("exit", (code, signal) => {
|
|
573
|
+
exitedCount += 1;
|
|
574
|
+
const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
|
|
575
|
+
if (!shuttingDown && code !== 0) {
|
|
576
|
+
console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
|
|
577
|
+
if (!firstFailure) firstFailure = { name: spec.name, code, signal };
|
|
578
|
+
killAll();
|
|
579
|
+
} else {
|
|
580
|
+
console.log(`[entrypoint] ${label}`);
|
|
581
|
+
}
|
|
582
|
+
if (exitedCount === children.length) {
|
|
583
|
+
if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
|
|
584
|
+
else resolve();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
proc.on("error", (err) => {
|
|
588
|
+
console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
|
|
589
|
+
if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
|
|
590
|
+
killAll();
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
installSignals();
|
|
594
|
+
}).finally(removeSignals);
|
|
227
595
|
}
|
|
228
596
|
async function main() {
|
|
229
597
|
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
230
|
-
await setupFromCloud();
|
|
598
|
+
const bundle = await setupFromCloud();
|
|
231
599
|
setupClaudeCredentials();
|
|
232
600
|
setupGitAndGh();
|
|
233
601
|
initWorkspaceIfEmpty();
|
|
234
|
-
|
|
602
|
+
let services;
|
|
603
|
+
try {
|
|
604
|
+
services = resolveServices();
|
|
605
|
+
} catch (err) {
|
|
606
|
+
fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
607
|
+
}
|
|
608
|
+
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
609
|
+
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
610
|
+
if (ingress) {
|
|
611
|
+
process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
|
|
612
|
+
const radarFqdn = ingress.hostnames.radar;
|
|
613
|
+
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
614
|
+
else if (services.some((s) => s.name === "radar")) {
|
|
615
|
+
fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
616
|
+
}
|
|
617
|
+
} else if (services.length > 1) {
|
|
618
|
+
const first = services[0];
|
|
619
|
+
console.warn(
|
|
620
|
+
`[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.`
|
|
621
|
+
);
|
|
622
|
+
if (first.name !== "radar") {
|
|
623
|
+
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.`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await spawnServiceGroup(services);
|
|
628
|
+
process.exit(0);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
235
633
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
634
|
+
if (!process.env.VITEST) {
|
|
635
|
+
main().catch((err) => {
|
|
636
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
641
|
+
0 && (module.exports = {
|
|
642
|
+
maybeProvisionIngress,
|
|
643
|
+
spawnServiceGroup
|
|
239
644
|
});
|
|
File without changes
|
|
@@ -145,21 +145,22 @@ function parseBody(text) {
|
|
|
145
145
|
|
|
146
146
|
// src/server/radar-teardown-entry.ts
|
|
147
147
|
var COMPOSE_FILE = (0, import_node_path.resolve)(process.cwd(), "docker-compose.yml");
|
|
148
|
-
var
|
|
148
|
+
var LAUNCH_POD_DIR = (0, import_node_path.dirname)(COMPOSE_FILE);
|
|
149
149
|
var COMPOSE_BASE = ["compose", "-f", COMPOSE_FILE];
|
|
150
|
-
var
|
|
151
|
-
|
|
150
|
+
var COMPOSE_SERVICE = "launch-pod";
|
|
151
|
+
var ENV_PATH = (0, import_node_path.join)(LAUNCH_POD_DIR, ".env");
|
|
152
|
+
function ensureLaunchPodCompose() {
|
|
152
153
|
if (!(0, import_node_fs.existsSync)(COMPOSE_FILE)) {
|
|
153
154
|
console.error(`[teardown] aborting \u2014 no docker-compose.yml at ${COMPOSE_FILE}`);
|
|
154
|
-
console.error(`[teardown] run from packages/cli/docker/
|
|
155
|
+
console.error(`[teardown] run from packages/cli/docker/launch-pod/ (the directory holding the launch-pod compose).`);
|
|
155
156
|
process.exit(1);
|
|
156
157
|
}
|
|
157
158
|
const text = (0, import_node_fs.readFileSync)(COMPOSE_FILE, "utf-8");
|
|
158
159
|
const hasServices = /(^|\n)services:\s*(\n|$)/.test(text);
|
|
159
|
-
const
|
|
160
|
-
if (!hasServices || !
|
|
161
|
-
console.error(`[teardown] aborting \u2014 ${COMPOSE_FILE} does not define a '
|
|
162
|
-
console.error(`[teardown] this command is only for the launch-pod
|
|
160
|
+
const hasLaunchPod = /\n[ \t]+launch-pod:/.test(text);
|
|
161
|
+
if (!hasServices || !hasLaunchPod) {
|
|
162
|
+
console.error(`[teardown] aborting \u2014 ${COMPOSE_FILE} does not define a 'launch-pod' service.`);
|
|
163
|
+
console.error(`[teardown] this command is only for the launch-pod container; refusing to touch this compose project.`);
|
|
163
164
|
process.exit(1);
|
|
164
165
|
}
|
|
165
166
|
}
|
|
@@ -188,10 +189,10 @@ function parseArgs(argv) {
|
|
|
188
189
|
return out;
|
|
189
190
|
}
|
|
190
191
|
function printHelp() {
|
|
191
|
-
console.log("usage: launch-
|
|
192
|
+
console.log("usage: launch-sequencer radar:teardown [--remove-env] [--remove-image] [--force]");
|
|
192
193
|
console.log("");
|
|
193
194
|
console.log(" --remove-env also delete .env (default: keep \u2014 contains live creds)");
|
|
194
|
-
console.log(" --remove-image also remove
|
|
195
|
+
console.log(" --remove-image also remove launch-pod:local image");
|
|
195
196
|
console.log(" --force skip the workspace-safety preflight (discards");
|
|
196
197
|
console.log(" uncommitted changes / unpushed commits / in-flight");
|
|
197
198
|
console.log(" analyzer sessions without warning)");
|
|
@@ -205,11 +206,11 @@ function sh(cmd, args, opts = {}) {
|
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
208
|
function workspaceSh(command) {
|
|
208
|
-
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q",
|
|
209
|
+
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q", COMPOSE_SERVICE]);
|
|
209
210
|
if (psQ.status === 0 && psQ.stdout.trim()) {
|
|
210
|
-
return sh("docker", [...COMPOSE_BASE, "exec", "-T",
|
|
211
|
+
return sh("docker", [...COMPOSE_BASE, "exec", "-T", COMPOSE_SERVICE, "sh", "-c", command]).stdout;
|
|
211
212
|
}
|
|
212
|
-
return sh("docker", [...COMPOSE_BASE, "run", "--rm", "--no-deps", "--entrypoint", "sh",
|
|
213
|
+
return sh("docker", [...COMPOSE_BASE, "run", "--rm", "--no-deps", "--entrypoint", "sh", COMPOSE_SERVICE, "-c", command]).stdout;
|
|
213
214
|
}
|
|
214
215
|
function loadIgnoreSegs() {
|
|
215
216
|
const json = workspaceSh("cat /workspace/.recall/config.json 2>/dev/null");
|
|
@@ -303,7 +304,7 @@ function filterStatusSignal(raw, modeOnly, segs, scaffoldClean) {
|
|
|
303
304
|
return out;
|
|
304
305
|
}
|
|
305
306
|
function probeDocker() {
|
|
306
|
-
const containerRunning = sh("docker", [...COMPOSE_BASE, "ps", "-q",
|
|
307
|
+
const containerRunning = sh("docker", [...COMPOSE_BASE, "ps", "-q", COMPOSE_SERVICE]).stdout.trim().length > 0;
|
|
307
308
|
const projectName = (sh("docker", [...COMPOSE_BASE, "config", "--format", "json"]).stdout.match(/"name"\s*:\s*"([^"]+)"/) ?? [])[1] ?? "";
|
|
308
309
|
let workspaceVolumeExists = false;
|
|
309
310
|
if (projectName) {
|
|
@@ -319,7 +320,7 @@ function preflightWorkspace(args) {
|
|
|
319
320
|
}
|
|
320
321
|
const state = probeDocker();
|
|
321
322
|
if (!state.containerRunning && !state.workspaceVolumeExists) {
|
|
322
|
-
console.log("[teardown] no
|
|
323
|
+
console.log("[teardown] no launch-pod container or workspace volume present \u2014 nothing to check");
|
|
323
324
|
return;
|
|
324
325
|
}
|
|
325
326
|
console.log("[teardown] checking workspace for unsaved work\u2026");
|
|
@@ -357,7 +358,7 @@ function preflightWorkspace(args) {
|
|
|
357
358
|
if (blocked) {
|
|
358
359
|
console.log("");
|
|
359
360
|
console.log("[teardown] aborting \u2014 workspace has unsaved work.");
|
|
360
|
-
console.log(" \u2022 commit + push from inside the container (docker compose exec
|
|
361
|
+
console.log(" \u2022 commit + push from inside the container (docker compose exec launch-pod sh), then re-run, OR");
|
|
361
362
|
console.log(" \u2022 re-run with --force to discard the work and tear down anyway.");
|
|
362
363
|
process.exit(2);
|
|
363
364
|
}
|
|
@@ -380,7 +381,7 @@ function parseEnvFile(path) {
|
|
|
380
381
|
return env;
|
|
381
382
|
}
|
|
382
383
|
async function releaseWebhook() {
|
|
383
|
-
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q",
|
|
384
|
+
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q", COMPOSE_SERVICE]);
|
|
384
385
|
if (psQ.status !== 0 || !psQ.stdout.trim()) {
|
|
385
386
|
console.log("[teardown] container not running \u2014 skipping webhook release");
|
|
386
387
|
console.log("[teardown] (any prior registration is now orphaned \u2014 clean up via cloud LS settings UI)");
|
|
@@ -435,15 +436,15 @@ function dockerComposeDown() {
|
|
|
435
436
|
}
|
|
436
437
|
function cleanupLocalArtifacts(args) {
|
|
437
438
|
if (args.removeImage) {
|
|
438
|
-
console.log("[teardown] removing image
|
|
439
|
-
const rmi = sh("docker", ["rmi", "
|
|
439
|
+
console.log("[teardown] removing image launch-pod:local");
|
|
440
|
+
const rmi = sh("docker", ["rmi", "launch-pod:local"]);
|
|
440
441
|
console.log(rmi.status === 0 ? " removed" : " not present");
|
|
441
442
|
}
|
|
442
|
-
for (const f of (0, import_node_fs.readdirSync)(
|
|
443
|
+
for (const f of (0, import_node_fs.readdirSync)(LAUNCH_POD_DIR)) {
|
|
443
444
|
if (f.startsWith("launchsecure-launch-kit-") && f.endsWith(".tgz")) {
|
|
444
445
|
console.log("[teardown] removing launch-kit tarball(s)");
|
|
445
446
|
try {
|
|
446
|
-
(0, import_node_fs.unlinkSync)((0, import_node_path.join)(
|
|
447
|
+
(0, import_node_fs.unlinkSync)((0, import_node_path.join)(LAUNCH_POD_DIR, f));
|
|
447
448
|
} catch {
|
|
448
449
|
}
|
|
449
450
|
}
|
|
@@ -464,7 +465,7 @@ async function main() {
|
|
|
464
465
|
printHelp();
|
|
465
466
|
return;
|
|
466
467
|
}
|
|
467
|
-
|
|
468
|
+
ensureLaunchPodCompose();
|
|
468
469
|
preflightWorkspace(args);
|
|
469
470
|
await releaseWebhook();
|
|
470
471
|
dockerComposeDown();
|