@launchsecure/launch-kit 0.0.33 → 0.0.35
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 +286 -50
- package/dist/server/course-entry.js +1 -1
- package/dist/server/graph-mcp-entry.js +180 -4
- package/dist/server/init-entry.js +563 -60
- package/dist/server/launch-bot-entry.js +4078 -0
- package/dist/server/launch-radar-entry.js +45 -0
- package/dist/server/orbit-entry.js +123 -26
- package/dist/server/parse-worker-entry.js +167 -2
- package/dist/server/radar-docker-init-entry.js +496 -39
- package/dist/server/radar-teardown-entry.js +23 -22
- package/dist/server/rover-entry.js +20555 -0
- package/package.json +8 -5
- package/scaffolds/ls-marketplace/plugins/kit/commands/standup.md +6 -6
- package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +6 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +72 -49
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/briefs.mjs +152 -0
- 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/kickoff/SKILL.md +151 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +14 -2
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +149 -133
|
@@ -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,269 @@ 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
|
+
// Claude web terminal — exposes a viewable/drivable `claude` session at
|
|
157
|
+
// `bot.<baseDomain>`. NOTE: no auth gate yet (tracked as a separate
|
|
158
|
+
// high-priority rover-security work item); ships behind a plain link first.
|
|
159
|
+
bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
|
|
160
|
+
};
|
|
161
|
+
var DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
162
|
+
function defaultServices() {
|
|
163
|
+
return [expandShorthand("radar")];
|
|
164
|
+
}
|
|
165
|
+
function expandShorthand(name) {
|
|
166
|
+
if (name === "preview") {
|
|
167
|
+
const raw = process.env.PREVIEW_PORT;
|
|
168
|
+
const port = raw && Number.isFinite(Number.parseInt(raw, 10)) ? Number.parseInt(raw, 10) : 3e3;
|
|
169
|
+
return { name: "preview", port, bin: "", args: [], skipSpawn: true };
|
|
170
|
+
}
|
|
171
|
+
const def = SHORTHANDS[name];
|
|
172
|
+
if (!def) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${[...Object.keys(SHORTHANDS), "preview"].join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
178
|
+
}
|
|
179
|
+
function coerceEntry(raw, index) {
|
|
180
|
+
if (typeof raw === "string") {
|
|
181
|
+
return expandShorthand(raw);
|
|
182
|
+
}
|
|
183
|
+
if (typeof raw !== "object" || raw === null) {
|
|
184
|
+
throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
|
|
185
|
+
}
|
|
186
|
+
const r = raw;
|
|
187
|
+
if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
|
|
188
|
+
throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
|
|
189
|
+
}
|
|
190
|
+
if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
|
|
191
|
+
throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
name: r.name,
|
|
195
|
+
port: r.port,
|
|
196
|
+
bin: r.bin,
|
|
197
|
+
args: r.args ?? []
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function validate(services) {
|
|
201
|
+
if (services.length === 0) {
|
|
202
|
+
throw new Error(`[launch-kit-services] resolved an empty service list`);
|
|
203
|
+
}
|
|
204
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
205
|
+
const seenPorts = /* @__PURE__ */ new Set();
|
|
206
|
+
for (const s of services) {
|
|
207
|
+
if (!DNS_NAME_RE.test(s.name)) {
|
|
208
|
+
throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
|
|
209
|
+
}
|
|
210
|
+
if (seenNames.has(s.name)) {
|
|
211
|
+
throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
|
|
212
|
+
}
|
|
213
|
+
seenNames.add(s.name);
|
|
214
|
+
if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
|
|
215
|
+
throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
|
|
216
|
+
}
|
|
217
|
+
if (seenPorts.has(s.port)) {
|
|
218
|
+
throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
|
|
219
|
+
}
|
|
220
|
+
seenPorts.add(s.port);
|
|
221
|
+
}
|
|
222
|
+
return services;
|
|
223
|
+
}
|
|
224
|
+
function resolveServices(opts = {}) {
|
|
225
|
+
const env = opts.env ?? process.env;
|
|
226
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
227
|
+
const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
|
|
228
|
+
if (rawEnv) {
|
|
229
|
+
let parsed;
|
|
230
|
+
try {
|
|
231
|
+
parsed = JSON.parse(rawEnv);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
234
|
+
}
|
|
235
|
+
if (!Array.isArray(parsed)) {
|
|
236
|
+
throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
|
|
237
|
+
}
|
|
238
|
+
return validate(parsed.map(coerceEntry));
|
|
239
|
+
}
|
|
240
|
+
const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
|
|
241
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
245
|
+
} catch (err) {
|
|
246
|
+
throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
247
|
+
}
|
|
248
|
+
if (!Array.isArray(parsed)) {
|
|
249
|
+
throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
|
|
250
|
+
}
|
|
251
|
+
return validate(parsed.map(coerceEntry));
|
|
252
|
+
}
|
|
253
|
+
return validate(defaultServices());
|
|
254
|
+
}
|
|
255
|
+
var SHORTHAND_NAMES = [...Object.keys(SHORTHANDS), "preview"];
|
|
256
|
+
|
|
257
|
+
// src/server/cf-ingress.ts
|
|
258
|
+
var import_node_fs2 = require("node:fs");
|
|
259
|
+
var import_node_path2 = require("node:path");
|
|
260
|
+
var CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
261
|
+
var CF_ERR_DNS_RECORD_EXISTS = 81053;
|
|
262
|
+
async function cf(opts) {
|
|
263
|
+
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
264
|
+
method: opts.method,
|
|
265
|
+
headers: {
|
|
266
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
Accept: "application/json",
|
|
269
|
+
"User-Agent": "launch-kit/cf-ingress"
|
|
270
|
+
},
|
|
271
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
272
|
+
signal: AbortSignal.timeout(15e3)
|
|
273
|
+
});
|
|
274
|
+
const text = await res.text();
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = text ? JSON.parse(text) : { success: false };
|
|
278
|
+
} catch {
|
|
279
|
+
throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
|
|
280
|
+
}
|
|
281
|
+
return parsed;
|
|
282
|
+
}
|
|
283
|
+
function isNotFound(env) {
|
|
284
|
+
return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
|
|
285
|
+
}
|
|
286
|
+
function loadState(path) {
|
|
287
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
290
|
+
if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
|
|
291
|
+
return parsed;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function saveState(path, state) {
|
|
299
|
+
const dir = (0, import_node_path2.dirname)(path);
|
|
300
|
+
if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
301
|
+
(0, import_node_fs2.writeFileSync)(path, JSON.stringify(state, null, 2));
|
|
302
|
+
}
|
|
303
|
+
async function ensureTunnel(input, knownTunnelId) {
|
|
304
|
+
if (knownTunnelId) {
|
|
305
|
+
const got = await cf({
|
|
306
|
+
apiToken: input.apiToken,
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
|
|
309
|
+
});
|
|
310
|
+
if (got.success && got.result && !got.result.deleted_at) {
|
|
311
|
+
return knownTunnelId;
|
|
312
|
+
}
|
|
313
|
+
if (!isNotFound(got) && !got.success) {
|
|
314
|
+
throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const created = await cf({
|
|
318
|
+
apiToken: input.apiToken,
|
|
319
|
+
method: "POST",
|
|
320
|
+
path: `/accounts/${input.accountId}/cfd_tunnel`,
|
|
321
|
+
body: { name: input.tunnelName, config_src: "cloudflare" }
|
|
322
|
+
});
|
|
323
|
+
if (!created.success || !created.result) {
|
|
324
|
+
throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
|
|
325
|
+
}
|
|
326
|
+
return created.result.id;
|
|
327
|
+
}
|
|
328
|
+
async function fetchConnectorToken(input, tunnelId) {
|
|
329
|
+
const res = await cf({
|
|
330
|
+
apiToken: input.apiToken,
|
|
331
|
+
method: "GET",
|
|
332
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
|
|
333
|
+
});
|
|
334
|
+
if (!res.success || typeof res.result !== "string") {
|
|
335
|
+
throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
|
|
336
|
+
}
|
|
337
|
+
return res.result;
|
|
338
|
+
}
|
|
339
|
+
async function setIngressConfig(input, tunnelId) {
|
|
340
|
+
const ingress = input.services.map((s) => ({
|
|
341
|
+
hostname: `${s.name}.${input.zone.name}`,
|
|
342
|
+
service: `http://localhost:${s.port}`
|
|
343
|
+
}));
|
|
344
|
+
ingress.push({ service: "http_status:404" });
|
|
345
|
+
const res = await cf({
|
|
346
|
+
apiToken: input.apiToken,
|
|
347
|
+
method: "PUT",
|
|
348
|
+
path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
349
|
+
body: { config: { ingress } }
|
|
350
|
+
});
|
|
351
|
+
if (!res.success) {
|
|
352
|
+
throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function ensureDnsRecord(input, tunnelId, service) {
|
|
356
|
+
const fqdn = `${service.name}.${input.zone.name}`;
|
|
357
|
+
const target = `${tunnelId}.cfargotunnel.com`;
|
|
358
|
+
const existing = await cf({
|
|
359
|
+
apiToken: input.apiToken,
|
|
360
|
+
method: "GET",
|
|
361
|
+
path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
|
|
362
|
+
});
|
|
363
|
+
if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
|
|
364
|
+
const rec = existing.result[0];
|
|
365
|
+
if (rec.content === target) return;
|
|
366
|
+
const upd = await cf({
|
|
367
|
+
apiToken: input.apiToken,
|
|
368
|
+
method: "PUT",
|
|
369
|
+
path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
|
|
370
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
371
|
+
});
|
|
372
|
+
if (!upd.success) {
|
|
373
|
+
throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const created = await cf({
|
|
378
|
+
apiToken: input.apiToken,
|
|
379
|
+
method: "POST",
|
|
380
|
+
path: `/zones/${input.zone.id}/dns_records`,
|
|
381
|
+
body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
|
|
382
|
+
});
|
|
383
|
+
if (created.success) return;
|
|
384
|
+
if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
|
|
385
|
+
throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
|
|
386
|
+
}
|
|
387
|
+
async function provisionIngress(input) {
|
|
388
|
+
const prior = loadState(input.stateFile);
|
|
389
|
+
const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
|
|
390
|
+
saveState(input.stateFile, {
|
|
391
|
+
tunnelId,
|
|
392
|
+
accountId: input.accountId,
|
|
393
|
+
tunnelName: input.tunnelName,
|
|
394
|
+
zoneId: input.zone.id
|
|
395
|
+
});
|
|
396
|
+
const connectorToken = await fetchConnectorToken(input, tunnelId);
|
|
397
|
+
await setIngressConfig(input, tunnelId);
|
|
398
|
+
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
399
|
+
const hostnames = {};
|
|
400
|
+
for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
|
|
401
|
+
return { tunnelId, connectorToken, hostnames };
|
|
402
|
+
}
|
|
403
|
+
|
|
124
404
|
// src/server/radar-docker-init-entry.ts
|
|
125
405
|
var REQUIRED_ENV = [
|
|
126
406
|
"CLAUDE_CREDENTIALS_B64",
|
|
127
|
-
"LS_PAT"
|
|
407
|
+
"LS_PAT",
|
|
408
|
+
"LS_ORG_SLUG",
|
|
409
|
+
"LS_PROJECT_SLUG"
|
|
128
410
|
];
|
|
129
411
|
function fail(message) {
|
|
130
412
|
console.error(message);
|
|
@@ -141,39 +423,39 @@ function run(cmd, args, stdio = "inherit") {
|
|
|
141
423
|
}
|
|
142
424
|
async function setupFromCloud() {
|
|
143
425
|
const pat = requireEnv("LS_PAT");
|
|
426
|
+
const orgSlug = requireEnv("LS_ORG_SLUG");
|
|
427
|
+
const projectSlug = requireEnv("LS_PROJECT_SLUG");
|
|
144
428
|
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
429
|
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
148
430
|
let bundle;
|
|
149
431
|
try {
|
|
150
432
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
151
433
|
} 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
|
|
434
|
+
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
435
|
}
|
|
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
436
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
157
437
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
158
438
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
159
439
|
if (!process.env.GH_TOKEN) {
|
|
160
440
|
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
441
|
}
|
|
162
|
-
|
|
442
|
+
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
443
|
+
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}`);
|
|
444
|
+
return bundle;
|
|
163
445
|
}
|
|
164
446
|
function setupClaudeCredentials() {
|
|
165
447
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
166
|
-
const claudeDir = (0,
|
|
167
|
-
(0,
|
|
448
|
+
const claudeDir = (0, import_node_path3.join)(home, ".claude");
|
|
449
|
+
(0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
|
|
168
450
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
169
|
-
const credsPath = (0,
|
|
170
|
-
(0,
|
|
171
|
-
(0,
|
|
172
|
-
const configPath = (0,
|
|
451
|
+
const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
|
|
452
|
+
(0, import_node_fs3.writeFileSync)(credsPath, decoded);
|
|
453
|
+
(0, import_node_fs3.chmodSync)(credsPath, 384);
|
|
454
|
+
const configPath = (0, import_node_path3.join)(home, ".claude.json");
|
|
173
455
|
let cfg = {};
|
|
174
|
-
if ((0,
|
|
456
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
175
457
|
try {
|
|
176
|
-
cfg = JSON.parse((0,
|
|
458
|
+
cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
|
|
177
459
|
} catch {
|
|
178
460
|
cfg = {};
|
|
179
461
|
}
|
|
@@ -182,8 +464,25 @@ function setupClaudeCredentials() {
|
|
|
182
464
|
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
183
465
|
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
184
466
|
cfg.installMethod = cfg.installMethod ?? "global";
|
|
185
|
-
|
|
186
|
-
|
|
467
|
+
const PREAPPROVED_MCPS = [
|
|
468
|
+
"launch-secure",
|
|
469
|
+
"launch-chart",
|
|
470
|
+
"launch-deck",
|
|
471
|
+
"launch-orbit",
|
|
472
|
+
"launch-recall",
|
|
473
|
+
"launch-beacon",
|
|
474
|
+
"launch-sequencer"
|
|
475
|
+
];
|
|
476
|
+
const projects = cfg.projects ?? {};
|
|
477
|
+
const wsKey = "/workspace";
|
|
478
|
+
const wsProject = projects[wsKey] ?? {};
|
|
479
|
+
const existingEnabled = Array.isArray(wsProject.enabledMcpjsonServers) ? wsProject.enabledMcpjsonServers : [];
|
|
480
|
+
const mergedEnabled = Array.from(/* @__PURE__ */ new Set([...existingEnabled, ...PREAPPROVED_MCPS]));
|
|
481
|
+
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
482
|
+
projects[wsKey] = wsProject;
|
|
483
|
+
cfg.projects = projects;
|
|
484
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
485
|
+
(0, import_node_fs3.chmodSync)(configPath, 384);
|
|
187
486
|
}
|
|
188
487
|
function setupGitAndGh() {
|
|
189
488
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
@@ -191,9 +490,30 @@ function setupGitAndGh() {
|
|
|
191
490
|
const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
192
491
|
if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
193
492
|
}
|
|
493
|
+
function detectAndSetPreviewPort() {
|
|
494
|
+
if (process.env.PREVIEW_PORT) return;
|
|
495
|
+
try {
|
|
496
|
+
const pkgPath = "/workspace/package.json";
|
|
497
|
+
if (!(0, import_node_fs3.existsSync)(pkgPath)) return;
|
|
498
|
+
const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
|
|
499
|
+
const scripts = pkg.scripts ?? {};
|
|
500
|
+
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
501
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
502
|
+
const script = scripts[name];
|
|
503
|
+
if (typeof script !== "string") continue;
|
|
504
|
+
const m = script.match(portRe);
|
|
505
|
+
if (m) {
|
|
506
|
+
process.env.PREVIEW_PORT = m[1];
|
|
507
|
+
console.log(`[entrypoint] preview port detected from package.json scripts.${name}: ${m[1]}`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
194
514
|
function initWorkspaceIfEmpty() {
|
|
195
515
|
process.chdir("/workspace");
|
|
196
|
-
if ((0,
|
|
516
|
+
if ((0, import_node_fs3.existsSync)(".git")) {
|
|
197
517
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
198
518
|
return;
|
|
199
519
|
}
|
|
@@ -208,32 +528,169 @@ function initWorkspaceIfEmpty() {
|
|
|
208
528
|
]);
|
|
209
529
|
if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
210
530
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
531
|
+
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
532
|
+
const token = bundle.cloudflareToken ?? null;
|
|
533
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
534
|
+
const zones = bundle.cloudflareZones ?? [];
|
|
535
|
+
if (!token && !accountId && zones.length === 0) return null;
|
|
536
|
+
if (!token || !accountId) {
|
|
537
|
+
fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
538
|
+
}
|
|
539
|
+
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
540
|
+
let chosen = null;
|
|
541
|
+
if (baseDomain) {
|
|
542
|
+
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
543
|
+
if (!chosen) {
|
|
544
|
+
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
545
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
546
|
+
} else if (zones.length === 1) {
|
|
547
|
+
chosen = { id: zones[0].id, name: zones[0].name };
|
|
548
|
+
} else {
|
|
549
|
+
fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
550
|
+
}
|
|
551
|
+
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
552
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
|
|
553
|
+
const result = await provisionIngress({
|
|
554
|
+
apiToken: token,
|
|
555
|
+
accountId,
|
|
556
|
+
zone: chosen,
|
|
557
|
+
tunnelName: `launch-kit-${projectSlug}`,
|
|
558
|
+
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
559
|
+
stateFile
|
|
226
560
|
});
|
|
561
|
+
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
562
|
+
console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
function spawnServiceGroup(services) {
|
|
567
|
+
const children = [];
|
|
568
|
+
let shuttingDown = false;
|
|
569
|
+
const killAll = (signal = "SIGTERM") => {
|
|
570
|
+
if (shuttingDown) return;
|
|
571
|
+
shuttingDown = true;
|
|
572
|
+
for (const c of children) {
|
|
573
|
+
try {
|
|
574
|
+
c.proc.kill(signal);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const prefixStream = (name, stream, sink) => {
|
|
580
|
+
let buf = "";
|
|
581
|
+
stream.setEncoding("utf8");
|
|
582
|
+
stream.on("data", (chunk) => {
|
|
583
|
+
buf += chunk;
|
|
584
|
+
const lines = buf.split("\n");
|
|
585
|
+
buf = lines.pop() ?? "";
|
|
586
|
+
for (const line of lines) sink.write(`[${name}] ${line}
|
|
587
|
+
`);
|
|
588
|
+
});
|
|
589
|
+
stream.on("end", () => {
|
|
590
|
+
if (buf) sink.write(`[${name}] ${buf}
|
|
591
|
+
`);
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
const signalHandlers = [];
|
|
595
|
+
const installSignals = () => {
|
|
596
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
597
|
+
const fn = () => {
|
|
598
|
+
console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
|
|
599
|
+
killAll(sig);
|
|
600
|
+
};
|
|
601
|
+
process.on(sig, fn);
|
|
602
|
+
signalHandlers.push({ sig, fn });
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const removeSignals = () => {
|
|
606
|
+
for (const h of signalHandlers) process.off(h.sig, h.fn);
|
|
607
|
+
signalHandlers.length = 0;
|
|
608
|
+
};
|
|
609
|
+
return new Promise((resolve, reject) => {
|
|
610
|
+
let exitedCount = 0;
|
|
611
|
+
let firstFailure = null;
|
|
612
|
+
for (const spec of services) {
|
|
613
|
+
if (spec.skipSpawn) {
|
|
614
|
+
console.log(`[entrypoint] ${spec.name} \u2192 ingress-only on port ${spec.port} (no spawn; user starts dev server here)`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const args = [...spec.args, "--port", String(spec.port)];
|
|
618
|
+
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
619
|
+
const proc = (0, import_node_child_process.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
620
|
+
children.push({ spec, proc });
|
|
621
|
+
if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
|
|
622
|
+
if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
|
|
623
|
+
proc.on("exit", (code, signal) => {
|
|
624
|
+
exitedCount += 1;
|
|
625
|
+
const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
|
|
626
|
+
if (!shuttingDown && code !== 0) {
|
|
627
|
+
console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
|
|
628
|
+
if (!firstFailure) firstFailure = { name: spec.name, code, signal };
|
|
629
|
+
killAll();
|
|
630
|
+
} else {
|
|
631
|
+
console.log(`[entrypoint] ${label}`);
|
|
632
|
+
}
|
|
633
|
+
if (exitedCount === children.length) {
|
|
634
|
+
if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
|
|
635
|
+
else resolve();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
proc.on("error", (err) => {
|
|
639
|
+
console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
|
|
640
|
+
if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
|
|
641
|
+
killAll();
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
installSignals();
|
|
645
|
+
}).finally(removeSignals);
|
|
227
646
|
}
|
|
228
647
|
async function main() {
|
|
229
648
|
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
230
|
-
await setupFromCloud();
|
|
649
|
+
const bundle = await setupFromCloud();
|
|
231
650
|
setupClaudeCredentials();
|
|
232
651
|
setupGitAndGh();
|
|
233
652
|
initWorkspaceIfEmpty();
|
|
234
|
-
|
|
653
|
+
detectAndSetPreviewPort();
|
|
654
|
+
let services;
|
|
655
|
+
try {
|
|
656
|
+
services = resolveServices();
|
|
657
|
+
} catch (err) {
|
|
658
|
+
fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
659
|
+
}
|
|
660
|
+
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
661
|
+
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
662
|
+
if (ingress) {
|
|
663
|
+
process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
|
|
664
|
+
const radarFqdn = ingress.hostnames.radar;
|
|
665
|
+
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
666
|
+
else if (services.some((s) => s.name === "radar")) {
|
|
667
|
+
fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
668
|
+
}
|
|
669
|
+
} else if (services.length > 1) {
|
|
670
|
+
const first = services[0];
|
|
671
|
+
console.warn(
|
|
672
|
+
`[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.`
|
|
673
|
+
);
|
|
674
|
+
if (first.name !== "radar") {
|
|
675
|
+
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.`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
await spawnServiceGroup(services);
|
|
680
|
+
process.exit(0);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
235
685
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
686
|
+
if (!process.env.VITEST) {
|
|
687
|
+
main().catch((err) => {
|
|
688
|
+
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
693
|
+
0 && (module.exports = {
|
|
694
|
+
maybeProvisionIngress,
|
|
695
|
+
spawnServiceGroup
|
|
239
696
|
});
|