@tenux/cli 0.0.20 → 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 +419 -85
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,19 +13,19 @@ 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
 
25
25
  // src/handlers/project.ts
26
26
  import { spawn, execSync } from "child_process";
27
27
  import { resolve, join } from "path";
28
- import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
28
+ import { existsSync, mkdirSync, readdirSync, statSync, rmSync } from "fs";
29
29
  import chalk from "chalk";
30
30
  var LOCKFILE_PM = {
31
31
  "bun.lockb": "bun",
@@ -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);
@@ -334,6 +334,338 @@ async function handleProjectGitStatus(command, supabase) {
334
334
  }).eq("id", command.id);
335
335
  }
336
336
  }
337
+ async function handleProjectDelete(command, supabase) {
338
+ const { name, path: projectPath, project_id } = command.payload;
339
+ const config = loadConfig();
340
+ const targetDir = resolve(config.projectsDir, projectPath);
341
+ const normalizedTarget = resolve(targetDir);
342
+ const normalizedProjects = resolve(config.projectsDir);
343
+ const sep = process.platform === "win32" ? "\\" : "/";
344
+ if (!normalizedTarget.startsWith(normalizedProjects + sep) && normalizedTarget !== normalizedProjects) {
345
+ const msg = `Refusing to delete \u2014 path escapes projects dir: ${projectPath}`;
346
+ console.log(chalk.red("\u2717"), msg);
347
+ await supabase.from("commands").update({
348
+ status: "error",
349
+ result: { error: msg },
350
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
351
+ }).eq("id", command.id);
352
+ return;
353
+ }
354
+ if (!existsSync(targetDir)) {
355
+ console.log(chalk.yellow("!"), `Project directory not found: ${projectPath} (already deleted?)`);
356
+ await supabase.from("commands").update({
357
+ status: "done",
358
+ result: { deleted: false, reason: "directory_not_found" },
359
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
360
+ }).eq("id", command.id);
361
+ return;
362
+ }
363
+ console.log(chalk.blue("\u2192"), `Deleting project directory: ${name} (${targetDir})`);
364
+ try {
365
+ rmSync(targetDir, { recursive: true, force: true });
366
+ console.log(chalk.green("\u2713"), `Deleted ${name}`);
367
+ await supabase.from("commands").update({
368
+ status: "done",
369
+ result: { deleted: true, path: projectPath },
370
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
371
+ }).eq("id", command.id);
372
+ } catch (err) {
373
+ const msg = err instanceof Error ? err.message : String(err);
374
+ console.log(chalk.red("\u2717"), `Delete failed: ${msg}`);
375
+ await supabase.from("commands").update({
376
+ status: "error",
377
+ result: { error: msg },
378
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
379
+ }).eq("id", command.id);
380
+ }
381
+ }
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
+ }
337
669
 
338
670
  // src/cli.ts
339
671
  var __dirname = dirname(fileURLToPath(import.meta.url));
@@ -344,30 +676,30 @@ function detectClaudeCode() {
344
676
  let installed = false;
345
677
  try {
346
678
  if (process.platform === "win32") {
347
- execSync2("where claude", { stdio: "ignore" });
679
+ execSync3("where claude", { stdio: "ignore" });
348
680
  } else {
349
- execSync2("which claude", { stdio: "ignore" });
681
+ execSync3("which claude", { stdio: "ignore" });
350
682
  }
351
683
  installed = true;
352
684
  } catch {
353
685
  installed = false;
354
686
  }
355
- const authenticated = existsSync2(join2(homedir(), ".claude"));
687
+ const authenticated = existsSync3(join2(homedir(), ".claude"));
356
688
  return { installed, authenticated };
357
689
  }
358
690
  async function sleep(ms) {
359
- return new Promise((resolve2) => setTimeout(resolve2, ms));
691
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
360
692
  }
361
693
  program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
362
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
694
+ console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
363
695
  const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
364
- console.log(chalk2.dim(" App:"), appUrl);
696
+ console.log(chalk3.dim(" App:"), appUrl);
365
697
  const deviceName = hostname();
