@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.
@@ -468,8 +468,272 @@ var init_mcp = __esm({
468
468
  }
469
469
  });
470
470
 
471
+ // src/server/launch-kit-services.ts
472
+ function defaultServices() {
473
+ return [expandShorthand("radar")];
474
+ }
475
+ function expandShorthand(name) {
476
+ const def = SHORTHANDS[name];
477
+ if (!def) {
478
+ throw new Error(
479
+ `[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
480
+ );
481
+ }
482
+ return { name, port: def.port, bin: def.bin, args: [...def.args] };
483
+ }
484
+ function coerceEntry(raw, index) {
485
+ if (typeof raw === "string") {
486
+ return expandShorthand(raw);
487
+ }
488
+ if (typeof raw !== "object" || raw === null) {
489
+ throw new Error(`[launch-kit-services] entry #${index} must be a string shorthand or an object`);
490
+ }
491
+ const r = raw;
492
+ if (typeof r.name !== "string" || typeof r.port !== "number" || typeof r.bin !== "string") {
493
+ throw new Error(`[launch-kit-services] entry #${index}: { name:string, port:number, bin:string } required`);
494
+ }
495
+ if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
496
+ throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
497
+ }
498
+ return {
499
+ name: r.name,
500
+ port: r.port,
501
+ bin: r.bin,
502
+ args: r.args ?? []
503
+ };
504
+ }
505
+ function validate(services) {
506
+ if (services.length === 0) {
507
+ throw new Error(`[launch-kit-services] resolved an empty service list`);
508
+ }
509
+ const seenNames = /* @__PURE__ */ new Set();
510
+ const seenPorts = /* @__PURE__ */ new Set();
511
+ for (const s of services) {
512
+ if (!DNS_NAME_RE.test(s.name)) {
513
+ throw new Error(`[launch-kit-services] service name "${s.name}" is not DNS-safe (lowercase letters/digits/hyphens, \u226463 chars, no leading/trailing hyphen)`);
514
+ }
515
+ if (seenNames.has(s.name)) {
516
+ throw new Error(`[launch-kit-services] duplicate service name "${s.name}"`);
517
+ }
518
+ seenNames.add(s.name);
519
+ if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) {
520
+ throw new Error(`[launch-kit-services] service "${s.name}" has invalid port ${s.port}`);
521
+ }
522
+ if (seenPorts.has(s.port)) {
523
+ throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
524
+ }
525
+ seenPorts.add(s.port);
526
+ }
527
+ return services;
528
+ }
529
+ function resolveServices(opts = {}) {
530
+ const env = opts.env ?? process.env;
531
+ const cwd = opts.cwd ?? process.cwd();
532
+ const rawEnv = env.LAUNCHKIT_SERVICES?.trim();
533
+ if (rawEnv) {
534
+ let parsed;
535
+ try {
536
+ parsed = JSON.parse(rawEnv);
537
+ } catch (err) {
538
+ throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
539
+ }
540
+ if (!Array.isArray(parsed)) {
541
+ throw new Error(`[launch-kit-services] LAUNCHKIT_SERVICES must be a JSON array`);
542
+ }
543
+ return validate(parsed.map(coerceEntry));
544
+ }
545
+ const filePath = (0, import_node_path.join)(cwd, ".launchpod", "services.json");
546
+ if ((0, import_node_fs.existsSync)(filePath)) {
547
+ let parsed;
548
+ try {
549
+ parsed = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
550
+ } catch (err) {
551
+ throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
552
+ }
553
+ if (!Array.isArray(parsed)) {
554
+ throw new Error(`[launch-kit-services] ${filePath} must be a JSON array`);
555
+ }
556
+ return validate(parsed.map(coerceEntry));
557
+ }
558
+ return validate(defaultServices());
559
+ }
560
+ var import_node_fs, import_node_path, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
561
+ var init_launch_kit_services = __esm({
562
+ "src/server/launch-kit-services.ts"() {
563
+ "use strict";
564
+ import_node_fs = require("node:fs");
565
+ import_node_path = require("node:path");
566
+ SHORTHANDS = {
567
+ radar: { port: 3517, bin: "launch-radar", args: [] },
568
+ sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
569
+ chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
570
+ deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
571
+ council: { port: 52839, bin: "launch-council", args: ["serve"] }
572
+ };
573
+ DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
574
+ SHORTHAND_NAMES = Object.keys(SHORTHANDS);
575
+ }
576
+ });
577
+
578
+ // src/server/cf-ingress.ts
579
+ async function cf(opts) {
580
+ const res = await fetch(`${CF_API_BASE}${opts.path}`, {
581
+ method: opts.method,
582
+ headers: {
583
+ Authorization: `Bearer ${opts.apiToken}`,
584
+ "Content-Type": "application/json",
585
+ Accept: "application/json",
586
+ "User-Agent": "launch-kit/cf-ingress"
587
+ },
588
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
589
+ signal: AbortSignal.timeout(15e3)
590
+ });
591
+ const text = await res.text();
592
+ let parsed;
593
+ try {
594
+ parsed = text ? JSON.parse(text) : { success: false };
595
+ } catch {
596
+ throw new Error(`[cf] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON body: ${text.slice(0, 200)}`);
597
+ }
598
+ return parsed;
599
+ }
600
+ function isNotFound(env) {
601
+ return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
602
+ }
603
+ function loadState(path6) {
604
+ if (!(0, import_node_fs2.existsSync)(path6)) return null;
605
+ try {
606
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path6, "utf8"));
607
+ if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
608
+ return parsed;
609
+ }
610
+ return null;
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+ function saveState(path6, state) {
616
+ const dir = (0, import_node_path2.dirname)(path6);
617
+ if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
618
+ (0, import_node_fs2.writeFileSync)(path6, JSON.stringify(state, null, 2));
619
+ }
620
+ async function ensureTunnel(input, knownTunnelId) {
621
+ if (knownTunnelId) {
622
+ const got = await cf({
623
+ apiToken: input.apiToken,
624
+ method: "GET",
625
+ path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
626
+ });
627
+ if (got.success && got.result && !got.result.deleted_at) {
628
+ return knownTunnelId;
629
+ }
630
+ if (!isNotFound(got) && !got.success) {
631
+ throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
632
+ }
633
+ }
634
+ const created = await cf({
635
+ apiToken: input.apiToken,
636
+ method: "POST",
637
+ path: `/accounts/${input.accountId}/cfd_tunnel`,
638
+ body: { name: input.tunnelName, config_src: "cloudflare" }
639
+ });
640
+ if (!created.success || !created.result) {
641
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
642
+ }
643
+ return created.result.id;
644
+ }
645
+ async function fetchConnectorToken(input, tunnelId) {
646
+ const res = await cf({
647
+ apiToken: input.apiToken,
648
+ method: "GET",
649
+ path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
650
+ });
651
+ if (!res.success || typeof res.result !== "string") {
652
+ throw new Error(`[cf] connector-token fetch failed: ${JSON.stringify(res.errors)}`);
653
+ }
654
+ return res.result;
655
+ }
656
+ async function setIngressConfig(input, tunnelId) {
657
+ const ingress = input.services.map((s) => ({
658
+ hostname: `${s.name}.${input.zone.name}`,
659
+ service: `http://localhost:${s.port}`
660
+ }));
661
+ ingress.push({ service: "http_status:404" });
662
+ const res = await cf({
663
+ apiToken: input.apiToken,
664
+ method: "PUT",
665
+ path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
666
+ body: { config: { ingress } }
667
+ });
668
+ if (!res.success) {
669
+ throw new Error(`[cf] ingress config PUT failed: ${JSON.stringify(res.errors)}`);
670
+ }
671
+ }
672
+ async function ensureDnsRecord(input, tunnelId, service) {
673
+ const fqdn = `${service.name}.${input.zone.name}`;
674
+ const target = `${tunnelId}.cfargotunnel.com`;
675
+ const existing = await cf({
676
+ apiToken: input.apiToken,
677
+ method: "GET",
678
+ path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
679
+ });
680
+ if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
681
+ const rec = existing.result[0];
682
+ if (rec.content === target) return;
683
+ const upd = await cf({
684
+ apiToken: input.apiToken,
685
+ method: "PUT",
686
+ path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
687
+ body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
688
+ });
689
+ if (!upd.success) {
690
+ throw new Error(`[cf] DNS record update for ${fqdn} failed: ${JSON.stringify(upd.errors)}`);
691
+ }
692
+ return;
693
+ }
694
+ const created = await cf({
695
+ apiToken: input.apiToken,
696
+ method: "POST",
697
+ path: `/zones/${input.zone.id}/dns_records`,
698
+ body: { type: "CNAME", name: fqdn, content: target, proxied: true, ttl: 1 }
699
+ });
700
+ if (created.success) return;
701
+ if ((created.errors ?? []).some((e) => e.code === CF_ERR_DNS_RECORD_EXISTS)) return;
702
+ throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
703
+ }
704
+ async function provisionIngress(input) {
705
+ const prior = loadState(input.stateFile);
706
+ const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
707
+ saveState(input.stateFile, {
708
+ tunnelId,
709
+ accountId: input.accountId,
710
+ tunnelName: input.tunnelName,
711
+ zoneId: input.zone.id
712
+ });
713
+ const connectorToken = await fetchConnectorToken(input, tunnelId);
714
+ await setIngressConfig(input, tunnelId);
715
+ await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
716
+ const hostnames = {};
717
+ for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
718
+ return { tunnelId, connectorToken, hostnames };
719
+ }
720
+ var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
721
+ var init_cf_ingress = __esm({
722
+ "src/server/cf-ingress.ts"() {
723
+ "use strict";
724
+ import_node_fs2 = require("node:fs");
725
+ import_node_path2 = require("node:path");
726
+ CF_API_BASE = "https://api.cloudflare.com/client/v4";
727
+ CF_ERR_DNS_RECORD_EXISTS = 81053;
728
+ }
729
+ });
730
+
471
731
  // src/server/radar-docker-init-entry.ts
