@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.
@@ -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,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 is scoped to the right org/project.`);
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
- 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()}`);
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, import_node_path.join)(home, ".claude");
167
- (0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
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, 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");
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, import_node_fs.existsSync)(configPath)) {
456
+ if ((0, import_node_fs3.existsSync)(configPath)) {
175
457
  try {
176
- cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
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
- (0, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
186
- (0, import_node_fs.chmodSync)(configPath, 384);
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, import_node_fs.existsSync)(".git")) {
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 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 {
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
- 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);
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
- execLaunchPodRadar();
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
- main().catch((err) => {
237
- console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
238
- process.exit(1);
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
  });