366
698
  const platform = process.platform;
367
- console.log(chalk2.dim(" Device:"), deviceName);
368
- console.log(chalk2.dim(" Platform:"), platform);
699
+ console.log(chalk3.dim(" Device:"), deviceName);
700
+ console.log(chalk3.dim(" Platform:"), platform);
369
701
  console.log();
370
- console.log(chalk2.dim(" Requesting device code..."));
702
+ console.log(chalk3.dim(" Requesting device code..."));
371
703
  let code;
372
704
  let expiresAt;
373
705
  let supabaseUrl;
@@ -384,7 +716,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
384
716
  });
385
717
  if (!res.ok) {
386
718
  const text = await res.text();
387
- 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}`);
388
720
  process.exit(1);
389
721
  }
390
722
  const data = await res.json();
@@ -394,26 +726,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
394
726
  supabaseAnonKey = data.supabase_anon_key;
395
727
  } catch (err) {
396
728
  console.log(
397
- chalk2.red(" \u2717"),
729
+ chalk3.red(" \u2717"),
398
730
  `Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
399
731
  );
400
732
  process.exit(1);
401
733
  }
402
734
  console.log();
403
- 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`));
404
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
405
- console.log(chalk2.bold.cyan(` \u2502 Code: ${chalk2.white.bold(code)} \u2502`));
406
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
407
- 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`));
408
740
  console.log();
409
741
  const linkUrl = `${appUrl}/link?code=${code}`;
410
- console.log(chalk2.dim(" Opening browser to:"), chalk2.underline(linkUrl));
742
+ console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
411
743
  try {
412
744
  const open = (await import("open")).default;
413
745
  await open(linkUrl);
414
746
  } catch {
415
- console.log(chalk2.yellow(" !"), "Could not open browser automatically.");
416
- 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));
417
749
  }
418
750
  console.log();
419
751
  const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -425,11 +757,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
425
757
  const expiresTime = new Date(expiresAt).getTime();
426
758
  while (!approved) {
427
759
  if (Date.now() > expiresTime) {
428
- 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.");
429
761
  process.exit(1);
430
762
  }
431
763
  process.stdout.write(
432
- `\r ${chalk2.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
764
+ `\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
433
765
  );
434
766
  dotIndex++;
435
767
  await sleep(2e3);
@@ -447,16 +779,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
447
779
  userId = data.user_id || "";
448
780
  approved = true;
449
781
  } else if (data.status === "expired") {
450
- 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.");
451
783
  process.exit(1);
452
784
  }
453
785
  } catch {
454
786
  }
455
787
  }
456
788
  process.stdout.write("\r" + " ".repeat(60) + "\r");
457
- console.log(chalk2.green(" \u2713"), "Device approved!");
789
+ console.log(chalk3.green(" \u2713"), "Device approved!");
458
790
  console.log();
459
- console.log(chalk2.dim(" Exchanging token for session..."));
791
+ console.log(chalk3.dim(" Exchanging token for session..."));
460
792
  let accessToken = "";
461
793
  let refreshToken = "";
462
794
  try {
@@ -468,11 +800,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
468
800
  type: "magiclink"
469
801
  });
470
802
  if (error) {
471
- console.log(chalk2.red(" \u2717"), `Auth failed: ${error.message}`);
803
+ console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
472
804
  process.exit(1);
473
805
  }
474
806
  if (!session.session) {
475
- console.log(chalk2.red(" \u2717"), "No session returned from auth exchange.");
807
+ console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
476
808
  process.exit(1);
477
809
  }
478
810
  accessToken = session.session.access_token;
@@ -480,12 +812,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
480
812
  userId = session.session.user?.id || userId;
