@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.
@@ -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 import_node_fs = require("node:fs");
7
- var import_node_path = require("node:path");
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 is scoped to the right org/project.`);
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
- 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()}`);
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, import_node_path.join)(home, ".claude");
167
- (0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
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, import_node_path.join)(claudeDir, ".credentials.json");
170
- (0, import_node_fs.writeFileSync)(credsPath, decoded);
171
- (0, import_node_fs.chmodSync)(credsPath, 384);
172
- const configPath = (0, import_node_path.join)(home, ".claude.json");
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, import_node_fs.existsSync)(configPath)) {
447
+ if ((0, import_node_fs3.existsSync)(configPath)) {
175
448
  try {
176
- cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
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, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
186
- (0, import_node_fs.chmodSync)(configPath, 384);
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, import_node_fs.existsSync)(".git")) {
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 execLaunchPodRadar() {
212
- console.log("[entrypoint] starting launch-pod radar");
213
- const child = (0, import_node_child_process.spawn)("launch-pod", ["radar"], { stdio: "inherit" });
214
- const forward = (sig) => () => {
215
- try {
216
- child.kill(sig);
217
- } catch {
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
- process.on("SIGTERM", forward("SIGTERM"));
221
- process.on("SIGINT", forward("SIGINT"));
222
- process.on("SIGHUP", forward("SIGHUP"));
223
- child.on("exit", (code, signal) => {
224
- if (signal) process.kill(process.pid, signal);
225
- else process.exit(code ?? 0);
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
- execLaunchPodRadar();
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
- main().catch((err) => {
237
- console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
238
- process.exit(1);
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 RADAR_DIR = (0, import_node_path.dirname)(COMPOSE_FILE);
148
+ var LAUNCH_POD_DIR = (0, import_node_path.dirname)(COMPOSE_FILE);
149
149
  var COMPOSE_BASE = ["compose", "-f", COMPOSE_FILE];
150
- var ENV_PATH = (0, import_node_path.join)(RADAR_DIR, ".env");
151
- function ensureRadarCompose() {
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/radar/ (the directory holding the radar compose).`);
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 hasRadar = /\n[ \t]+radar:/.test(text);
160
- if (!hasServices || !hasRadar) {
161
- console.error(`[teardown] aborting \u2014 ${COMPOSE_FILE} does not define a 'radar' service.`);
162
- console.error(`[teardown] this command is only for the launch-pod radar container; refusing to touch this compose project.`);
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-pod radar:teardown [--remove-env] [--remove-image] [--force]");
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 launchpod-radar:local image");
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", "radar"]);
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", "radar", "sh", "-c", command]).stdout;
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", "radar", "-c", command]).stdout;
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", "radar"]).stdout.trim().length > 0;
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 radar container or workspace volume present \u2014 nothing to check");
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 radar sh), then re-run, OR");
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", "radar"]);
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 launchpod-radar:local");
439
- const rmi = sh("docker", ["rmi", "launchpod-radar:local"]);
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)(RADAR_DIR)) {
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)(RADAR_DIR, f));
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
- ensureRadarCompose();
468
+ ensureLaunchPodCompose();
468
469
  preflightWorkspace(args);
469
470
  await releaseWebhook();
470
471
  dockerComposeDown();