@tenux/cli 0.0.21 → 0.0.22

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.
Files changed (2) hide show
  1. package/dist/cli.js +373 -84
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,12 +13,12 @@ import {
13
13
 
14
14
  // src/cli.ts
15
15
  import { Command } from "commander";
16
- import chalk2 from "chalk";
16
+ import chalk3 from "chalk";
17
17
  import { hostname } from "os";
18
18
  import { homedir } from "os";
19
19
  import { join as join2, dirname } from "path";
20
- import { existsSync as existsSync2, readFileSync } from "fs";
21
- import { execSync as execSync2 } from "child_process";
20
+ import { existsSync as existsSync3, readFileSync } from "fs";
21
+ import { execSync as execSync3 } from "child_process";
22
22
  import { fileURLToPath } from "url";
23
23
  import { createClient } from "@supabase/supabase-js";
24
24
 
@@ -65,7 +65,7 @@ function installCommand(pm) {
65
65
  }
66
66
  }
67
67
  function run(cmd, args, cwd, timeoutMs = 3e5) {
68
- return new Promise((resolve2, reject) => {
68
+ return new Promise((resolve3, reject) => {
69
69
  const isWindows = process.platform === "win32";
70
70
  const proc = spawn(cmd, args, {
71
71
  cwd,
@@ -82,7 +82,7 @@ function run(cmd, args, cwd, timeoutMs = 3e5) {
82
82
  stderr += d.toString();
83
83
  });
84
84
  proc.on("close", (code) => {
85
- if (code === 0) resolve2(stdout);
85
+ if (code === 0) resolve3(stdout);
86
86
  else reject(new Error(stderr.trim().slice(0, 500) || `Exit code ${code}`));
87
87
  });
88
88
  proc.on("error", reject);
@@ -380,6 +380,293 @@ async function handleProjectDelete(command, supabase) {
380
380
  }
381
381
  }
382
382
 
383
+ // src/handlers/server.ts
384
+ import { spawn as spawn2, exec, execSync as execSync2 } from "child_process";
385
+ import { resolve as resolve2 } from "path";
386
+ import { existsSync as existsSync2 } from "fs";
387
+ import chalk2 from "chalk";
388
+ var MAX_LOG_LINES = 500;
389
+ var LOG_PUSH_INTERVAL = 2e3;
390
+ var runningServers = /* @__PURE__ */ new Map();
391
+ function makeKey(projectName, commandName) {
392
+ return `${projectName}::${commandName}`;
393
+ }
394
+ function killTree(child) {
395
+ if (child.exitCode !== null) return;
396
+ const pid = child.pid;
397
+ if (!pid) {
398
+ child.kill("SIGTERM");
399
+ return;
400
+ }
401
+ try {
402
+ if (process.platform === "win32") {
403
+ execSync2(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
404
+ } else {
405
+ process.kill(-pid, "SIGTERM");
406
+ }
407
+ } catch {
408
+ child.kill("SIGTERM");
409
+ }
410
+ }
411
+ var tailscalePorts = /* @__PURE__ */ new Set();
412
+ function setupTailscaleServe(port) {
413
+ if (tailscalePorts.has(port)) return;
414
+ tailscalePorts.add(port);
415
+ exec(`tailscale serve --bg --https=${port} localhost:${port}`, (err) => {
416
+ if (err) {
417
+ console.log(chalk2.yellow("!"), `[tailscale] Failed for port ${port}: ${err.message}`);
418
+ tailscalePorts.delete(port);
419
+ } else {
420
+ console.log(chalk2.green("\u2713"), `[tailscale] HTTPS proxy on :${port}`);
421
+ }
422
+ });
423
+ }
424
+ function teardownTailscaleServe(port) {
425
+ if (!tailscalePorts.has(port)) return;
426
+ tailscalePorts.delete(port);
427
+ exec(`tailscale serve --https=${port} off`, (err) => {
428
+ if (err) {
429
+ console.log(chalk2.yellow("!"), `[tailscale] Failed to remove :${port}: ${err.message}`);
430
+ } else {
431
+ console.log(chalk2.dim(" [tailscale] Removed :${port}"));
432
+ }
433
+ });
434
+ }
435
+ function getTailscaleHostname() {
436
+ try {
437
+ const output = execSync2("tailscale status --json", { timeout: 5e3 }).toString();
438
+ const status = JSON.parse(output);
439
+ return status.Self?.DNSName?.replace(/\.$/, "") ?? null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+ async function handleServerStart(command, supabase) {
445
+ const payload = command.payload;
446
+ const config = loadConfig();
447
+ const fullPath = payload.project_path.startsWith("/") || payload.project_path.match(/^[A-Z]:\\/i) ? payload.project_path : resolve2(config.projectsDir, payload.project_path);
448
+ if (!existsSync2(fullPath)) {
449
+ throw new Error(`Project path not found: ${fullPath}`);
450
+ }
451
+ const key = makeKey(payload.project_name, payload.command_name);
452
+ const existing = runningServers.get(key);
453
+ if (existing) {
454
+ clearInterval(existing.logInterval);
455
+ killTree(existing.process);
456
+ runningServers.delete(key);
457
+ }
458
+ const { data: instance, error: upsertErr } = await supabase.from("server_instances").upsert(
459
+ {
460
+ user_id: command.user_id,
461
+ device_id: command.device_id,
462
+ project_id: payload.project_id,
463
+ command_name: payload.command_name,
464
+ command: payload.command,
465
+ status: "starting",
466
+ port: payload.port ?? null,
467
+ pid: null,
468
+ detected_port: null,
469
+ tailscale_url: null,
470
+ logs: [],
471
+ error_message: null,
472
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
473
+ stopped_at: null,
474
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
475
+ },
476
+ { onConflict: "device_id,project_id,command_name" }
477
+ ).select().single();
478
+ if (upsertErr || !instance) {
479
+ throw new Error(`Failed to create server instance: ${upsertErr?.message}`);
480
+ }
481
+ console.log(chalk2.blue("\u2192"), `Starting server: ${payload.project_name}::${payload.command_name}`);
482
+ console.log(chalk2.dim(` cmd: ${payload.command}`));
483
+ console.log(chalk2.dim(` cwd: ${fullPath}`));
484
+ const env = { ...process.env, FORCE_COLOR: "0" };
485
+ delete env.PORT;
486
+ delete env.NODE_OPTIONS;
487
+ const parts = payload.command.split(/\s+/).filter(Boolean);
488
+ const bin = parts[0];
489
+ const cmdArgs = parts.slice(1);
490
+ const isWindows = process.platform === "win32";
491
+ const child = spawn2(bin, cmdArgs, {
492
+ cwd: fullPath,
493
+ shell: isWindows,
494
+ stdio: ["ignore", "pipe", "pipe"],
495
+ env
496
+ });
497
+ const logs = [];
498
+ let detectedPort = false;
499
+ let logDirty = false;
500
+ const pushLog = (text) => {
501
+ const lines = text.split("\n");
502
+ for (const line of lines) {
503
+ if (line.trim()) {
504
+ logs.push(line);
505
+ if (logs.length > MAX_LOG_LINES) logs.shift();
506
+ logDirty = true;
507
+ if (!detectedPort) {
508
+ const portMatch = line.match(/(?:Network|Local):\s+https?:\/\/[^:]+:(\d+)/i) || line.match(/listening (?:on|at) (?:https?:\/\/)?[^:]*:(\d+)/i) || line.match(/started (?:on|at) (?:https?:\/\/)?[^:]*:(\d+)/i);
509
+ if (portMatch) {
510
+ const actualPort = parseInt(portMatch[1], 10);
511
+ detectedPort = true;
512
+ managed.detectedPort = actualPort;
513
+ setupTailscaleServe(actualPort);
514
+ const hostname2 = getTailscaleHostname();
515
+ const tsUrl = hostname2 ? `https://${hostname2}:${actualPort}` : null;
516
+ supabase.from("server_instances").update({
517
+ status: "running",
518
+ detected_port: actualPort,
519
+ tailscale_url: tsUrl,
520
+ pid: child.pid ?? null,
521
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
522
+ }).eq("id", instance.id).then(() => {
523
+ console.log(chalk2.green("\u2713"), `Server running on :${actualPort}`);
524
+ });
525
+ }
526
+ }
527
+ }
528
+ }
529
+ };
530
+ child.stdout.on("data", (chunk) => pushLog(chunk.toString()));
531
+ child.stderr.on("data", (chunk) => pushLog(chunk.toString()));
532
+ const logInterval = setInterval(async () => {
533
+ if (!logDirty) return;
534
+ logDirty = false;
535
+ await supabase.from("server_instances").update({
536
+ logs: logs.slice(-100),
537
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
538
+ }).eq("id", instance.id);
539
+ }, LOG_PUSH_INTERVAL);
540
+ const managed = {
541
+ instanceId: instance.id,
542
+ projectName: payload.project_name,
543
+ commandName: payload.command_name,
544
+ command: payload.command,
545
+ process: child,
546
+ logs,
547
+ logInterval
548
+ };
549
+ child.on("close", async (code) => {
550
+ clearInterval(logInterval);
551
+ logs.push(`[Process exited with code ${code}]`);
552
+ if (managed.detectedPort) {
553
+ teardownTailscaleServe(managed.detectedPort);
554
+ }
555
+ runningServers.delete(key);
556
+ await supabase.from("server_instances").update({
557
+ status: code === 0 ? "stopped" : "error",
558
+ error_message: code !== 0 ? `Exited with code ${code}` : null,
559
+ logs: logs.slice(-100),
560
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
561
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
562
+ }).eq("id", instance.id);
563
+ console.log(
564
+ code === 0 ? chalk2.dim(" Server stopped") : chalk2.yellow("!"),
565
+ `${payload.project_name}::${payload.command_name} exited (code ${code})`
566
+ );
567
+ });
568
+ runningServers.set(key, managed);
569
+ await supabase.from("server_instances").update({
570
+ pid: child.pid ?? null,
571
+ status: "starting",
572
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
573
+ }).eq("id", instance.id);
574
+ setTimeout(async () => {
575
+ if (!detectedPort) {
576
+ const configuredPort = payload.port;
577
+ if (configuredPort) {
578
+ managed.detectedPort = configuredPort;
579
+ setupTailscaleServe(configuredPort);
580
+ const hostname2 = getTailscaleHostname();
581
+ const tsUrl = hostname2 ? `https://${hostname2}:${configuredPort}` : null;
582
+ await supabase.from("server_instances").update({
583
+ status: "running",
584
+ detected_port: configuredPort,
585
+ tailscale_url: tsUrl,
586
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
587
+ }).eq("id", instance.id);
588
+ } else {
589
+ await supabase.from("server_instances").update({ status: "running", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", instance.id);
590
+ }
591
+ }
592
+ }, 8e3);
593
+ await supabase.from("commands").update({
594
+ status: "done",
595
+ result: { server_instance_id: instance.id },
596
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
597
+ }).eq("id", command.id);
598
+ }
599
+ async function handleServerStop(command, supabase) {
600
+ const payload = command.payload;
601
+ const key = makeKey(payload.project_name, payload.command_name);
602
+ const managed = runningServers.get(key);
603
+ if (managed) {
604
+ console.log(chalk2.blue("\u2192"), `Stopping server: ${key}`);
605
+ clearInterval(managed.logInterval);
606
+ killTree(managed.process);
607
+ } else {
608
+ if (payload.server_instance_id) {
609
+ await supabase.from("server_instances").update({
610
+ status: "stopped",
611
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
612
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
613
+ }).eq("id", payload.server_instance_id);
614
+ }
615
+ }
616
+ await supabase.from("commands").update({
617
+ status: "done",
618
+ result: { stopped: true },
619
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
620
+ }).eq("id", command.id);
621
+ }
622
+ async function handleServerStatus(command, supabase) {
623
+ const config = loadConfig();
624
+ const servers = [];
625
+ for (const [key, managed] of runningServers) {
626
+ servers.push({
627
+ key,
628
+ running: managed.process.exitCode === null,
629
+ pid: managed.process.pid,
630
+ detectedPort: managed.detectedPort,
631
+ logCount: managed.logs.length
632
+ });
633
+ }
634
+ await supabase.from("commands").update({
635
+ status: "done",
636
+ result: { servers, deviceId: config.deviceId },
637
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
638
+ }).eq("id", command.id);
639
+ }
640
+ async function cleanupServers(supabase, deviceId) {
641
+ for (const [key, managed] of runningServers) {
642
+ console.log(chalk2.dim(` Stopping server: ${key}`));
643
+ clearInterval(managed.logInterval);
644
+ if (managed.process.exitCode === null) {
645
+ killTree(managed.process);
646
+ }
647
+ if (managed.detectedPort) {
648
+ teardownTailscaleServe(managed.detectedPort);
649
+ }
650
+ }
651
+ runningServers.clear();
652
+ await supabase.from("server_instances").update({
653
+ status: "stopped",
654
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
655
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
656
+ }).eq("device_id", deviceId).in("status", ["starting", "running"]);
657
+ }
658
+ async function cleanupStaleServers(supabase, deviceId) {
659
+ const { data, error } = await supabase.from("server_instances").update({
660
+ status: "stopped",
661
+ error_message: "Agent restarted",
662
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
663
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
664
+ }).eq("device_id", deviceId).in("status", ["starting", "running"]).select("id");
665
+ if (data && data.length > 0) {
666
+ console.log(chalk2.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
667
+ }
668
+ }
669
+
383
670
  // src/cli.ts
384
671
  var __dirname = dirname(fileURLToPath(import.meta.url));
385
672
  var pkg = JSON.parse(readFileSync(join2(__dirname, "..", "package.json"), "utf-8"));
@@ -389,30 +676,30 @@ function detectClaudeCode() {
389
676
  let installed = false;
390
677
  try {
391
678
  if (process.platform === "win32") {
392
- execSync2("where claude", { stdio: "ignore" });
679
+ execSync3("where claude", { stdio: "ignore" });
393
680
  } else {
394
- execSync2("which claude", { stdio: "ignore" });
681
+ execSync3("which claude", { stdio: "ignore" });
395
682
  }
396
683
  installed = true;
397
684
  } catch {
398
685
  installed = false;
399
686
  }
400
- const authenticated = existsSync2(join2(homedir(), ".claude"));
687
+ const authenticated = existsSync3(join2(homedir(), ".claude"));
401
688
  return { installed, authenticated };
402
689
  }
403
690
  async function sleep(ms) {
404
- return new Promise((resolve2) => setTimeout(resolve2, ms));
691
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
405
692
  }
406
693
  program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
407
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
694
+ console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
408
695
  const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
409
- console.log(chalk2.dim(" App:"), appUrl);
696
+ console.log(chalk3.dim(" App:"), appUrl);
410
697
  const deviceName = hostname();
411
698
  const platform = process.platform;
412
- console.log(chalk2.dim(" Device:"), deviceName);
413
- console.log(chalk2.dim(" Platform:"), platform);
699
+ console.log(chalk3.dim(" Device:"), deviceName);
700
+ console.log(chalk3.dim(" Platform:"), platform);
414
701
  console.log();
415
- console.log(chalk2.dim(" Requesting device code..."));
702
+ console.log(chalk3.dim(" Requesting device code..."));
416
703
  let code;
417
704
  let expiresAt;
418
705
  let supabaseUrl;
@@ -429,7 +716,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
429
716
  });
430
717
  if (!res.ok) {
431
718
  const text = await res.text();
432
- console.log(chalk2.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
719
+ console.log(chalk3.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
433
720
  process.exit(1);
434
721
  }
435
722
  const data = await res.json();
@@ -439,26 +726,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
439
726
  supabaseAnonKey = data.supabase_anon_key;
440
727
  } catch (err) {
441
728
  console.log(
442
- chalk2.red(" \u2717"),
729
+ chalk3.red(" \u2717"),
443
730
  `Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
444
731
  );
445
732
  process.exit(1);
446
733
  }
447
734
  console.log();
448
- console.log(chalk2.bold.cyan(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
449
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
450
- console.log(chalk2.bold.cyan(` \u2502 Code: ${chalk2.white.bold(code)} \u2502`));
451
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
452
- console.log(chalk2.bold.cyan(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
735
+ console.log(chalk3.bold.cyan(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
736
+ console.log(chalk3.bold.cyan(` \u2502 \u2502`));
737
+ console.log(chalk3.bold.cyan(` \u2502 Code: ${chalk3.white.bold(code)} \u2502`));
738
+ console.log(chalk3.bold.cyan(` \u2502 \u2502`));
739
+ console.log(chalk3.bold.cyan(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
453
740
  console.log();
454
741
  const linkUrl = `${appUrl}/link?code=${code}`;
455
- console.log(chalk2.dim(" Opening browser to:"), chalk2.underline(linkUrl));
742
+ console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
456
743
  try {
457
744
  const open = (await import("open")).default;
458
745
  await open(linkUrl);
459
746
  } catch {
460
- console.log(chalk2.yellow(" !"), "Could not open browser automatically.");
461
- console.log(chalk2.dim(" Open this URL manually:"), chalk2.underline(linkUrl));
747
+ console.log(chalk3.yellow(" !"), "Could not open browser automatically.");
748
+ console.log(chalk3.dim(" Open this URL manually:"), chalk3.underline(linkUrl));
462
749
  }
463
750
  console.log();
464
751
  const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -470,11 +757,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
470
757
  const expiresTime = new Date(expiresAt).getTime();
471
758
  while (!approved) {
472
759
  if (Date.now() > expiresTime) {
473
- console.log(chalk2.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
760
+ console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
474
761
  process.exit(1);
475
762
  }
476
763
  process.stdout.write(
477
- `\r ${chalk2.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
764
+ `\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
478
765
  );
479
766
  dotIndex++;
480
767
  await sleep(2e3);
@@ -492,16 +779,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
492
779
  userId = data.user_id || "";
493
780
  approved = true;
494
781
  } else if (data.status === "expired") {
495
- console.log(chalk2.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
782
+ console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
496
783
  process.exit(1);
497
784
  }
498
785
  } catch {
499
786
  }
500
787
  }
501
788
  process.stdout.write("\r" + " ".repeat(60) + "\r");
502
- console.log(chalk2.green(" \u2713"), "Device approved!");
789
+ console.log(chalk3.green(" \u2713"), "Device approved!");
503
790
  console.log();
504
- console.log(chalk2.dim(" Exchanging token for session..."));
791
+ console.log(chalk3.dim(" Exchanging token for session..."));
505
792
  let accessToken = "";
506
793
  let refreshToken = "";
507
794
  try {
@@ -513,11 +800,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
513
800
  type: "magiclink"
514
801
  });
515
802
  if (error) {
516
- console.log(chalk2.red(" \u2717"), `Auth failed: ${error.message}`);
803
+ console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
517
804
  process.exit(1);
518
805
  }
519
806
  if (!session.session) {
520
- console.log(chalk2.red(" \u2717"), "No session returned from auth exchange.");
807
+ console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
521
808
  process.exit(1);
522
809
  }
523
810
  accessToken = session.session.access_token;
@@ -525,12 +812,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
525
812
  userId = session.session.user?.id || userId;
526
813
  } catch (err) {
527
814
  console.log(
528
- chalk2.red(" \u2717"),
815
+ chalk3.red(" \u2717"),
529
816
  `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
530
817
  );
531
818
  process.exit(1);
532
819
  }
533
- console.log(chalk2.green(" \u2713"), "Session established.");
820
+ console.log(chalk3.green(" \u2713"), "Session established.");
534
821
  const config = {
535
822
  appUrl,
536
823
  supabaseUrl,
@@ -543,66 +830,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
543
830
  projectsDir: join2(homedir(), ".tenux", "projects")
544
831
  };
545
832
  saveConfig(config);
546
- console.log(chalk2.green(" \u2713"), "Config saved to", chalk2.dim(getConfigPath()));
833
+ console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
547
834
  console.log();
548
835
  const claude = detectClaudeCode();
549
836
  if (claude.installed) {
550
- console.log(chalk2.green(" \u2713"), "Claude Code detected");
837
+ console.log(chalk3.green(" \u2713"), "Claude Code detected");
551
838
  if (claude.authenticated) {
552
- console.log(chalk2.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
839
+ console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
553
840
  } else {
554
841
  console.log(
555
- chalk2.yellow(" !"),
842
+ chalk3.yellow(" !"),
556
843
  "Claude Code found but ~/.claude not detected.",
557
- chalk2.dim("Run `claude login` to authenticate.")
844
+ chalk3.dim("Run `claude login` to authenticate.")
558
845
  );
559
846
  }
560
847
  } else {
561
- console.log(chalk2.yellow(" !"), "Claude Code not found.");
848
+ console.log(chalk3.yellow(" !"), "Claude Code not found.");
562
849
  console.log(
563
- chalk2.dim(" Install it:"),
564
- chalk2.underline("https://docs.anthropic.com/en/docs/claude-code")
850
+ chalk3.dim(" Install it:"),
851
+ chalk3.underline("https://docs.anthropic.com/en/docs/claude-code")
565
852
  );
566
853
  console.log(
567
- chalk2.dim(" Then run:"),
568
- chalk2.cyan("claude login")
854
+ chalk3.dim(" Then run:"),
855
+ chalk3.cyan("claude login")
569
856
  );
570
857
  }
571
858
  console.log();
572
- console.log(chalk2.dim(" Device:"), deviceName);
573
- console.log(chalk2.dim(" Device ID:"), deviceId);
574
- console.log(chalk2.dim(" User ID:"), userId);
575
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
859
+ console.log(chalk3.dim(" Device:"), deviceName);
860
+ console.log(chalk3.dim(" Device ID:"), deviceId);
861
+ console.log(chalk3.dim(" User ID:"), userId);
862
+ console.log(chalk3.dim(" Projects:"), config.projectsDir);
576
863
  console.log();
577
864
  console.log(
578
- chalk2.dim(" Next: start the agent:"),
579
- chalk2.cyan("tenux start")
865
+ chalk3.dim(" Next: start the agent:"),
866
+ chalk3.cyan("tenux start")
580
867
  );
581
868
  console.log();
582
869
  });
583
870
  program.command("start").description("Start the agent and listen for commands").action(async () => {
584
871
  if (!configExists()) {
585
- console.log(chalk2.red("\u2717"), "Not logged in. Run `tenux login` first.");
872
+ console.log(chalk3.red("\u2717"), "Not logged in. Run `tenux login` first.");
586
873
  process.exit(1);
587
874
  }
588
875
  const claude = detectClaudeCode();
589
876
  if (!claude.installed) {
590
- console.log(chalk2.red("\u2717"), "Claude Code not found.");
591
- console.log(chalk2.dim(" Install it:"), chalk2.cyan("npm i -g @anthropic-ai/claude-code"));
592
- console.log(chalk2.dim(" Then run:"), chalk2.cyan("claude login"));
877
+ console.log(chalk3.red("\u2717"), "Claude Code not found.");
878
+ console.log(chalk3.dim(" Install it:"), chalk3.cyan("npm i -g @anthropic-ai/claude-code"));
879
+ console.log(chalk3.dim(" Then run:"), chalk3.cyan("claude login"));
593
880
  process.exit(1);
594
881
  }
595
882
  const config = loadConfig();
596
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
597
- console.log(chalk2.dim(" Device:"), config.deviceName);
598
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
883
+ console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
884
+ console.log(chalk3.dim(" Device:"), config.deviceName);
885
+ console.log(chalk3.dim(" Projects:"), config.projectsDir);
599
886
  console.log();
600
887
  const supabase = getSupabase();
601
888
  try {
602
889
  await initSupabaseSession();
603
890
  } catch (err) {
604
- console.log(chalk2.red("\u2717"), `Session expired: ${err.message}`);
605
- console.log(chalk2.dim(" Run `tenux login` to re-authenticate."));
891
+ console.log(chalk3.red("\u2717"), `Session expired: ${err.message}`);
892
+ console.log(chalk3.dim(" Run `tenux login` to re-authenticate."));
606
893
  process.exit(1);
607
894
  }
608
895
  await supabase.from("devices").update({
@@ -610,56 +897,58 @@ program.command("start").description("Start the agent and listen for commands").
610
897
  is_online: true,
611
898
  last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
612
899
  }).eq("id", config.deviceId);
900
+ await cleanupStaleServers(supabase, config.deviceId);
613
901
  const heartbeat = setInterval(async () => {
614
902
  const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
615
903
  if (error) {
616
- console.log(chalk2.red("\u2717"), chalk2.dim(`Heartbeat failed: ${error.message}`));
904
+ console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
617
905
  }
618
906
  }, 3e4);
619
907
  const relay = new Relay(supabase);
620
908
  const shutdown = async () => {
621
- console.log(chalk2.dim("\n Shutting down..."));
909
+ console.log(chalk3.dim("\n Shutting down..."));
622
910
  clearInterval(heartbeat);
911
+ await cleanupServers(supabase, config.deviceId);
623
912
  await relay.stop();
624
913
  await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
625
914
  process.exit(0);
626
915
  };
627
- relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("project.delete", handleProjectDelete).on("agent.shutdown", async () => {
628
- console.log(chalk2.yellow("\n \u26A1 Remote shutdown requested"));
916
+ relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("project.delete", handleProjectDelete).on("server.start", handleServerStart).on("server.stop", handleServerStop).on("server.status", handleServerStatus).on("agent.shutdown", async () => {
917
+ console.log(chalk3.yellow("\n \u26A1 Remote shutdown requested"));
629
918
  await shutdown();
630
919
  });
631
920
  await relay.start();
632
- console.log(chalk2.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
921
+ console.log(chalk3.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
633
922
  process.on("SIGINT", shutdown);
634
923
  process.on("SIGTERM", shutdown);
635
924
  });
636
925
  program.command("status").description("Show agent configuration and status").action(() => {
637
926
  if (!configExists()) {
638
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
927
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
639
928
  process.exit(1);
640
929
  }
641
930
  const config = loadConfig();
642
931
  const claude = detectClaudeCode();
643
- console.log(chalk2.bold("\n tenux"), chalk2.dim("agent status\n"));
644
- console.log(chalk2.dim(" Config:"), getConfigPath());
645
- console.log(chalk2.dim(" App URL:"), config.appUrl);
646
- console.log(chalk2.dim(" Device:"), config.deviceName);
647
- console.log(chalk2.dim(" Device ID:"), config.deviceId);
648
- console.log(chalk2.dim(" User ID:"), config.userId);
649
- console.log(chalk2.dim(" Supabase:"), config.supabaseUrl);
650
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
932
+ console.log(chalk3.bold("\n tenux"), chalk3.dim("agent status\n"));
933
+ console.log(chalk3.dim(" Config:"), getConfigPath());
934
+ console.log(chalk3.dim(" App URL:"), config.appUrl);
935
+ console.log(chalk3.dim(" Device:"), config.deviceName);
936
+ console.log(chalk3.dim(" Device ID:"), config.deviceId);
937
+ console.log(chalk3.dim(" User ID:"), config.userId);
938
+ console.log(chalk3.dim(" Supabase:"), config.supabaseUrl);
939
+ console.log(chalk3.dim(" Projects:"), config.projectsDir);
651
940
  console.log(
652
- chalk2.dim(" Auth:"),
653
- config.accessToken ? chalk2.green("authenticated") : chalk2.yellow("not authenticated")
941
+ chalk3.dim(" Auth:"),
942
+ config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
654
943
  );
655
944
  console.log(
656
- chalk2.dim(" Claude Code:"),
657
- claude.installed ? chalk2.green("installed") : chalk2.yellow("not installed")
945
+ chalk3.dim(" Claude Code:"),
946
+ claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
658
947
  );
659
948
  if (claude.installed) {
660
949
  console.log(
661
- chalk2.dim(" Claude Auth:"),
662
- claude.authenticated ? chalk2.green("yes (~/.claude exists)") : chalk2.yellow("not authenticated")
950
+ chalk3.dim(" Claude Auth:"),
951
+ claude.authenticated ? chalk3.green("yes (~/.claude exists)") : chalk3.yellow("not authenticated")
663
952
  );
664
953
  }
665
954
  console.log();
@@ -667,7 +956,7 @@ program.command("status").description("Show agent configuration and status").act
667
956
  var configCmd = program.command("config").description("Manage agent configuration");
668
957
  configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
669
958
  if (!configExists()) {
670
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
959
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
671
960
  process.exit(1);
672
961
  }
673
962
  const keyMap = {
@@ -678,17 +967,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
678
967
  const configKey = keyMap[key];
679
968
  if (!configKey) {
680
969
  console.log(
681
- chalk2.red("\u2717"),
970
+ chalk3.red("\u2717"),
682
971
  `Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
683
972
  );
684
973
  process.exit(1);
685
974
  }
686
975
  updateConfig({ [configKey]: value });
687
- console.log(chalk2.green("\u2713"), `Set ${key}`);
976
+ console.log(chalk3.green("\u2713"), `Set ${key}`);
688
977
  });
689
978
  configCmd.command("get <key>").description("Get a config value").action((key) => {
690
979
  if (!configExists()) {
691
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
980
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
692
981
  process.exit(1);
693
982
  }
694
983
  const config = loadConfig();
@@ -703,12 +992,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
703
992
  const configKey = keyMap[key];
704
993
  if (!configKey) {
705
994
  console.log(
706
- chalk2.red("\u2717"),
995
+ chalk3.red("\u2717"),
707
996
  `Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
708
997
  );
709
998
  process.exit(1);
710
999
  }
711
1000
  const val = config[configKey];
712
- console.log(val ?? chalk2.dim("(not set)"));
1001
+ console.log(val ?? chalk3.dim("(not set)"));
713
1002
  });
714
1003
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenux/cli",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Tenux — mobile-first IDE for 10x engineering",
5
5
  "author": "Antelogic LLC",
6
6
  "license": "MIT",