481
813
  } catch (err) {
482
814
  console.log(
483
- chalk2.red(" \u2717"),
815
+ chalk3.red(" \u2717"),
484
816
  `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
485
817
  );
486
818
  process.exit(1);
487
819
  }
488
- console.log(chalk2.green(" \u2713"), "Session established.");
820
+ console.log(chalk3.green(" \u2713"), "Session established.");
489
821
  const config = {
490
822
  appUrl,
491
823
  supabaseUrl,
@@ -498,66 +830,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
498
830
  projectsDir: join2(homedir(), ".tenux", "projects")
499
831
  };
500
832
  saveConfig(config);
501
- console.log(chalk2.green(" \u2713"), "Config saved to", chalk2.dim(getConfigPath()));
833
+ console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
502
834
  console.log();
503
835
  const claude = detectClaudeCode();
504
836
  if (claude.installed) {
505
- console.log(chalk2.green(" \u2713"), "Claude Code detected");
837
+ console.log(chalk3.green(" \u2713"), "Claude Code detected");
506
838
  if (claude.authenticated) {
507
- console.log(chalk2.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
839
+ console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
508
840
  } else {
509
841
  console.log(
510
- chalk2.yellow(" !"),
842
+ chalk3.yellow(" !"),
511
843
  "Claude Code found but ~/.claude not detected.",
512
- chalk2.dim("Run `claude login` to authenticate.")
844
+ chalk3.dim("Run `claude login` to authenticate.")
513
845
  );
514
846
  }
515
847
  } else {
516
- console.log(chalk2.yellow(" !"), "Claude Code not found.");
848
+ console.log(chalk3.yellow(" !"), "Claude Code not found.");
517
849
  console.log(
518
- chalk2.dim(" Install it:"),
519
- 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")
520
852
  );
521
853
  console.log(
522
- chalk2.dim(" Then run:"),
523
- chalk2.cyan("claude login")
854
+ chalk3.dim(" Then run:"),
855
+ chalk3.cyan("claude login")
524
856
  );
525
857
  }
526
858
  console.log();
527
- console.log(chalk2.dim(" Device:"), deviceName);
528
- console.log(chalk2.dim(" Device ID:"), deviceId);
529
- console.log(chalk2.dim(" User ID:"), userId);
530
- 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);
531
863
  console.log();
532
864
  console.log(
533
- chalk2.dim(" Next: start the agent:"),
534
- chalk2.cyan("tenux start")
865
+ chalk3.dim(" Next: start the agent:"),
866
+ chalk3.cyan("tenux start")
535
867
  );
536
868
  console.log();
537
869
  });
538
870
  program.command("start").description("Start the agent and listen for commands").action(async () => {
539
871
  if (!configExists()) {
540
- 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.");
541
873
  process.exit(1);
542
874
  }
543
875
  const claude = detectClaudeCode();
544
876
  if (!claude.installed) {
545
- console.log(chalk2.red("\u2717"), "Claude Code not found.");
546
- console.log(chalk2.dim(" Install it:"), chalk2.cyan("npm i -g @anthropic-ai/claude-code"));
547
- 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"));
548
880
  process.exit(1);
549
881
  }
550
882
  const config = loadConfig();
551
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
552
- console.log(chalk2.dim(" Device:"), config.deviceName);
553
- 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);
554
886
  console.log();
555
887
  const supabase = getSupabase();
556
888
  try {
557
889
  await initSupabaseSession();
558
890
  } catch (err) {
559
- console.log(chalk2.red("\u2717"), `Session expired: ${err.message}`);
560
- 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."));
561
893
  process.exit(1);
562
894
  }
563
895
  await supabase.from("devices").update({
@@ -565,56 +897,58 @@ program.command("start").description("Start the agent and listen for commands").
565
897
  is_online: true,
566
898
  last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
567
899
  }).eq("id", config.deviceId);
900
+ await cleanupStaleServers(supabase, config.deviceId);
568
901
  const heartbeat = setInterval(async () => {
569
902
  const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
570
903
  if (error) {
571
- console.log(chalk2.red("\u2717"), chalk2.dim(`Heartbeat failed: ${error.message}`));
904
+ console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
572
905
  }
573
906
  }, 3e4);
574
907
  const relay = new Relay(supabase);
575
908
  const shutdown = async () => {
576
- console.log(chalk2.dim("\n Shutting down..."));
909
+ console.log(chalk3.dim("\n Shutting down..."));
577
910
  clearInterval(heartbeat);
911
+ await cleanupServers(supabase, config.deviceId);
578
912
  await relay.stop();
579
913
  await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
580
914
  process.exit(0);
581
915
  };
582
- relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("agent.shutdown", async () => {
583
- 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"));
584
918
  await shutdown();
585
919
  });
586
920
  await relay.start();
587
- 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");
588
922
  process.on("SIGINT", shutdown);
589
923
  process.on("SIGTERM", shutdown);
590
924
  });
591
925
  program.command("status").description("Show agent configuration and status").action(() => {
592
926
  if (!configExists()) {
593
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
927
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
594
928
  process.exit(1);
595
929
  }
596
930
  const config = loadConfig();
597
931
  const claude = detectClaudeCode();
598
- console.log(chalk2.bold("\n tenux"), chalk2.dim("agent status\n"));
599
- console.log(chalk2.dim(" Config:"), getConfigPath());
600
- console.log(chalk2.dim(" App URL:"), config.appUrl);
601
- console.log(chalk2.dim(" Device:"), config.deviceName);
602
- console.log(chalk2.dim(" Device ID:"), config.deviceId);
603
- console.log(chalk2.dim(" User ID:"), config.userId);
604
- console.log(chalk2.dim(" Supabase:"), config.supabaseUrl);
605
- 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);
606
940
  console.log(
607
- chalk2.dim(" Auth:"),
608
- config.accessToken ? chalk2.green("authenticated") : chalk2.yellow("not authenticated")
941
+ chalk3.dim(" Auth:"),
942
+ config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
609
943
  );
610
944
  console.log(
611
- chalk2.dim(" Claude Code:"),
612
- claude.installed ? chalk2.green("installed") : chalk2.yellow("not installed")
945
+ chalk3.dim(" Claude Code:"),
946
+ claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
613
947
  );
614
948
  if (claude.installed) {
615
949
  console.log(
616
- chalk2.dim(" Claude Auth:"),
617
- 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")
618
952
  );
619
953
  }
620
954
  console.log();
@@ -622,7 +956,7 @@ program.command("status").description("Show agent configuration and status").act
622
956
  var configCmd = program.command("config").description("Manage agent configuration");
623
957
  configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
624
958
  if (!configExists()) {
625
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
959
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
626
960
  process.exit(1);
627
961
  }
628
962
  const keyMap = {
@@ -633,17 +967,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
633
967
  const configKey = keyMap[key];
634
968
  if (!configKey) {
635
969
  console.log(
636
- chalk2.red("\u2717"),
970
+ chalk3.red("\u2717"),
637
971
  `Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
638
972
  );
639
973
  process.exit(1);
640
974
  }
641
975
  updateConfig({ [configKey]: value });
642
- console.log(chalk2.green("\u2713"), `Set ${key}`);
976
+ console.log(chalk3.green("\u2713"), `Set ${key}`);
643
977
  });
644
978
  configCmd.command("get <key>").description("Get a config value").action((key) => {
645
979
  if (!configExists()) {
646
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
980
+ console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
647
981
  process.exit(1);
648
982
  }
649
983
  const config = loadConfig();
@@ -658,12 +992,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
658
992
  const configKey = keyMap[key];
659
993
  if (!configKey) {
660
994
  console.log(
661
- chalk2.red("\u2717"),
995
+ chalk3.red("\u2717"),
662
996
  `Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
663
997
  );
664
998
  process.exit(1);
665
999
  }
666
1000
  const val = config[configKey];
667
- console.log(val ?? chalk2.dim("(not set)"));
1001
+ console.log(val ?? chalk3.dim("(not set)"));
668
1002
  });
669
1003
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenux/cli",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Tenux — mobile-first IDE for 10x engineering",
5
5
  "author": "Antelogic LLC",
6
6
  "license": "MIT",