@launchsecure/launch-kit 0.0.34 → 0.0.36

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.
@@ -34,6 +34,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
34
  ));
35
35
 
36
36
  // src/server/cf-ingress.ts
37
+ function serviceLabel(s) {
38
+ return s.label ?? s.name;
39
+ }
37
40
  async function cf(opts) {
38
41
  const res = await fetch(`${CF_API_BASE}${opts.path}`, {
39
42
  method: opts.method,
@@ -113,7 +116,7 @@ async function fetchConnectorToken(input, tunnelId) {
113
116
  }
114
117
  async function setIngressConfig(input, tunnelId) {
115
118
  const ingress = input.services.map((s) => ({
116
- hostname: `${s.name}.${input.zone.name}`,
119
+ hostname: `${serviceLabel(s)}.${input.zone.name}`,
117
120
  service: `http://localhost:${s.port}`
118
121
  }));
119
122
  ingress.push({ service: "http_status:404" });
@@ -128,7 +131,7 @@ async function setIngressConfig(input, tunnelId) {
128
131
  }
129
132
  }
130
133
  async function ensureDnsRecord(input, tunnelId, service) {
131
- const fqdn = `${service.name}.${input.zone.name}`;
134
+ const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
132
135
  const target = `${tunnelId}.cfargotunnel.com`;
133
136
  const existing = await cf({
134
137
  apiToken: input.apiToken,
@@ -172,7 +175,7 @@ async function provisionIngress(input) {
172
175
  await setIngressConfig(input, tunnelId);
173
176
  await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
174
177
  const hostnames = {};
175
- for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
178
+ for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
176
179
  return { tunnelId, connectorToken, hostnames };
177
180
  }
178
181
  var import_node_fs, import_node_path, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
@@ -222,6 +225,17 @@ async function ensureDocker(opts = {}) {
222
225
  const initial = detectDocker();
223
226
  if (initial.daemonAlive) return initial;
224
227
  const platform = process.platform;
228
+ if (initial.cliPresent && canStartDaemon(platform)) {
229
+ if (await confirm(opts, "Docker is installed but its daemon isn't running. Start it now?")) {
230
+ const started = await startDockerDaemon(platform);
231
+ if (started.daemonAlive) return started;
232
+ console.log(
233
+ `
234
+ Docker did not become ready within ${Math.round(DAEMON_START_TIMEOUT_MS / 1e3)}s. Open Docker manually and re-run setup.`
235
+ );
236
+ throw new Error("Docker daemon did not start in time.");
237
+ }
238
+ }
225
239
  if (!opts.autoInstall) {
226
240
  printManualInstructions(initial, platform);
227
241
  throw new Error("Docker is not ready. Install + start it, then re-run `launch-rover setup`.");
@@ -250,6 +264,40 @@ async function ensureDocker(opts = {}) {
250
264
  `Auto-install is not supported on ${platform}. Install Docker Desktop manually, then re-run setup.`
251
265
  );
252
266
  }
267
+ function canStartDaemon(platform) {
268
+ if (platform === "darwin") return true;
269
+ if (platform === "linux") return commandExists("systemctl");
270
+ return false;
271
+ }
272
+ async function startDockerDaemon(platform) {
273
+ if (platform === "darwin") {
274
+ runAndPrint("open", ["-a", "Docker"]);
275
+ } else if (platform === "linux") {
276
+ runAndPrint("sudo", ["systemctl", "start", "docker"]);
277
+ } else {
278
+ throw new Error(`Cannot start the Docker daemon on ${platform}.`);
279
+ }
280
+ console.log("Waiting for the Docker daemon to come up\u2026");
281
+ const deadline = DAEMON_START_TIMEOUT_MS;
282
+ let waited = 0;
283
+ while (waited < deadline) {
284
+ await (0, import_promises2.setTimeout)(DAEMON_POLL_INTERVAL_MS);
285
+ waited += DAEMON_POLL_INTERVAL_MS;
286
+ const probe = detectDocker();
287
+ if (probe.daemonAlive) {
288
+ console.log(`Docker daemon is up (after ~${Math.round(waited / 1e3)}s).`);
289
+ return probe;
290
+ }
291
+ }
292
+ return detectDocker();
293
+ }
294
+ function commandExists(cmd) {
295
+ const probe = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [cmd], {
296
+ stdio: "ignore",
297
+ timeout: 1500
298
+ });
299
+ return probe.status === 0;
300
+ }
253
301
  function printManualInstructions(detection, platform) {
254
302
  console.log("\n\u2500\u2500 Docker is required to run launch-rover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
255
303
  if (!detection.cliPresent) {
@@ -305,13 +353,16 @@ function runAndPrint(cmd, args) {
305
353
  throw new Error(`${cmd} exited with code ${result.status ?? "signal"}`);
306
354
  }
307
355
  }
308
- var import_node_child_process, import_promises, import_node_process;
356
+ var import_node_child_process, import_promises, import_node_process, import_promises2, DAEMON_START_TIMEOUT_MS, DAEMON_POLL_INTERVAL_MS;
309
357
  var init_docker_install = __esm({
310
358
  "src/server/rover/docker-install.ts"() {
311
359
  "use strict";
312
360
  import_node_child_process = require("node:child_process");
313
361
  import_promises = require("node:readline/promises");
314
362
  import_node_process = require("node:process");
363
+ import_promises2 = require("node:timers/promises");
364
+ DAEMON_START_TIMEOUT_MS = 6e4;
365
+ DAEMON_POLL_INTERVAL_MS = 2e3;
315
366
  }
316
367
  });
317
368
 
@@ -439,18 +490,18 @@ var init_mcp_client = __esm({
439
490
  var state_exports = {};
440
491
  __export(state_exports, {
441
492
  clearRoverState: () => clearRoverState,
442
- listRoverOrgs: () => listRoverOrgs,
493
+ listRoverIds: () => listRoverIds,
443
494
  loadRoverState: () => loadRoverState,
444
495
  saveRoverState: () => saveRoverState
445
496
  });
446
497
  function stateDir() {
447
498
  return (0, import_node_path2.join)((0, import_node_os.homedir)(), LAUNCHSECURE_DIR, "rover");
448
499
  }
449
- function statePath(orgSlug) {
450
- return (0, import_node_path2.join)(stateDir(), `${orgSlug}.json`);
500
+ function statePath(roverId) {
501
+ return (0, import_node_path2.join)(stateDir(), `${roverId}.json`);
451
502
  }
452
- function loadRoverState(orgSlug) {
453
- const p = statePath(orgSlug);
503
+ function loadRoverState(roverId) {
504
+ const p = statePath(roverId);
454
505
  if (!(0, import_node_fs2.existsSync)(p)) return null;
455
506
  try {
456
507
  const data = JSON.parse((0, import_node_fs2.readFileSync)(p, "utf-8"));
@@ -462,23 +513,23 @@ function loadRoverState(orgSlug) {
462
513
  }
463
514
  function saveRoverState(state) {
464
515
  (0, import_node_fs2.mkdirSync)(stateDir(), { recursive: true });
465
- const final = statePath(state.orgSlug);
516
+ const final = statePath(state.roverId);
466
517
  const tmp = `${final}.tmp`;
467
518
  (0, import_node_fs2.writeFileSync)(tmp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
468
519
  (0, import_node_fs2.renameSync)(tmp, final);
469
520
  }
470
- function clearRoverState(orgSlug) {
521
+ function clearRoverState(roverId) {
471
522
  try {
472
- (0, import_node_fs2.unlinkSync)(statePath(orgSlug));
523
+ (0, import_node_fs2.unlinkSync)(statePath(roverId));
473
524
  } catch {
474
525
  }
475
526
  }
476
- function listRoverOrgs() {
527
+ function listRoverIds() {
477
528
  const dir = stateDir();
478
529
  if (!(0, import_node_fs2.existsSync)(dir)) return [];
479
530
  try {
480
531
  const fs = require("node:fs");
481
- return fs.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
532
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.includes(".tunnel.")).map((f) => f.slice(0, -5));
482
533
  } catch {
483
534
  return [];
484
535
  }
@@ -514,7 +565,8 @@ async function registerOrRefresh(params) {
514
565
  secret,
515
566
  tunnelUrl: params.tunnelUrl,
516
567
  installedAt: params.existing?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
517
- daemonPort: params.daemonPort
568
+ daemonPort: params.daemonPort,
569
+ installPat: params.installPat
518
570
  };
519
571
  saveRoverState(state);
520
572
  return {
@@ -530,6 +582,139 @@ var init_registration = __esm({
530
582
  }
531
583
  });
532
584
 
585
+ // package.json
586
+ var require_package = __commonJS({
587
+ "package.json"(exports2, module2) {
588
+ module2.exports = {
589
+ name: "@launchsecure/launch-kit",
590
+ version: "0.0.36",
591
+ description: "LaunchSecure toolkit \u2014 launch-sequencer (pipeline runner + terminal bridge), launch-radar (feedback webhook receiver), launch-chart (project graph MCP), launch-deck (visual playground MCP), launch-kit-beacon (feedback Web Component), launch-recall (file-watcher backup). launch-pod is the container image these run inside.",
592
+ license: "MIT",
593
+ author: "LaunchSecure - AutomateWithUs",
594
+ homepage: "https://automatewith.us",
595
+ repository: {
596
+ type: "git",
597
+ url: "https://github.com/launchsecure/launchsecure-v2",
598
+ directory: "packages/cli"
599
+ },
600
+ keywords: [
601
+ "launchsecure",
602
+ "launch-kit",
603
+ "launch-chart",
604
+ "launch-pod",
605
+ "project-graph",
606
+ "launch-deck",
607
+ "launch-kit-beacon",
608
+ "feedback",
609
+ "bug-report",
610
+ "screenshot",
611
+ "web-component",
612
+ "launch-recall",
613
+ "playground",
614
+ "visual",
615
+ "mcp",
616
+ "pipeline",
617
+ "ai",
618
+ "cli",
619
+ "file-watcher",
620
+ "backup",
621
+ "recovery"
622
+ ],
623
+ exports: {
624
+ "./beacon": {
625
+ types: "./dist/beacon/types/index.d.ts",
626
+ import: "./dist/beacon/beacon.mjs",
627
+ require: "./dist/beacon/beacon.umd.js"
628
+ }
629
+ },
630
+ engines: {
631
+ node: ">=18.0.0"
632
+ },
633
+ publishConfig: {
634
+ access: "public"
635
+ },
636
+ bin: {
637
+ "launch-kit": "./dist/server/init-entry.js",
638
+ "launch-sequencer": "./dist/server/cli.js",
639
+ "launch-radar": "./dist/server/launch-radar-entry.js",
640
+ "launch-chart": "./dist/server/graph-mcp-entry.js",
641
+ "launch-deck": "./dist/server/deck-mcp-entry.js",
642
+ "launch-recall": "./dist/server/recall-entry.js",
643
+ "launch-orbit": "./dist/server/orbit-entry.js",
644
+ "launch-course": "./dist/server/course-entry.js",
645
+ "launch-beacon": "./dist/server/beacon-monitor-entry.js",
646
+ "launch-rover": "./dist/server/rover-entry.js",
647
+ "launch-bot": "./dist/server/launch-bot-entry.js"
648
+ },
649
+ scripts: {
650
+ build: "pnpm build:client && pnpm build:chart-client && pnpm build:deck-client && pnpm build:council-client && pnpm build:beacon && pnpm build:server",
651
+ "build:beacon": "vite build --config vite.beacon.config.ts && tsc -p tsconfig.beacon.json --emitDeclarationOnly --outDir dist/beacon/types",
652
+ "test:beacon": "vitest run --config vite.beacon.config.ts",
653
+ "test:radar": "vitest run --config vite.radar.config.ts",
654
+ "test:chart": "vitest run --config vite.chart.test.config.ts",
655
+ "build:deck-client": "vite build --config vite.deck.config.ts",
656
+ "build:council-client": "vite build --config vite.council.config.ts",
657
+ "build:client": "vite build",
658
+ "build:chart-client": "vite build --config vite.chart.config.ts",
659
+ "build:server": "esbuild src/server/cli.ts src/server/fb-wizard.ts src/server/graph-mcp-entry.ts src/server/chart-serve.ts src/server/deck-mcp-entry.ts src/server/deck-serve.ts src/server/council-entry.ts src/server/council-serve.ts src/server/recall-entry.ts src/server/init-entry.ts src/server/orbit-entry.ts src/server/course-entry.ts src/server/beacon-monitor-entry.ts src/server/parse-worker-entry.ts src/server/radar-teardown-entry.ts src/server/radar-docker-init-entry.ts src/server/launch-radar-entry.ts src/server/rover-entry.ts src/server/launch-bot-entry.ts --bundle --platform=node --target=node18 --outdir=dist/server --external:node-pty --external:ws --external:typescript --external:web-tree-sitter --external:tree-sitter-typescript --external:cloudflared --external:pg --external:pg-native --external:pgsql-parser --external:libpg-query && rm -rf dist/server/public && cp -r ../claude-code-web/src/public dist/server/public && rm -rf dist/server/graph/queries && mkdir -p dist/server/graph && cp -r src/server/graph/queries dist/server/graph/queries",
660
+ "dev:client": "vite",
661
+ "dev:deck-serve": "cd ../.. && tsx watch packages/cli/src/server/deck-mcp-entry.ts serve",
662
+ "dev:chart": "pnpm build:server && pnpm build:chart-client && node dist/server/graph-mcp-entry.js serve",
663
+ "dev:server": "pnpm build:server && node dist/server/cli.js",
664
+ dev: 'pnpm build:server && concurrently -k -n client,server -c cyan,magenta "vite" "node dist/server/cli.js"',
665
+ prepublishOnly: "pnpm build"
666
+ },
667
+ files: [
668
+ "dist",
669
+ "prompts",
670
+ "scaffolds"
671
+ ],
672
+ dependencies: {
673
+ "cacheable-lookup": "^7.0.0",
674
+ cloudflared: "^0.7.1",
675
+ "html-to-image": "^1.11.13",
676
+ "node-pty": "^1.2.0-beta.12",
677
+ pg: "^8.13.0",
678
+ "pgsql-parser": "^17.9.15",
679
+ "tree-sitter": "^0.21.1",
680
+ "tree-sitter-typescript": "^0.23.2",
681
+ typescript: "^5.5.0",
682
+ undici: "^6.25.0",
683
+ "web-tree-sitter": "^0.24.7",
684
+ ws: "^8.18.0"
685
+ },
686
+ devDependencies: {
687
+ "@launchsecure/claude-code-web": "workspace:*",
688
+ "@launchsecure/ui": "workspace:*",
689
+ "@types/node": "^20.0.0",
690
+ "@types/pg": "^8.11.10",
691
+ "@types/react": "^18.3.12",
692
+ "@types/react-dom": "^18.3.1",
693
+ "@types/ws": "^8.5.10",
694
+ "@vitejs/plugin-react": "^4.3.4",
695
+ "@xterm/addon-fit": "^0.10.0",
696
+ "@xterm/addon-web-links": "^0.11.0",
697
+ "@xterm/xterm": "^5.5.0",
698
+ "@xyflow/react": "^12.10.2",
699
+ autoprefixer: "^10.4.27",
700
+ concurrently: "^9.2.1",
701
+ esbuild: "^0.28.0",
702
+ "happy-dom": "^20.8.8",
703
+ marked: "^15.0.0",
704
+ mermaid: "^11.4.0",
705
+ postcss: "^8.5.8",
706
+ react: "^18.3.1",
707
+ "react-dom": "^18.3.1",
708
+ "react-force-graph-2d": "^1.25.5",
709
+ "react-router-dom": "^6.28.0",
710
+ tailwindcss: "^3.4.19",
711
+ vite: "^5.4.11",
712
+ vitest: "^1.6.0"
713
+ }
714
+ };
715
+ }
716
+ });
717
+
533
718
  // ../../node_modules/.pnpm/cacheable-lookup@7.0.0/node_modules/cacheable-lookup/source/index.js
534
719
  var import_node_dns, import_node_util, import_node_os2, AsyncResolver, kCacheableLookupCreateConnection, kCacheableLookupInstance, kExpires, supportsALL, verifyAgent, map4to6, getIfaceInfo, isIterable, ignoreNoResultErrors, ttl, all, all4, all6, CacheableLookup;
535
720
  var init_source = __esm({
@@ -11455,7 +11640,7 @@ var require_mock_interceptor = __commonJS({
11455
11640
  var require_mock_client = __commonJS({
11456
11641
  "../../node_modules/.pnpm/undici@6.25.0/node_modules/undici/lib/mock/mock-client.js"(exports2, module2) {
11457
11642
  "use strict";
11458
- var { promisify: promisify2 } = require("node:util");
11643
+ var { promisify: promisify3 } = require("node:util");
11459
11644
  var Client = require_client();
11460
11645
  var { buildMockDispatch } = require_mock_utils();
11461
11646
  var {
@@ -11495,7 +11680,7 @@ var require_mock_client = __commonJS({
11495
11680
  return new MockInterceptor(opts, this[kDispatches]);
11496
11681
  }
11497
11682
  async [kClose]() {
11498
- await promisify2(this[kOriginalClose])();
11683
+ await promisify3(this[kOriginalClose])();
11499
11684
  this[kConnected] = 0;
11500
11685
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
11501
11686
  }
@@ -11508,7 +11693,7 @@ var require_mock_client = __commonJS({
11508
11693
  var require_mock_pool = __commonJS({
11509
11694
  "../../node_modules/.pnpm/undici@6.25.0/node_modules/undici/lib/mock/mock-pool.js"(exports2, module2) {
11510
11695
  "use strict";
11511
- var { promisify: promisify2 } = require("node:util");
11696
+ var { promisify: promisify3 } = require("node:util");
11512
11697
  var Pool = require_pool();
11513
11698
  var { buildMockDispatch } = require_mock_utils();
11514
11699
  var {
@@ -11548,7 +11733,7 @@ var require_mock_pool = __commonJS({
11548
11733
  return new MockInterceptor(opts, this[kDispatches]);
11549
11734
  }
11550
11735
  async [kClose]() {
11551
- await promisify2(this[kOriginalClose])();
11736
+ await promisify3(this[kOriginalClose])();
11552
11737
  this[kConnected] = 0;
11553
11738
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
11554
11739
  }
@@ -19288,17 +19473,17 @@ function createTunnel(opts) {
19288
19473
  const exhaustive = opts.provider;
19289
19474
  throw new Error(`[tunnel] unknown provider: ${String(exhaustive)}`);
19290
19475
  }
19291
- var import_node_fs3, import_node_events, import_promises2, import_undici, import_cloudflared, dnsResolver, dnsCache, dnsResilientDispatcher, BACKOFF_MIN_MS, BACKOFF_MAX_MS, SELFTEST_INTERVAL_MS, SELFTEST_TIMEOUT_MS, NAMED_FATAL_PATTERNS, CloudflaredTunnel;
19476
+ var import_node_fs3, import_node_events, import_promises3, import_undici, import_cloudflared, dnsResolver, dnsCache, dnsResilientDispatcher, BACKOFF_MIN_MS, BACKOFF_MAX_MS, SELFTEST_INTERVAL_MS, SELFTEST_TIMEOUT_MS, NAMED_FATAL_PATTERNS, CloudflaredTunnel;
19292
19477
  var init_tunnel = __esm({
19293
19478
  "src/server/tunnel/index.ts"() {
19294
19479
  "use strict";
19295
19480
  import_node_fs3 = require("node:fs");
19296
19481
  import_node_events = require("node:events");
19297
- import_promises2 = require("node:dns/promises");
19482
+ import_promises3 = require("node:dns/promises");
19298
19483
  init_source();
19299
19484
  import_undici = __toESM(require_undici());
19300
19485
  import_cloudflared = require("cloudflared");
19301
- dnsResolver = new import_promises2.Resolver();
19486
+ dnsResolver = new import_promises3.Resolver();
19302
19487
  dnsResolver.setServers(["1.1.1.1", "8.8.8.8"]);
19303
19488
  dnsCache = new CacheableLookup({ resolver: dnsResolver });
19304
19489
  dnsResilientDispatcher = new import_undici.Agent({
@@ -19530,6 +19715,262 @@ var init_hmac = __esm({
19530
19715
  }
19531
19716
  });
19532
19717
 
19718
+ // src/server/rover/pods.ts
19719
+ async function podsUp(state, body) {
19720
+ if (!state.installPat) {
19721
+ throw new PodError(
19722
+ "rover state is missing the install PAT \u2014 re-run `launch-rover setup` to refresh.",
19723
+ 500
19724
+ );
19725
+ }
19726
+ if (!body || typeof body !== "object") {
19727
+ throw new PodError("body must be a JSON object");
19728
+ }
19729
+ if (typeof body.projectSlug !== "string" || !DNS_SLUG_RE.test(body.projectSlug)) {
19730
+ throw new PodError("projectSlug must be a DNS-safe string (a-z, 0-9, -)");
19731
+ }
19732
+ if (typeof body.claudeCredentialsB64 !== "string" || body.claudeCredentialsB64.length < 16) {
19733
+ throw new PodError("claudeCredentialsB64 is required (base64-encoded Claude credentials JSON)");
19734
+ }
19735
+ const image = body.image ?? DEFAULT_IMAGE;
19736
+ const containerName = POD_NAME_PREFIX + body.projectSlug;
19737
+ const workspaceVol = `${VOLUME_PREFIX}${body.projectSlug}-workspace`;
19738
+ const claudeVol = `${VOLUME_PREFIX}${body.projectSlug}-claude`;
19739
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
19740
+ await stopAndRemove(containerName);
19741
+ const dockerConfigDir = body.skipPull ? null : setupDockerConfig();
19742
+ try {
19743
+ if (!body.skipPull) {
19744
+ await dockerRun(
19745
+ ["pull", image],
19746
+ dockerConfigDir ? { DOCKER_CONFIG: dockerConfigDir } : {}
19747
+ );
19748
+ }
19749
+ const runArgs = [
19750
+ "run",
19751
+ "-d",
19752
+ "--name",
19753
+ containerName,
19754
+ "--restart",
19755
+ "unless-stopped",
19756
+ // Lets the container reach the host's loopback. Built-in on Docker
19757
+ // Desktop (macOS/Windows); explicit on Linux. Required when LS runs
19758
+ // on the host as a dev server (LS_SERVER_URL=http://localhost:…).
19759
+ "--add-host",
19760
+ "host.docker.internal:host-gateway",
19761
+ "--label",
19762
+ `${MANAGED_LABEL}=true`,
19763
+ "--label",
19764
+ `launch-rover.projectSlug=${body.projectSlug}`,
19765
+ "--label",
19766
+ `launch-rover.createdAt=${createdAt}`,
19767
+ "-v",
19768
+ `${workspaceVol}:/workspace`,
19769
+ "-v",
19770
+ `${claudeVol}:/home/launchpod/.claude`
19771
+ ];
19772
+ if (body.podId) runArgs.push("--label", `launch-rover.podId=${body.podId}`);
19773
+ if (body.name) runArgs.push("--label", `launch-rover.name=${body.name}`);
19774
+ const envVars = {
19775
+ CLAUDE_CREDENTIALS_B64: body.claudeCredentialsB64,
19776
+ LS_PAT: state.installPat,
19777
+ LS_ORG_SLUG: state.orgSlug,
19778
+ LS_PROJECT_SLUG: body.projectSlug,
19779
+ LS_SERVER_URL: rewriteLocalhostForContainer(state.serverUrl)
19780
+ };
19781
+ if (body.services && Array.isArray(body.services)) {
19782
+ envVars.LAUNCHKIT_SERVICES = JSON.stringify(body.services);
19783
+ }
19784
+ if (body.cfBaseDomain) envVars.LAUNCHKIT_CF_BASE_DOMAIN = body.cfBaseDomain;
19785
+ if (body.env && typeof body.env === "object") {
19786
+ for (const [k, v] of Object.entries(body.env)) {
19787
+ if (typeof v === "string") envVars[k] = v;
19788
+ }
19789
+ }
19790
+ for (const [k, v] of Object.entries(envVars)) {
19791
+ runArgs.push("-e", `${k}=${v}`);
19792
+ }
19793
+ runArgs.push(image);
19794
+ const containerId = (await dockerRun(runArgs)).stdout.trim();
19795
+ return { podId: body.podId ?? null, projectSlug: body.projectSlug, containerId, image, createdAt };
19796
+ } finally {
19797
+ if (dockerConfigDir) (0, import_node_fs4.rmSync)(dockerConfigDir, { recursive: true, force: true });
19798
+ }
19799
+ }
19800
+ async function podsDown(_state, body) {
19801
+ if (!body || typeof body !== "object") {
19802
+ throw new PodError("body must be a JSON object");
19803
+ }
19804
+ if (typeof body.projectSlug !== "string" || !DNS_SLUG_RE.test(body.projectSlug)) {
19805
+ throw new PodError("projectSlug must be a DNS-safe string (a-z, 0-9, -)");
19806
+ }
19807
+ const mode = body.mode ?? "stop";
19808
+ if (mode !== "stop" && mode !== "remove" && mode !== "purge") {
19809
+ throw new PodError(`mode must be "stop" | "remove" | "purge" (got "${mode}")`);
19810
+ }
19811
+ const containerName = POD_NAME_PREFIX + body.projectSlug;
19812
+ const stopped = await dockerStopIfExists(containerName);
19813
+ let removed = false;
19814
+ let volumesRemoved = false;
19815
+ if (mode === "remove" || mode === "purge") {
19816
+ removed = await dockerRmIfExists(containerName);
19817
+ }
19818
+ if (mode === "purge") {
19819
+ const workspaceVol = `${VOLUME_PREFIX}${body.projectSlug}-workspace`;
19820
+ const claudeVol = `${VOLUME_PREFIX}${body.projectSlug}-claude`;
19821
+ const a = await dockerVolumeRmIfExists(workspaceVol);
19822
+ const b = await dockerVolumeRmIfExists(claudeVol);
19823
+ volumesRemoved = a || b;
19824
+ }
19825
+ return { projectSlug: body.projectSlug, mode, stopped, removed, volumesRemoved };
19826
+ }
19827
+ async function podsList(_state) {
19828
+ const { stdout: stdout2 } = await dockerRun([
19829
+ "ps",
19830
+ "-a",
19831
+ "--filter",
19832
+ `label=${MANAGED_LABEL}=true`,
19833
+ "--format",
19834
+ "{{json .}}"
19835
+ ]);
19836
+ const pods = [];
19837
+ for (const line of stdout2.split("\n")) {
19838
+ const trimmed = line.trim();
19839
+ if (!trimmed) continue;
19840
+ let row;
19841
+ try {
19842
+ row = JSON.parse(trimmed);
19843
+ } catch {
19844
+ continue;
19845
+ }
19846
+ pods.push(parsePsRow(row));
19847
+ }
19848
+ return { pods };
19849
+ }
19850
+ async function stopAndRemove(containerName) {
19851
+ await dockerStopIfExists(containerName);
19852
+ await dockerRmIfExists(containerName);
19853
+ }
19854
+ async function dockerStopIfExists(containerName) {
19855
+ try {
19856
+ await dockerRun(["stop", containerName]);
19857
+ return true;
19858
+ } catch (err) {
19859
+ if (isNoSuchContainer(err)) return false;
19860
+ throw err;
19861
+ }
19862
+ }
19863
+ async function dockerRmIfExists(containerName) {
19864
+ try {
19865
+ await dockerRun(["rm", containerName]);
19866
+ return true;
19867
+ } catch (err) {
19868
+ if (isNoSuchContainer(err)) return false;
19869
+ throw err;
19870
+ }
19871
+ }
19872
+ async function dockerVolumeRmIfExists(volumeName) {
19873
+ try {
19874
+ await dockerRun(["volume", "rm", volumeName]);
19875
+ return true;
19876
+ } catch (err) {
19877
+ if (isNoSuchVolume(err)) return false;
19878
+ throw err;
19879
+ }
19880
+ }
19881
+ function isNoSuchContainer(err) {
19882
+ const e = err;
19883
+ const text = `${e?.stderr ?? ""}${e?.message ?? ""}`.toLowerCase();
19884
+ return text.includes("no such container");
19885
+ }
19886
+ function isNoSuchVolume(err) {
19887
+ const e = err;
19888
+ const text = `${e?.stderr ?? ""}${e?.message ?? ""}`.toLowerCase();
19889
+ return text.includes("no such volume") || text.includes("get no such volume");
19890
+ }
19891
+ async function dockerRun(args, envOverlay = {}) {
19892
+ try {
19893
+ const { stdout: stdout2, stderr } = await execFileAsync("docker", args, {
19894
+ env: { ...process.env, ...envOverlay },
19895
+ maxBuffer: 32 * 1024 * 1024
19896
+ });
19897
+ return { stdout: stdout2, stderr };
19898
+ } catch (err) {
19899
+ const e = err;
19900
+ throw new PodError(
19901
+ `docker ${args[0]} failed: ${(e.stderr || e.message || "").trim()}`,
19902
+ 502
19903
+ );
19904
+ }
19905
+ }
19906
+ function setupDockerConfig() {
19907
+ const token = process.env.GHCR_PULL_TOKEN;
19908
+ if (!token) {
19909
+ console.log(
19910
+ "[rover] no GHCR_PULL_TOKEN set \u2014 pulling image anonymously (succeeds only if the image is public)"
19911
+ );
19912
+ return null;
19913
+ }
19914
+ const dir = (0, import_node_fs4.mkdtempSync)((0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "launch-rover-docker-"));
19915
+ const auth = Buffer.from(`${GHCR_USERNAME}:${token}`, "utf-8").toString("base64");
19916
+ const config = { auths: { [GHCR_REGISTRY]: { auth } } };
19917
+ (0, import_node_fs4.writeFileSync)((0, import_node_path3.join)(dir, "config.json"), JSON.stringify(config), { mode: 384 });
19918
+ return dir;
19919
+ }
19920
+ function parsePsRow(row) {
19921
+ const labels = parseLabels(row.Labels);
19922
+ return {
19923
+ podId: labels["launch-rover.podId"] ?? null,
19924
+ projectSlug: labels["launch-rover.projectSlug"] ?? null,
19925
+ name: labels["launch-rover.name"] ?? null,
19926
+ containerName: row.Names ?? "",
19927
+ containerId: row.ID ?? "",
19928
+ image: row.Image ?? "",
19929
+ state: row.State ?? "",
19930
+ status: row.Status ?? "",
19931
+ createdAt: labels["launch-rover.createdAt"] ?? row.CreatedAt ?? null
19932
+ };
19933
+ }
19934
+ function rewriteLocalhostForContainer(url) {
19935
+ return url.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(?=[:\/]|$)/i, "$1host.docker.internal");
19936
+ }
19937
+ function parseLabels(raw) {
19938
+ const out = {};
19939
+ if (!raw) return out;
19940
+ for (const pair of raw.split(",")) {
19941
+ const eq = pair.indexOf("=");
19942
+ if (eq <= 0) continue;
19943
+ out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
19944
+ }
19945
+ return out;
19946
+ }
19947
+ var import_node_child_process2, import_node_fs4, import_node_os3, import_node_path3, import_node_util2, execFileAsync, DEFAULT_IMAGE, GHCR_REGISTRY, GHCR_USERNAME, MANAGED_LABEL, POD_NAME_PREFIX, VOLUME_PREFIX, DNS_SLUG_RE, PodError;
19948
+ var init_pods = __esm({
19949
+ "src/server/rover/pods.ts"() {
19950
+ "use strict";
19951
+ import_node_child_process2 = require("node:child_process");
19952
+ import_node_fs4 = require("node:fs");
19953
+ import_node_os3 = require("node:os");
19954
+ import_node_path3 = require("node:path");
19955
+ import_node_util2 = require("node:util");
19956
+ execFileAsync = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
19957
+ DEFAULT_IMAGE = "ghcr.io/launchsecure/launch-pod:latest";
19958
+ GHCR_REGISTRY = "ghcr.io";
19959
+ GHCR_USERNAME = process.env.GHCR_PULL_USERNAME ?? "ghcr-pull";
19960
+ MANAGED_LABEL = "launch-rover.managed";
19961
+ POD_NAME_PREFIX = "launch-pod-";
19962
+ VOLUME_PREFIX = "lp-";
19963
+ DNS_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
19964
+ PodError = class extends Error {
19965
+ constructor(message, httpStatus = 400) {
19966
+ super(message);
19967
+ this.name = "PodError";
19968
+ this.httpStatus = httpStatus;
19969
+ }
19970
+ };
19971
+ }
19972
+ });
19973
+
19533
19974
  // src/server/rover/daemon.ts
19534
19975
  var daemon_exports = {};
19535
19976
  __export(daemon_exports, {
@@ -19537,33 +19978,33 @@ __export(daemon_exports, {
19537
19978
  startDaemon: () => startDaemon
19538
19979
  });
19539
19980
  function lockPath() {
19540
- return (0, import_node_path3.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, ROVER_LOCK_FILENAME);
19981
+ return (0, import_node_path4.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR, ROVER_LOCK_FILENAME);
19541
19982
  }
19542
19983
  function readRoverLock() {
19543
19984
  const p = lockPath();
19544
- if (!(0, import_node_fs4.existsSync)(p)) return null;
19985
+ if (!(0, import_node_fs5.existsSync)(p)) return null;
19545
19986
  try {
19546
- return JSON.parse((0, import_node_fs4.readFileSync)(p, "utf-8"));
19987
+ return JSON.parse((0, import_node_fs5.readFileSync)(p, "utf-8"));
19547
19988
  } catch {
19548
19989
  return null;
19549
19990
  }
19550
19991
  }
19551
19992
  function writeLock(lock) {
19552
- const dir = (0, import_node_path3.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR);
19553
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
19554
- (0, import_node_fs4.writeFileSync)(lockPath(), JSON.stringify(lock, null, 2) + "\n", "utf-8");
19993
+ const dir = (0, import_node_path4.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR);
19994
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
19995
+ (0, import_node_fs5.writeFileSync)(lockPath(), JSON.stringify(lock, null, 2) + "\n", "utf-8");
19555
19996
  }
19556
19997
  function clearLock() {
19557
19998
  try {
19558
- (0, import_node_fs4.unlinkSync)(lockPath());
19999
+ (0, import_node_fs5.unlinkSync)(lockPath());
19559
20000
  } catch {
19560
20001
  }
19561
20002
  }
19562
20003
  async function startDaemon(opts) {
19563
- const state = loadRoverState(opts.orgSlug);
20004
+ const state = loadRoverState(opts.roverId);
19564
20005
  if (!state) {
19565
20006
  throw new Error(
19566
- `No rover state for org "${opts.orgSlug}". Run \`launch-rover setup\` first.`
20007
+ `No rover state for id "${opts.roverId}". Run \`launch-rover setup\` first.`
19567
20008
  );
19568
20009
  }
19569
20010
  const port = opts.port ?? state.daemonPort;
@@ -19588,7 +20029,8 @@ async function startDaemon(opts) {
19588
20029
  cwd: process.cwd(),
19589
20030
  url: state.tunnelUrl,
19590
20031
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
19591
- orgSlug: opts.orgSlug
20032
+ roverId: state.roverId,
20033
+ orgSlug: state.orgSlug
19592
20034
  };
19593
20035
  writeLock(lock);
19594
20036
  console.log(`[rover] listening on 0.0.0.0:${port} (tunnel: ${state.tunnelUrl})`);
@@ -19633,7 +20075,33 @@ async function handleRequest(req, res, state) {
19633
20075
  console.warn(`[rover] ${method} ${path} \u2192 401 (${failure})`);
19634
20076
  return json(res, 401, { error: "invalid_signature" });
19635
20077
  }
19636
- return json(res, 501, { error: "not_implemented", path });
20078
+ let parsed = {};
20079
+ if (rawBody.length > 0) {
20080
+ try {
20081
+ parsed = JSON.parse(rawBody);
20082
+ } catch {
20083
+ return json(res, 400, { error: "malformed_json" });
20084
+ }
20085
+ }
20086
+ try {
20087
+ if (path === "/pods/up") {
20088
+ const result2 = await podsUp(state, parsed);
20089
+ return json(res, 200, result2);
20090
+ }
20091
+ if (path === "/pods/down") {
20092
+ const result2 = await podsDown(state, parsed);
20093
+ return json(res, 200, result2);
20094
+ }
20095
+ const result = await podsList(state);
20096
+ return json(res, 200, result);
20097
+ } catch (err) {
20098
+ if (err instanceof PodError) {
20099
+ console.warn(`[rover] ${method} ${path} \u2192 ${err.httpStatus} (${err.message})`);
20100
+ return json(res, err.httpStatus, { error: err.message });
20101
+ }
20102
+ console.error(`[rover] ${method} ${path} crashed:`, err);
20103
+ return json(res, 500, { error: err instanceof Error ? err.message : "internal_error" });
20104
+ }
19637
20105
  }
19638
20106
  return json(res, 404, { error: "not_found", path });
19639
20107
  }
@@ -19651,7 +20119,7 @@ function readBody(req) {
19651
20119
  });
19652
20120
  }
19653
20121
  async function sendHeartbeat(state) {
19654
- const payload = JSON.stringify({ organizationId: state.organizationId });
20122
+ const payload = JSON.stringify({ roverId: state.roverId });
19655
20123
  const { header } = buildSignatureHeader(state.secret, payload);
19656
20124
  const url = new URL("/api/rover/heartbeat", state.serverUrl);
19657
20125
  await postJson(url, payload, { "X-LS-Signature": header });
@@ -19690,18 +20158,19 @@ function postJson(url, body, extra) {
19690
20158
  req.end();
19691
20159
  });
19692
20160
  }
19693
- var import_node_fs4, import_node_http2, import_node_http3, import_node_https2, import_node_os3, import_node_path3, ROVER_LOCK_FILENAME;
20161
+ var import_node_fs5, import_node_http2, import_node_http3, import_node_https2, import_node_os4, import_node_path4, ROVER_LOCK_FILENAME;
19694
20162
  var init_daemon = __esm({
19695
20163
  "src/server/rover/daemon.ts"() {
19696
20164
  "use strict";
19697
- import_node_fs4 = require("node:fs");
20165
+ import_node_fs5 = require("node:fs");
19698
20166
  import_node_http2 = require("node:http");
19699
20167
  import_node_http3 = require("node:http");
19700
20168
  import_node_https2 = require("node:https");
19701
- import_node_os3 = require("node:os");
19702
- import_node_path3 = require("node:path");
20169
+ import_node_os4 = require("node:os");
20170
+ import_node_path4 = require("node:path");
19703
20171
  init_launch_kit_paths();
19704
20172
  init_hmac();
20173
+ init_pods();
19705
20174
  init_state();
19706
20175
  ROVER_LOCK_FILENAME = "launch-rover.lock";
19707
20176
  }
@@ -19748,7 +20217,7 @@ function printUsage() {
19748
20217
  "Usage: launch-rover setup --pat=<PAT> --org=<orgSlug> [options]",
19749
20218
  "",
19750
20219
  "Required:",
19751
- " --pat=<ls_pat_...> Setup PAT issued by LS (mcp:rover:setup scope)",
20220
+ " --pat=<ls_pat_...> Setup PAT issued by LS (rover:setup scope)",
19752
20221
  " --org=<slug> Organization slug the rover belongs to",
19753
20222
  "",
19754
20223
  "Optional:",
@@ -19756,15 +20225,27 @@ function printUsage() {
19756
20225
  " --name=<host-name> Human-readable rover name (default: hostname)",
19757
20226
  " --port=<n> Local daemon port (default: 52749)",
19758
20227
  " --auto-install Allow setup to install Docker via brew / get-docker.sh",
19759
- " --yes / -y Skip confirmation prompts (assume yes)",
20228
+ " --yes / -y Skip confirmation prompts (assume yes). Also auto-starts",
20229
+ " an installed-but-stopped Docker daemon without prompting.",
19760
20230
  " --detach Spawn the daemon as a detached process and exit",
19761
20231
  ""
19762
20232
  ].join("\n")
19763
20233
  );
19764
20234
  }
20235
+ function ensureNodeVersion() {
20236
+ const raw = process.versions.node;
20237
+ const major = Number.parseInt(raw.split(".")[0] ?? "", 10);
20238
+ if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
20239
+ throw new Error(
20240
+ `launch-rover requires Node >= ${MIN_NODE_MAJOR} (running ${raw}). Upgrade Node, then re-run \`launch-rover setup\`.`
20241
+ );
20242
+ }
20243
+ console.log(`[rover] node ok: v${raw}`);
20244
+ }
19765
20245
  async function runSetup(argv) {
19766
20246
  const args = parseArgs(argv);
19767
20247
  console.log(`[rover] setup org=${args.orgSlug} server=${args.serverUrl}`);
20248
+ ensureNodeVersion();
19768
20249
  await ensureDocker({ autoInstall: args.autoInstall, yes: args.yes });
19769
20250
  const dockerInfo = detectDocker();
19770
20251
  console.log(`[rover] docker ready: ${dockerInfo.version ?? "unknown"}`);
@@ -19773,26 +20254,33 @@ async function runSetup(argv) {
19773
20254
  pat: args.pat,
19774
20255
  orgSlug: args.orgSlug
19775
20256
  });
19776
- console.log("[rover] fetching CF config from LS\u2026");
20257
+ console.log("[rover] fetching rover config from LS\u2026");
19777
20258
  const lsConfig = await mcp.call("rover_get_config", {});
19778
- if (!lsConfig.cfApiToken || !lsConfig.cfAccountId || !lsConfig.cfZoneId) {
20259
+ if (!lsConfig.id) {
19779
20260
  throw new Error(
19780
- "LS rover config is missing Cloudflare credentials. Edit the rover in LS Org Settings \u2192 Rover and fill in the CF fields."
20261
+ "LS rover config did not return an id. PAT may not be bound to a specific rover \u2014 create the rover in LS Organization \u2192 Rovers and re-issue the install command."
19781
20262
  );
19782
20263
  }
19783
- console.log(`[rover] LS rover "${lsConfig.name}" (status: ${lsConfig.status})`);
19784
- const zoneName = await fetchZoneName(lsConfig.cfApiToken, lsConfig.cfZoneId);
19785
- console.log(`[rover] resolved CF zone: ${zoneName}`);
19786
- const stateDir2 = (0, import_node_path4.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR, "rover");
19787
- (0, import_node_fs5.mkdirSync)(stateDir2, { recursive: true });
19788
- const tunnelStateFile = (0, import_node_path4.join)(stateDir2, `${args.orgSlug}.tunnel.json`);
19789
- const subdomain = `rover-${args.orgSlug.replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 50)}`;
19790
- const tunnelName = `launch-rover-${args.orgSlug}`;
19791
- console.log(`[rover] provisioning CF tunnel ${tunnelName} \u2192 ${subdomain}.${zoneName}`);
20264
+ if (!lsConfig.cfApiToken || !lsConfig.cfDomainName) {
20265
+ throw new Error(
20266
+ "LS rover config is missing Cloudflare credentials. Connect a Cloudflare integration and set the rover's domain in LS Organization \u2192 Rovers."
20267
+ );
20268
+ }
20269
+ console.log(`[rover] LS rover "${lsConfig.name}" (id: ${lsConfig.id}, status: ${lsConfig.status})`);
20270
+ const roverId = lsConfig.id;
20271
+ const zone = await resolveZone(lsConfig.cfApiToken, lsConfig.cfDomainName);
20272
+ console.log(`[rover] resolved CF zone: ${zone.name} (account ${zone.accountId})`);
20273
+ const stateDir2 = (0, import_node_path5.join)((0, import_node_os5.homedir)(), LAUNCHSECURE_DIR, "rover");
20274
+ (0, import_node_fs6.mkdirSync)(stateDir2, { recursive: true });
20275
+ const tunnelStateFile = (0, import_node_path5.join)(stateDir2, `${roverId}.tunnel.json`);
20276
+ const idSuffix = roverId.replace(/[^a-z0-9]/gi, "").toLowerCase().slice(-8);
20277
+ const subdomain = `rover-${args.orgSlug.replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 30)}-${idSuffix}`;
20278
+ const tunnelName = `launch-rover-${roverId}`;
20279
+ console.log(`[rover] provisioning CF tunnel ${tunnelName} \u2192 ${subdomain}.${zone.name}`);
19792
20280
  const ingress = await provisionIngress({
19793
20281
  apiToken: lsConfig.cfApiToken,
19794
- accountId: lsConfig.cfAccountId,
19795
- zone: { id: lsConfig.cfZoneId, name: zoneName },
20282
+ accountId: zone.accountId,
20283
+ zone: { id: zone.id, name: zone.name },
19796
20284
  tunnelName,
19797
20285
  services: [{ name: subdomain, port: args.port }],
19798
20286
  stateFile: tunnelStateFile
@@ -19803,18 +20291,19 @@ async function runSetup(argv) {
19803
20291
  }
19804
20292
  const tunnelUrl = `https://${hostname}`;
19805
20293
  console.log(`[rover] tunnel public URL: ${tunnelUrl}`);
19806
- const existing = loadRoverState(args.orgSlug);
20294
+ const existing = loadRoverState(roverId);
19807
20295
  const outcome = await registerOrRefresh({
19808
20296
  mcp,
19809
20297
  orgSlug: args.orgSlug,
19810
20298
  serverUrl: args.serverUrl,
19811
20299
  tunnelUrl,
19812
20300
  daemonPort: args.port,
20301
+ installPat: args.pat,
19813
20302
  hostInfo: {
19814
- platform: (0, import_node_os4.platform)(),
19815
- hostname: args.name ?? (0, import_node_os4.hostname)(),
20303
+ platform: (0, import_node_os5.platform)(),
20304
+ hostname: args.name ?? (0, import_node_os5.hostname)(),
19816
20305
  dockerVersion: dockerInfo.version ?? void 0,
19817
- kitVersion: process.env.npm_package_version
20306
+ kitVersion: KIT_VERSION
19818
20307
  },
19819
20308
  existing
19820
20309
  });
@@ -19825,30 +20314,39 @@ async function runSetup(argv) {
19825
20314
  console.log("");
19826
20315
  if (outcome.approvalStatus === "pending_approval") {
19827
20316
  console.log("\u2713 Rover is live and registered, awaiting admin approval in LS.");
19828
- console.log(` ${args.serverUrl}/${args.orgSlug}/settings/rover`);
20317
+ console.log(` ${args.serverUrl}/${args.orgSlug}/rovers`);
19829
20318
  } else if (outcome.approvalStatus === "approved") {
19830
20319
  console.log("\u2713 Rover is live and already approved. Heartbeats will start within 60s.");
19831
20320
  } else {
19832
20321
  console.log(`! Rover state: ${outcome.approvalStatus}.`);
19833
20322
  }
19834
20323
  }
19835
- async function fetchZoneName(apiToken, zoneId) {
19836
- const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}`, {
19837
- headers: {
19838
- Authorization: `Bearer ${apiToken}`,
19839
- Accept: "application/json"
19840
- },
19841
- signal: AbortSignal.timeout(1e4)
19842
- });
20324
+ async function resolveZone(apiToken, domainName) {
20325
+ const res = await fetch(
20326
+ `https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(domainName)}`,
20327
+ {
20328
+ headers: {
20329
+ Authorization: `Bearer ${apiToken}`,
20330
+ Accept: "application/json"
20331
+ },
20332
+ signal: AbortSignal.timeout(1e4)
20333
+ }
20334
+ );
19843
20335
  if (!res.ok) {
19844
20336
  throw new Error(`CF zone lookup failed: HTTP ${res.status}`);
19845
20337
  }
19846
20338
  const body = await res.json();
19847
- if (!body.success || !body.result?.name) {
20339
+ if (!body.success) {
19848
20340
  const reason = body.errors?.[0]?.message ?? "unknown";
19849
20341
  throw new Error(`CF zone lookup failed: ${reason}`);
19850
20342
  }
19851
- return body.result.name;
20343
+ const zone = body.result?.[0];
20344
+ if (!zone?.id || !zone.account?.id) {
20345
+ throw new Error(
20346
+ `No Cloudflare zone found for "${domainName}". The API token may lack Zone:Read, or the domain isn't on this Cloudflare account.`
20347
+ );
20348
+ }
20349
+ return { id: zone.id, name: zone.name, accountId: zone.account.id };
19852
20350
  }
19853
20351
  async function launchDaemon(args, ingress, state) {
19854
20352
  if (!args.detach) {
@@ -19863,11 +20361,11 @@ async function launchDaemon(args, ingress, state) {
19863
20361
  });
19864
20362
  void tunnel.start();
19865
20363
  const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
19866
- await startDaemon2({ orgSlug: args.orgSlug, port: args.port });
20364
+ await startDaemon2({ roverId: state.roverId, port: args.port });
19867
20365
  return;
19868
20366
  }
19869
20367
  const argv0 = process.argv[1] ?? "launch-rover";
19870
- const child = (0, import_node_child_process2.spawn)(process.execPath, [argv0, "serve", `--org=${args.orgSlug}`], {
20368
+ const child = (0, import_node_child_process3.spawn)(process.execPath, [argv0, "serve", `--rover=${state.roverId}`], {
19871
20369
  detached: true,
19872
20370
  stdio: "ignore",
19873
20371
  env: {
@@ -19879,25 +20377,27 @@ async function launchDaemon(args, ingress, state) {
19879
20377
  child.unref();
19880
20378
  console.log(`[rover] daemon spawned (pid ${child.pid}, detached)`);
19881
20379
  }
19882
- function runSetupReset(orgSlug) {
19883
- clearRoverState(orgSlug);
20380
+ function runSetupReset(roverId) {
20381
+ clearRoverState(roverId);
19884
20382
  }
19885
- var import_node_os4, import_node_child_process2, import_node_path4, import_node_fs5, DEFAULT_SERVER, DEFAULT_DAEMON_PORT;
20383
+ var import_node_os5, import_node_child_process3, import_node_path5, import_node_fs6, KIT_VERSION, DEFAULT_SERVER, DEFAULT_DAEMON_PORT, MIN_NODE_MAJOR;
19886
20384
  var init_setup = __esm({
19887
20385
  "src/server/rover/setup.ts"() {
19888
20386
  "use strict";
19889
- import_node_os4 = require("node:os");
19890
- import_node_child_process2 = require("node:child_process");
19891
- import_node_path4 = require("node:path");
19892
- import_node_fs5 = require("node:fs");
20387
+ import_node_os5 = require("node:os");
20388
+ import_node_child_process3 = require("node:child_process");
20389
+ import_node_path5 = require("node:path");
20390
+ import_node_fs6 = require("node:fs");
19893
20391
  init_cf_ingress();
19894
20392
  init_launch_kit_paths();
19895
20393
  init_docker_install();
19896
20394
  init_mcp_client();
19897
20395
  init_registration();
19898
20396
  init_state();
20397
+ KIT_VERSION = require_package().version;
19899
20398
  DEFAULT_SERVER = "https://launchsecure-v2.vercel.app";
19900
20399
  DEFAULT_DAEMON_PORT = 52749;
20400
+ MIN_NODE_MAJOR = 18;
19901
20401
  }
19902
20402
  });
19903
20403
 
@@ -19907,41 +20407,41 @@ __export(teardown_exports, {
19907
20407
  runStatus: () => runStatus,
19908
20408
  runTeardown: () => runTeardown
19909
20409
  });
19910
- function parseArgs2(argv, requireOrg = true) {
19911
- let orgSlug;
20410
+ function parseArgs2(argv, requireRover = true) {
20411
+ let roverId;
19912
20412
  let force = false;
19913
20413
  for (const arg of argv) {
19914
- if (arg.startsWith("--org=")) orgSlug = arg.slice("--org=".length);
20414
+ if (arg.startsWith("--rover=")) roverId = arg.slice("--rover=".length);
19915
20415
  else if (arg === "--force" || arg === "-f") force = true;
19916
20416
  else if (arg === "--help" || arg === "-h") return null;
19917
20417
  else throw new Error(`Unknown argument: ${arg}`);
19918
20418
  }
19919
- if (!orgSlug) {
19920
- const known = listRoverOrgs();
20419
+ if (!roverId) {
20420
+ const known = listRoverIds();
19921
20421
  if (known.length === 1) {
19922
- orgSlug = known[0];
19923
- } else if (requireOrg) {
20422
+ roverId = known[0];
20423
+ } else if (requireRover) {
19924
20424
  const hint = known.length === 0 ? "no rovers found" : `multiple rovers found: ${known.join(", ")}`;
19925
- throw new Error(`--org=<slug> is required (${hint})`);
20425
+ throw new Error(`--rover=<id> is required (${hint})`);
19926
20426
  } else {
19927
20427
  return null;
19928
20428
  }
19929
20429
  }
19930
- return { orgSlug, force };
20430
+ return { roverId, force };
19931
20431
  }
19932
20432
  async function runTeardown(argv) {
19933
20433
  const parsed = parseArgs2(argv);
19934
20434
  if (!parsed) {
19935
- console.log("Usage: launch-rover teardown --org=<slug> [--force]");
20435
+ console.log("Usage: launch-rover teardown --rover=<id> [--force]");
19936
20436
  return;
19937
20437
  }
19938
- const state = loadRoverState(parsed.orgSlug);
20438
+ const state = loadRoverState(parsed.roverId);
19939
20439
  if (!state) {
19940
- console.log(`[rover] no local state for org "${parsed.orgSlug}" \u2014 nothing to tear down.`);
20440
+ console.log(`[rover] no local state for rover "${parsed.roverId}" \u2014 nothing to tear down.`);
19941
20441
  return;
19942
20442
  }
19943
20443
  const lock = readRoverLock();
19944
- if (lock && lock.orgSlug === parsed.orgSlug) {
20444
+ if (lock && lock.roverId === parsed.roverId) {
19945
20445
  if (isPidAlive(lock.pid)) {
19946
20446
  console.log(`[rover] stopping daemon (pid ${lock.pid})\u2026`);
19947
20447
  try {
@@ -19950,7 +20450,7 @@ async function runTeardown(argv) {
19950
20450
  }
19951
20451
  const deadline = Date.now() + 3e3;
19952
20452
  while (Date.now() < deadline && isPidAlive(lock.pid)) {
19953
- await sleep(100);
20453
+ await sleep2(100);
19954
20454
  }
19955
20455
  if (isPidAlive(lock.pid) && parsed.force) {
19956
20456
  console.log("[rover] daemon did not exit, sending SIGKILL");
@@ -19961,44 +20461,43 @@ async function runTeardown(argv) {
19961
20461
  }
19962
20462
  }
19963
20463
  }
19964
- clearRoverState(parsed.orgSlug);
19965
- const tunnelStateFile = (0, import_node_path5.join)(
19966
- (0, import_node_os5.homedir)(),
20464
+ clearRoverState(parsed.roverId);
20465
+ const tunnelStateFile = (0, import_node_path6.join)(
20466
+ (0, import_node_os6.homedir)(),
19967
20467
  LAUNCHSECURE_DIR,
19968
20468
  "rover",
19969
- `${parsed.orgSlug}.tunnel.json`
20469
+ `${parsed.roverId}.tunnel.json`
19970
20470
  );
19971
- if ((0, import_node_fs6.existsSync)(tunnelStateFile)) {
20471
+ if ((0, import_node_fs7.existsSync)(tunnelStateFile)) {
19972
20472
  try {
19973
- (0, import_node_fs6.unlinkSync)(tunnelStateFile);
20473
+ (0, import_node_fs7.unlinkSync)(tunnelStateFile);
19974
20474
  } catch {
19975
20475
  }
19976
20476
  }
19977
- console.log(`[rover] teardown complete for org "${parsed.orgSlug}".`);
20477
+ console.log(`[rover] teardown complete for rover "${parsed.roverId}".`);
19978
20478
  console.log(" \u2192 To fully reset on the LS side (revoke install PAT, allow re-create), use the");
19979
- console.log(` "Reset" button at ${state.serverUrl}/${parsed.orgSlug}/settings/rover`);
20479
+ console.log(` "Reset" button at ${state.serverUrl}/${state.orgSlug}/rovers`);
19980
20480
  }
19981
20481
  function runStatus(argv) {
19982
20482
  const parsed = parseArgs2(
19983
20483
  argv,
19984
- /* requireOrg */
20484
+ /* requireRover */
19985
20485
  false
19986
20486
  );
19987
- const orgs = parsed ? [parsed.orgSlug] : listRoverOrgs();
19988
- if (orgs.length === 0) {
20487
+ const rovers = parsed ? [parsed.roverId] : listRoverIds();
20488
+ if (rovers.length === 0) {
19989
20489
  console.log("[rover] no rovers configured on this host.");
19990
20490
  return;
19991
20491
  }
19992
- for (const orgSlug of orgs) {
19993
- const state = loadRoverState(orgSlug);
20492
+ for (const roverId of rovers) {
20493
+ const state = loadRoverState(roverId);
19994
20494
  if (!state) {
19995
- console.log(`[rover] ${orgSlug}: no state file`);
20495
+ console.log(`[rover] ${roverId}: no state file`);
19996
20496
  continue;
19997
20497
  }
19998
20498
  const lock = readRoverLock();
19999
- const live = lock && lock.orgSlug === orgSlug && isPidAlive(lock.pid);
20000
- console.log(`\u2500\u2500 ${orgSlug} \u2500\u2500`);
20001
- console.log(` rover id: ${state.roverId}`);
20499
+ const live = lock && lock.roverId === roverId && isPidAlive(lock.pid);
20500
+ console.log(`\u2500\u2500 ${roverId} (${state.orgSlug}) \u2500\u2500`);
20002
20501
  console.log(` org id: ${state.organizationId}`);
20003
20502
  console.log(` server: ${state.serverUrl}`);
20004
20503
  console.log(` tunnel url: ${state.tunnelUrl}`);
@@ -20019,16 +20518,16 @@ function isPidAlive(pid) {
20019
20518
  return false;
20020
20519
  }
20021
20520
  }
20022
- function sleep(ms) {
20521
+ function sleep2(ms) {
20023
20522
  return new Promise((resolve) => setTimeout(resolve, ms));
20024
20523
  }
20025
- var import_node_fs6, import_node_os5, import_node_path5;
20524
+ var import_node_fs7, import_node_os6, import_node_path6;
20026
20525
  var init_teardown = __esm({
20027
20526
  "src/server/rover/teardown.ts"() {
20028
20527
  "use strict";
20029
- import_node_fs6 = require("node:fs");
20030
- import_node_os5 = require("node:os");
20031
- import_node_path5 = require("node:path");
20528
+ import_node_fs7 = require("node:fs");
20529
+ import_node_os6 = require("node:os");
20530
+ import_node_path6 = require("node:path");
20032
20531
  init_launch_kit_paths();
20033
20532
  init_daemon();
20034
20533
  init_state();
@@ -20049,9 +20548,10 @@ function printUsage2() {
20049
20548
  " setup --pat=<PAT> --org=<slug> [options]",
20050
20549
  " install Docker, provision CF tunnel,",
20051
20550
  " register with LaunchSecure, start daemon",
20052
- " serve [--org=<slug>] start the rover HTTP daemon (foreground)",
20053
- " status [--org=<slug>] print state + daemon liveness",
20054
- " teardown --org=<slug> stop daemon and clear local state",
20551
+ " serve [--rover=<id>] start the rover HTTP daemon (foreground).",
20552
+ " With one local rover, --rover is optional.",
20553
+ " status [--rover=<id>] print state + daemon liveness",
20554
+ " teardown --rover=<id> stop daemon and clear local state",
20055
20555
  " (does NOT delete the LS RoverInstance \u2014 use",
20056
20556
  " the Reset button in LS for that)",
20057
20557
  "",
@@ -20074,21 +20574,21 @@ async function main() {
20074
20574
  return;
20075
20575
  }
20076
20576
  case "serve": {
20077
- const orgSlug = argv.find((a) => a.startsWith("--org="))?.slice("--org=".length);
20078
- if (!orgSlug) {
20079
- const { listRoverOrgs: listRoverOrgs2 } = await Promise.resolve().then(() => (init_state(), state_exports));
20080
- const known = listRoverOrgs2();
20577
+ const roverId = argv.find((a) => a.startsWith("--rover="))?.slice("--rover=".length);
20578
+ if (!roverId) {
20579
+ const { listRoverIds: listRoverIds2 } = await Promise.resolve().then(() => (init_state(), state_exports));
20580
+ const known = listRoverIds2();
20081
20581
  if (known.length !== 1) {
20082
20582
  throw new Error(
20083
- `serve needs --org=<slug>${known.length ? ` (known: ${known.join(", ")})` : " (no rovers configured)"}`
20583
+ `serve needs --rover=<id>${known.length ? ` (known: ${known.join(", ")})` : " (no rovers configured)"}`
20084
20584
  );
20085
20585
  }
20086
20586
  const { startDaemon: startDaemon3 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
20087
- await startDaemon3({ orgSlug: known[0] });
20587
+ await startDaemon3({ roverId: known[0] });
20088
20588
  return;
20089
20589
  }
20090
20590
  const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
20091
- await startDaemon2({ orgSlug });
20591
+ await startDaemon2({ roverId });
20092
20592
  return;
20093
20593
  }
20094
20594
  case "status": {