472
732
  var radar_docker_init_entry_exports = {};
733
+ __export(radar_docker_init_entry_exports, {
734
+ maybeProvisionIngress: () => maybeProvisionIngress,
735
+ spawnServiceGroup: () => spawnServiceGroup
736
+ });
473
737
  function fail(message) {
474
738
  console.error(message);
475
739
  process.exit(1);
@@ -485,39 +749,39 @@ function run2(cmd, args, stdio = "inherit") {
485
749
  }
486
750
  async function setupFromCloud() {
487
751
  const pat = requireEnv("LS_PAT");
752
+ const orgSlug = requireEnv("LS_ORG_SLUG");
753
+ const projectSlug = requireEnv("LS_PROJECT_SLUG");
488
754
  const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
489
- const orgSlug = process.env.LS_ORG_SLUG;
490
- const projectSlug = process.env.LS_PROJECT_SLUG;
491
755
  const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
492
756
  let bundle;
493
757
  try {
494
758
  bundle = await mcp.call("radar_bootstrap_get", {});
495
759
  } catch (err) {
496
- fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and is scoped to the right org/project.`);
760
+ 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.`);
497
761
  }
498
- if (!process.env.LS_ORG_SLUG) process.env.LS_ORG_SLUG = bundle.orgSlug;
499
- if (!process.env.LS_PROJECT_SLUG) process.env.LS_PROJECT_SLUG = bundle.projectSlug;
500
762
  if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
501
763
  if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
502
764
  if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
503
765
  if (!process.env.GH_TOKEN) {
504
766
  fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
505
767
  }
506
- console.log(`[entrypoint] bundle from cloud: org=${process.env.LS_ORG_SLUG} project=${process.env.LS_PROJECT_SLUG} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()}`);
768
+ const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
769
+ 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}`);
770
+ return bundle;
507
771
  }
508
772
  function setupClaudeCredentials() {
509
773
  const home = process.env.HOME ?? "/home/launchpod";
510
- const claudeDir = (0, import_node_path.join)(home, ".claude");
511
- (0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
774
+ const claudeDir = (0, import_node_path3.join)(home, ".claude");
775
+ (0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
512
776
  const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
513
- const credsPath = (0, import_node_path.join)(claudeDir, ".credentials.json");
514
- (0, import_node_fs.writeFileSync)(credsPath, decoded);
515
- (0, import_node_fs.chmodSync)(credsPath, 384);
516
- const configPath = (0, import_node_path.join)(home, ".claude.json");
777
+ const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
778
+ (0, import_node_fs3.writeFileSync)(credsPath, decoded);
779
+ (0, import_node_fs3.chmodSync)(credsPath, 384);
780
+ const configPath = (0, import_node_path3.join)(home, ".claude.json");
517
781
  let cfg = {};
518
- if ((0, import_node_fs.existsSync)(configPath)) {
782
+ if ((0, import_node_fs3.existsSync)(configPath)) {
519
783
  try {
520
- cfg = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
784
+ cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
521
785
  } catch {
522
786
  cfg = {};
523
787
  }
@@ -526,8 +790,8 @@ function setupClaudeCredentials() {
526
790
  cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
527
791
  cfg.numStartups = (cfg.numStartups ?? 0) + 1;
528
792
  cfg.installMethod = cfg.installMethod ?? "global";
529
- (0, import_node_fs.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
530
- (0, import_node_fs.chmodSync)(configPath, 384);
793
+ (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
794
+ (0, import_node_fs3.chmodSync)(configPath, 384);
531
795
  }
532
796
  function setupGitAndGh() {
533
797
  const name = process.env.GIT_USER_NAME ?? "Radar Bot";
@@ -537,7 +801,7 @@ function setupGitAndGh() {
537
801
  }
538
802
  function initWorkspaceIfEmpty() {
539
803
  process.chdir("/workspace");
540
- if ((0, import_node_fs.existsSync)(".git")) {
804
+ if ((0, import_node_fs3.existsSync)(".git")) {
541
805
  console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
542
806
  return;
543
807
  }
@@ -552,47 +816,178 @@ function initWorkspaceIfEmpty() {
552
816
  ]);
553
817
  if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
554
818
  }
555
- function execLaunchPodRadar() {
556
- console.log("[entrypoint] starting launch-pod radar");
557
- const child = (0, import_node_child_process2.spawn)("launch-pod", ["radar"], { stdio: "inherit" });
558
- const forward = (sig) => () => {
559
- try {
560
- child.kill(sig);
561
- } catch {
819
+ async function maybeProvisionIngress(bundle, services, projectSlug) {
820
+ const token = bundle.cloudflareToken ?? null;
821
+ const accountId = bundle.cloudflareAccountId ?? null;
822
+ const zones = bundle.cloudflareZones ?? [];
823
+ if (!token && !accountId && zones.length === 0) return null;
824
+ if (!token || !accountId) {
825
+ fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
826
+ }
827
+ const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
828
+ let chosen = null;
829
+ if (baseDomain) {
830
+ chosen = zones.find((z) => z.name === baseDomain) ?? null;
831
+ if (!chosen) {
832
+ 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.`);
562
833
  }
563
- };
564
- process.on("SIGTERM", forward("SIGTERM"));
565
- process.on("SIGINT", forward("SIGINT"));
566
- process.on("SIGHUP", forward("SIGHUP"));
567
- child.on("exit", (code, signal) => {
568
- if (signal) process.kill(process.pid, signal);
569
- else process.exit(code ?? 0);
834
+ } else if (zones.length === 1) {
835
+ chosen = { id: zones[0].id, name: zones[0].name };
836
+ } else {
837
+ fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
838
+ }
839
+ const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
840
+ console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
841
+ const result = await provisionIngress({
842
+ apiToken: token,
843
+ accountId,
844
+ zone: chosen,
845
+ tunnelName: `launch-kit-${projectSlug}`,
846
+ services: services.map((s) => ({ name: s.name, port: s.port })),
847
+ stateFile
570
848
  });
849
+ for (const [name, fqdn] of Object.entries(result.hostnames)) {
850
+ console.log(`[entrypoint] ${name} \u2192 https://${fqdn}`);
851
+ }
852
+ return result;
853
+ }
854
+ function spawnServiceGroup(services) {
855
+ const children = [];
856
+ let shuttingDown = false;
857
+ const killAll = (signal = "SIGTERM") => {
858
+ if (shuttingDown) return;
859
+ shuttingDown = true;
860
+ for (const c2 of children) {
861
+ try {
862
+ c2.proc.kill(signal);
863
+ } catch {
864
+ }
865
+ }
866
+ };
867
+ const prefixStream = (name, stream, sink) => {
868
+ let buf = "";
869
+ stream.setEncoding("utf8");
870
+ stream.on("data", (chunk) => {
871
+ buf += chunk;
872
+ const lines = buf.split("\n");
873
+ buf = lines.pop() ?? "";
874
+ for (const line of lines) sink.write(`[${name}] ${line}
875
+ `);
876
+ });
877
+ stream.on("end", () => {
878
+ if (buf) sink.write(`[${name}] ${buf}
879
+ `);
880
+ });
881
+ };
882
+ const signalHandlers = [];
883
+ const installSignals = () => {
884
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
885
+ const fn = () => {
886
+ console.log(`[entrypoint] received ${sig} \u2014 forwarding to ${children.length} child process(es)`);
887
+ killAll(sig);
888
+ };
889
+ process.on(sig, fn);
890
+ signalHandlers.push({ sig, fn });
891
+ }
892
+ };
893
+ const removeSignals = () => {
894
+ for (const h of signalHandlers) process.off(h.sig, h.fn);
895
+ signalHandlers.length = 0;
896
+ };
897
+ return new Promise((resolve3, reject) => {
898
+ let exitedCount = 0;
899
+ let firstFailure = null;
900
+ for (const spec of services) {
901
+ const args = [...spec.args, "--port", String(spec.port)];
902
+ console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
903
+ const proc = (0, import_node_child_process2.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
904
+ children.push({ spec, proc });
905
+ if (proc.stdout) prefixStream(spec.name, proc.stdout, process.stdout);
906
+ if (proc.stderr) prefixStream(spec.name, proc.stderr, process.stderr);
907
+ proc.on("exit", (code, signal) => {
908
+ exitedCount += 1;
909
+ const label = `[${spec.name}] exited code=${code ?? "?"} signal=${signal ?? "-"}`;
910
+ if (!shuttingDown && code !== 0) {
911
+ console.error(`[entrypoint] ${label} \u2014 bringing the group down`);
912
+ if (!firstFailure) firstFailure = { name: spec.name, code, signal };
913
+ killAll();
914
+ } else {
915
+ console.log(`[entrypoint] ${label}`);
916
+ }
917
+ if (exitedCount === children.length) {
918
+ if (firstFailure) reject(new Error(`service "${firstFailure.name}" exited code=${firstFailure.code ?? "?"}`));
919
+ else resolve3();
920
+ }
921
+ });
922
+ proc.on("error", (err) => {
923
+ console.error(`[entrypoint] [${spec.name}] spawn error: ${err.message}`);
924
+ if (!firstFailure) firstFailure = { name: spec.name, code: null, signal: null };
925
+ killAll();
926
+ });
927
+ }
928
+ installSignals();
929
+ }).finally(removeSignals);
571
930
  }
572
931
  async function main() {
573
932
  for (const k of REQUIRED_ENV) requireEnv(k);
574
- await setupFromCloud();
933
+ const bundle = await setupFromCloud();
575
934
  setupClaudeCredentials();
576
935
  setupGitAndGh();
577
936
  initWorkspaceIfEmpty();
578
- execLaunchPodRadar();
937
+ let services;
938
+ try {
939
+ services = resolveServices();
940
+ } catch (err) {
941
+ fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
942
+ }
943
+ console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
944
+ const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
945
+ if (ingress) {
946
+ process.env.RADAR_CF_TUNNEL_TOKEN = ingress.connectorToken;
947
+ const radarFqdn = ingress.hostnames.radar;
948
+ if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
949
+ else if (services.some((s) => s.name === "radar")) {
950
+ fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
951
+ }
952
+ } else if (services.length > 1) {
953
+ const first = services[0];
954
+ console.warn(
955
+ `[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.`
956
+ );
957
+ if (first.name !== "radar") {
958
+ 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.`);
959
+ }
960
+ }
961
+ try {
962
+ await spawnServiceGroup(services);
963
+ process.exit(0);
964
+ } catch (err) {
965
+ console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
966
+ process.exit(1);
967
+ }
579
968
  }
580
- var import_node_child_process2, import_node_fs, import_node_path, REQUIRED_ENV;
969
+ var import_node_child_process2, import_node_fs3, import_node_path3, REQUIRED_ENV;
581
970
  var init_radar_docker_init_entry = __esm({
582
971
  "src/server/radar-docker-init-entry.ts"() {
583
972
  "use strict";
584
973
  import_node_child_process2 = require("node:child_process");
585
- import_node_fs = require("node:fs");
586
- import_node_path = require("node:path");
974
+ import_node_fs3 = require("node:fs");
975
+ import_node_path3 = require("node:path");
587
976
  init_mcp();
977
+ init_launch_kit_services();
978
+ init_cf_ingress();
588
979
  REQUIRED_ENV = [
589
980
  "CLAUDE_CREDENTIALS_B64",
590
- "LS_PAT"
981
+ "LS_PAT",
982
+ "LS_ORG_SLUG",
983
+ "LS_PROJECT_SLUG"
591
984
  ];
592
- main().catch((err) => {
593
- console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
594
- process.exit(1);
595
- });
985
+ if (!process.env.VITEST) {
986
+ main().catch((err) => {
987
+ console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
988
+ process.exit(1);
989
+ });
990
+ }
596
991
  }
597
992
  });
598
993
 
@@ -771,8 +1166,8 @@ Wired in Claude Code (.mcp.json):
771
1166
  launch-recall \u2014 restore deleted/modified files from shadow git
772
1167
 
773
1168
  Other tools (run on demand via npx):
774
- npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
775
- npx launch-pod \u2014 full pipeline UI (separate launch-pod login)
1169
+ npx launch-radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
1170
+ npx launch-sequencer \u2014 full pipeline UI (separate sequencer login)
776
1171
  npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
777
1172
  in-browser monitor. Paste the printed URL into
778
1173
  the beacon debug panel; events stream to
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/server/launch-radar-entry.ts
27
+ var import_node_child_process = require("node:child_process");
28
+ var import_node_path = __toESM(require("node:path"));
29
+ var sequencerEntry = import_node_path.default.join(__dirname, "cli.js");
30
+ var child = (0, import_node_child_process.spawn)(process.execPath, [sequencerEntry, "radar", ...process.argv.slice(2)], {
31
+ stdio: "inherit"
32
+ });
33
+ var forward = (sig) => () => {
34
+ try {
35
+ child.kill(sig);
36
+ } catch {
37
+ }
38
+ };
39
+ process.on("SIGTERM", forward("SIGTERM"));
40
+ process.on("SIGINT", forward("SIGINT"));
41
+ process.on("SIGHUP", forward("SIGHUP"));
42
+ child.on("exit", (code, signal) => {
43
+ if (signal) process.kill(process.pid, signal);
44
+ else process.exit(code ?? 0);
45
+ });