@tenux/cli 0.0.22 → 0.0.23

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 +407 -132
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  // src/cli.ts
15
15
  import { Command } from "commander";
16
- import chalk3 from "chalk";
16
+ import chalk4 from "chalk";
17
17
  import { hostname } from "os";
18
18
  import { homedir } from "os";
19
19
  import { join as join2, dirname } from "path";
@@ -381,10 +381,305 @@ async function handleProjectDelete(command, supabase) {
381
381
  }
382
382
 
383
383
  // src/handlers/server.ts
384
- import { spawn as spawn2, exec, execSync as execSync2 } from "child_process";
384
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
385
385
  import { resolve as resolve2 } from "path";
386
386
  import { existsSync as existsSync2 } from "fs";
387
+
388
+ // src/lib/tunnel.ts
389
+ import http from "http";
390
+ import WebSocket from "ws";
387
391
  import chalk2 from "chalk";
392
+ var MSG_HTTP_REQUEST = 1;
393
+ var MSG_HTTP_RESPONSE = 2;
394
+ var MSG_WS_OPEN = 3;
395
+ var MSG_WS_FRAME = 4;
396
+ var MSG_WS_CLOSE = 5;
397
+ function getMessageType(data) {
398
+ return new DataView(data).getUint8(0);
399
+ }
400
+ function decodeHttpRequest(data) {
401
+ const view = new DataView(data);
402
+ const bytes = new Uint8Array(data);
403
+ let offset = 1;
404
+ const requestId = view.getUint32(offset);
405
+ offset += 4;
406
+ const methodLen = view.getUint16(offset);
407
+ offset += 2;
408
+ const method = Buffer.from(bytes.slice(offset, offset + methodLen)).toString();
409
+ offset += methodLen;
410
+ const pathLen = view.getUint16(offset);
411
+ offset += 2;
412
+ const path = Buffer.from(bytes.slice(offset, offset + pathLen)).toString();
413
+ offset += pathLen;
414
+ const headersLen = view.getUint32(offset);
415
+ offset += 4;
416
+ const headersJson = Buffer.from(bytes.slice(offset, offset + headersLen)).toString();
417
+ offset += headersLen;
418
+ const headers = JSON.parse(headersJson);
419
+ const body = Buffer.from(bytes.slice(offset));
420
+ return { requestId, method, path, headers, body };
421
+ }
422
+ function encodeHttpResponse(requestId, status, headers, body) {
423
+ const headersJson = Buffer.from(JSON.stringify(headers));
424
+ const totalLen = 1 + 4 + 2 + 4 + headersJson.length + body.length;
425
+ const buf = Buffer.alloc(totalLen);
426
+ let offset = 0;
427
+ buf.writeUInt8(MSG_HTTP_RESPONSE, offset);
428
+ offset += 1;
429
+ buf.writeUInt32BE(requestId, offset);
430
+ offset += 4;
431
+ buf.writeUInt16BE(status, offset);
432
+ offset += 2;
433
+ buf.writeUInt32BE(headersJson.length, offset);
434
+ offset += 4;
435
+ headersJson.copy(buf, offset);
436
+ offset += headersJson.length;
437
+ body.copy(buf, offset);
438
+ return buf;
439
+ }
440
+ function decodeWsOpen(data) {
441
+ const view = new DataView(data);
442
+ const bytes = new Uint8Array(data);
443
+ let offset = 1;
444
+ const channelId = view.getUint32(offset);
445
+ offset += 4;
446
+ const pathLen = view.getUint16(offset);
447
+ offset += 2;
448
+ const path = Buffer.from(bytes.slice(offset, offset + pathLen)).toString();
449
+ offset += pathLen;
450
+ const headersLen = view.getUint32(offset);
451
+ offset += 4;
452
+ const headersJson = Buffer.from(bytes.slice(offset, offset + headersLen)).toString();
453
+ const headers = JSON.parse(headersJson);
454
+ return { channelId, path, headers };
455
+ }
456
+ function getChannelId(data) {
457
+ return new DataView(data).getUint32(1);
458
+ }
459
+ function getWsFramePayload(data) {
460
+ return Buffer.from(new Uint8Array(data, 5));
461
+ }
462
+ function getWsCloseCode(data) {
463
+ return new DataView(data).getUint16(5);
464
+ }
465
+ function encodeWsFrame(channelId, payload) {
466
+ const buf = Buffer.alloc(5 + payload.length);
467
+ buf.writeUInt8(MSG_WS_FRAME, 0);
468
+ buf.writeUInt32BE(channelId, 1);
469
+ payload.copy(buf, 5);
470
+ return buf;
471
+ }
472
+ function encodeWsClose(channelId, closeCode) {
473
+ const buf = Buffer.alloc(7);
474
+ buf.writeUInt8(MSG_WS_CLOSE, 0);
475
+ buf.writeUInt32BE(channelId, 1);
476
+ buf.writeUInt16BE(closeCode, 5);
477
+ return buf;
478
+ }
479
+ var TUNNEL_HOST = "tenux.dev";
480
+ var activeTunnels = /* @__PURE__ */ new Map();
481
+ function connectTunnel(instanceId, localPort, accessToken, userId) {
482
+ disconnectTunnel(instanceId);
483
+ const tunnelUrl = `https://r-${instanceId}.${TUNNEL_HOST}`;
484
+ const wsUrl = `wss://r-${instanceId}.${TUNNEL_HOST}/_tunnel/connect`;
485
+ const conn = {
486
+ ws: null,
487
+ localPort,
488
+ instanceId,
489
+ localWsSockets: /* @__PURE__ */ new Map(),
490
+ closed: false
491
+ };
492
+ function connect() {
493
+ if (conn.closed) return;
494
+ const ws = new WebSocket(wsUrl, {
495
+ headers: {
496
+ "Authorization": `Bearer ${accessToken}`,
497
+ "X-User-Id": userId
498
+ }
499
+ });
500
+ ws.binaryType = "arraybuffer";
501
+ conn.ws = ws;
502
+ ws.on("open", () => {
503
+ console.log(chalk2.green("\u2713"), `[tunnel] Connected: ${tunnelUrl}`);
504
+ });
505
+ ws.on("message", (data) => {
506
+ const ab = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
507
+ handleRelayMessage(conn, ab);
508
+ });
509
+ ws.on("close", () => {
510
+ if (conn.closed) return;
511
+ console.log(chalk2.yellow("!"), `[tunnel] Disconnected, reconnecting in 3s...`);
512
+ conn.reconnectTimer = setTimeout(connect, 3e3);
513
+ });
514
+ ws.on("error", (err) => {
515
+ console.log(chalk2.yellow("!"), `[tunnel] Error: ${err.message}`);
516
+ });
517
+ }
518
+ connect();
519
+ activeTunnels.set(instanceId, conn);
520
+ return tunnelUrl;
521
+ }
522
+ function disconnectTunnel(instanceId) {
523
+ const conn = activeTunnels.get(instanceId);
524
+ if (!conn) return;
525
+ conn.closed = true;
526
+ if (conn.reconnectTimer) clearTimeout(conn.reconnectTimer);
527
+ for (const [, localWs] of conn.localWsSockets) {
528
+ try {
529
+ localWs.close();
530
+ } catch {
531
+ }
532
+ }
533
+ conn.localWsSockets.clear();
534
+ try {
535
+ conn.ws.close(1e3, "tunnel disconnect");
536
+ } catch {
537
+ }
538
+ activeTunnels.delete(instanceId);
539
+ console.log(chalk2.dim(" [tunnel] Disconnected:", instanceId));
540
+ }
541
+ function disconnectAllTunnels() {
542
+ for (const instanceId of activeTunnels.keys()) {
543
+ disconnectTunnel(instanceId);
544
+ }
545
+ }
546
+ function handleRelayMessage(conn, data) {
547
+ const type = getMessageType(data);
548
+ switch (type) {
549
+ case MSG_HTTP_REQUEST:
550
+ handleHttpRequest(conn, data);
551
+ break;
552
+ case MSG_WS_OPEN:
553
+ handleWsOpen(conn, data);
554
+ break;
555
+ case MSG_WS_FRAME:
556
+ handleWsFrame(conn, data);
557
+ break;
558
+ case MSG_WS_CLOSE:
559
+ handleWsClose(conn, data);
560
+ break;
561
+ }
562
+ }
563
+ function handleHttpRequest(conn, data) {
564
+ const req = decodeHttpRequest(data);
565
+ const options = {
566
+ hostname: "127.0.0.1",
567
+ port: conn.localPort,
568
+ path: req.path,
569
+ method: req.method,
570
+ headers: {
571
+ ...req.headers,
572
+ host: `127.0.0.1:${conn.localPort}`
573
+ }
574
+ };
575
+ const localReq = http.request(options, (localRes) => {
576
+ const chunks = [];
577
+ localRes.on("data", (chunk) => {
578
+ chunks.push(chunk);
579
+ });
580
+ localRes.on("end", () => {
581
+ const body = Buffer.concat(chunks);
582
+ const headers = {};
583
+ for (const [key, value] of Object.entries(localRes.headers)) {
584
+ if (value) {
585
+ headers[key] = Array.isArray(value) ? value.join(", ") : value;
586
+ }
587
+ }
588
+ const responseMsg = encodeHttpResponse(req.requestId, localRes.statusCode ?? 500, headers, body);
589
+ try {
590
+ if (conn.ws.readyState === WebSocket.OPEN) {
591
+ conn.ws.send(responseMsg);
592
+ }
593
+ } catch (err) {
594
+ console.log(chalk2.yellow("!"), `[tunnel] Failed to send response: ${err.message}`);
595
+ }
596
+ });
597
+ });
598
+ localReq.on("error", (err) => {
599
+ const body = Buffer.from(`Local server error: ${err.message}`);
600
+ const headers = { "content-type": "text/plain" };
601
+ const responseMsg = encodeHttpResponse(req.requestId, 502, headers, body);
602
+ try {
603
+ if (conn.ws.readyState === WebSocket.OPEN) {
604
+ conn.ws.send(responseMsg);
605
+ }
606
+ } catch {
607
+ }
608
+ });
609
+ if (req.body.length > 0) {
610
+ localReq.write(req.body);
611
+ }
612
+ localReq.end();
613
+ }
614
+ function handleWsOpen(conn, data) {
615
+ const { channelId, path, headers } = decodeWsOpen(data);
616
+ const wsUrl = `ws://127.0.0.1:${conn.localPort}${path}`;
617
+ const localWs = new WebSocket(wsUrl, {
618
+ headers: {
619
+ ...headers,
620
+ host: `127.0.0.1:${conn.localPort}`
621
+ }
622
+ });
623
+ localWs.binaryType = "arraybuffer";
624
+ conn.localWsSockets.set(channelId, localWs);
625
+ localWs.on("message", (data2) => {
626
+ const payload = Buffer.isBuffer(data2) ? data2 : Buffer.from(data2);
627
+ const frame = encodeWsFrame(channelId, payload);
628
+ try {
629
+ if (conn.ws.readyState === WebSocket.OPEN) {
630
+ conn.ws.send(frame);
631
+ }
632
+ } catch {
633
+ }
634
+ });
635
+ localWs.on("close", (code) => {
636
+ conn.localWsSockets.delete(channelId);
637
+ const closeMsg = encodeWsClose(channelId, code || 1e3);
638
+ try {
639
+ if (conn.ws.readyState === WebSocket.OPEN) {
640
+ conn.ws.send(closeMsg);
641
+ }
642
+ } catch {
643
+ }
644
+ });
645
+ localWs.on("error", (err) => {
646
+ console.log(chalk2.dim(` [tunnel] Local WS error (channel ${channelId}): ${err.message}`));
647
+ conn.localWsSockets.delete(channelId);
648
+ const closeMsg = encodeWsClose(channelId, 1011);
649
+ try {
650
+ if (conn.ws.readyState === WebSocket.OPEN) {
651
+ conn.ws.send(closeMsg);
652
+ }
653
+ } catch {
654
+ }
655
+ });
656
+ }
657
+ function handleWsFrame(conn, data) {
658
+ const channelId = getChannelId(data);
659
+ const payload = getWsFramePayload(data);
660
+ const localWs = conn.localWsSockets.get(channelId);
661
+ if (localWs && localWs.readyState === WebSocket.OPEN) {
662
+ try {
663
+ localWs.send(payload);
664
+ } catch {
665
+ }
666
+ }
667
+ }
668
+ function handleWsClose(conn, data) {
669
+ const channelId = getChannelId(data);
670
+ const closeCode = getWsCloseCode(data);
671
+ const localWs = conn.localWsSockets.get(channelId);
672
+ if (localWs) {
673
+ conn.localWsSockets.delete(channelId);
674
+ try {
675
+ localWs.close(closeCode);
676
+ } catch {
677
+ }
678
+ }
679
+ }
680
+
681
+ // src/handlers/server.ts
682
+ import chalk3 from "chalk";
388
683
  var MAX_LOG_LINES = 500;
389
684
  var LOG_PUSH_INTERVAL = 2e3;
390
685
  var runningServers = /* @__PURE__ */ new Map();
@@ -408,39 +703,6 @@ function killTree(child) {
408
703
  child.kill("SIGTERM");
409
704
  }
410
705
  }
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
706
  async function handleServerStart(command, supabase) {
445
707
  const payload = command.payload;
446
708
  const config = loadConfig();
@@ -453,6 +715,7 @@ async function handleServerStart(command, supabase) {
453
715
  if (existing) {
454
716
  clearInterval(existing.logInterval);
455
717
  killTree(existing.process);
718
+ if (existing.tunnelUrl) disconnectTunnel(existing.instanceId);
456
719
  runningServers.delete(key);
457
720
  }
458
721
  const { data: instance, error: upsertErr } = await supabase.from("server_instances").upsert(
@@ -466,7 +729,7 @@ async function handleServerStart(command, supabase) {
466
729
  port: payload.port ?? null,
467
730
  pid: null,
468
731
  detected_port: null,
469
- tailscale_url: null,
732
+ tunnel_url: null,
470
733
  logs: [],
471
734
  error_message: null,
472
735
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -478,9 +741,11 @@ async function handleServerStart(command, supabase) {
478
741
  if (upsertErr || !instance) {
479
742
  throw new Error(`Failed to create server instance: ${upsertErr?.message}`);
480
743
  }
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}`));
744
+ console.log(chalk3.blue("\u2192"), `Starting server: ${payload.project_name}::${payload.command_name}`);
745
+ console.log(chalk3.dim(` cmd: ${payload.command}`));
746
+ console.log(chalk3.dim(` cwd: ${fullPath}`));
747
+ const { data: sessionData } = await supabase.auth.getSession();
748
+ const accessToken = sessionData?.session?.access_token ?? "";
484
749
  const env = { ...process.env, FORCE_COLOR: "0" };
485
750
  delete env.PORT;
486
751
  delete env.NODE_OPTIONS;
@@ -510,17 +775,22 @@ async function handleServerStart(command, supabase) {
510
775
  const actualPort = parseInt(portMatch[1], 10);
511
776
  detectedPort = true;
512
777
  managed.detectedPort = actualPort;
513
- setupTailscaleServe(actualPort);
514
- const hostname2 = getTailscaleHostname();
515
- const tsUrl = hostname2 ? `https://${hostname2}:${actualPort}` : null;
778
+ const tunnelUrl = connectTunnel(
779
+ instance.id,
780
+ actualPort,
781
+ accessToken,
782
+ command.user_id
783
+ );
784
+ managed.tunnelUrl = tunnelUrl;
516
785
  supabase.from("server_instances").update({
517
786
  status: "running",
518
787
  detected_port: actualPort,
519
- tailscale_url: tsUrl,
788
+ tunnel_url: tunnelUrl,
520
789
  pid: child.pid ?? null,
521
790
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
522
791
  }).eq("id", instance.id).then(() => {
523
- console.log(chalk2.green("\u2713"), `Server running on :${actualPort}`);
792
+ console.log(chalk3.green("\u2713"), `Server running on :${actualPort}`);
793
+ console.log(chalk3.green("\u2713"), `Tunnel: ${tunnelUrl}`);
524
794
  });
525
795
  }
526
796
  }
@@ -549,19 +819,18 @@ async function handleServerStart(command, supabase) {
549
819
  child.on("close", async (code) => {
550
820
  clearInterval(logInterval);
551
821
  logs.push(`[Process exited with code ${code}]`);
552
- if (managed.detectedPort) {
553
- teardownTailscaleServe(managed.detectedPort);
554
- }
822
+ disconnectTunnel(managed.instanceId);
555
823
  runningServers.delete(key);
556
824
  await supabase.from("server_instances").update({
557
825
  status: code === 0 ? "stopped" : "error",
558
826
  error_message: code !== 0 ? `Exited with code ${code}` : null,
559
827
  logs: logs.slice(-100),
828
+ tunnel_url: null,
560
829
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
561
830
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
562
831
  }).eq("id", instance.id);
563
832
  console.log(
564
- code === 0 ? chalk2.dim(" Server stopped") : chalk2.yellow("!"),
833
+ code === 0 ? chalk3.dim(" Server stopped") : chalk3.yellow("!"),
565
834
  `${payload.project_name}::${payload.command_name} exited (code ${code})`
566
835
  );
567
836
  });
@@ -576,13 +845,17 @@ async function handleServerStart(command, supabase) {
576
845
  const configuredPort = payload.port;
577
846
  if (configuredPort) {
578
847
  managed.detectedPort = configuredPort;
579
- setupTailscaleServe(configuredPort);
580
- const hostname2 = getTailscaleHostname();
581
- const tsUrl = hostname2 ? `https://${hostname2}:${configuredPort}` : null;
848
+ const tunnelUrl = connectTunnel(
849
+ instance.id,
850
+ configuredPort,
851
+ accessToken,
852
+ command.user_id
853
+ );
854
+ managed.tunnelUrl = tunnelUrl;
582
855
  await supabase.from("server_instances").update({
583
856
  status: "running",
584
857
  detected_port: configuredPort,
585
- tailscale_url: tsUrl,
858
+ tunnel_url: tunnelUrl,
586
859
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
587
860
  }).eq("id", instance.id);
588
861
  } else {
@@ -601,13 +874,14 @@ async function handleServerStop(command, supabase) {
601
874
  const key = makeKey(payload.project_name, payload.command_name);
602
875
  const managed = runningServers.get(key);
603
876
  if (managed) {
604
- console.log(chalk2.blue("\u2192"), `Stopping server: ${key}`);
877
+ console.log(chalk3.blue("\u2192"), `Stopping server: ${key}`);
605
878
  clearInterval(managed.logInterval);
606
879
  killTree(managed.process);
607
880
  } else {
608
881
  if (payload.server_instance_id) {
609
882
  await supabase.from("server_instances").update({
610
883
  status: "stopped",
884
+ tunnel_url: null,
611
885
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
612
886
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
613
887
  }).eq("id", payload.server_instance_id);
@@ -628,6 +902,7 @@ async function handleServerStatus(command, supabase) {
628
902
  running: managed.process.exitCode === null,
629
903
  pid: managed.process.pid,
630
904
  detectedPort: managed.detectedPort,
905
+ tunnelUrl: managed.tunnelUrl,
631
906
  logCount: managed.logs.length
632
907
  });
633
908
  }
@@ -639,18 +914,17 @@ async function handleServerStatus(command, supabase) {
639
914
  }
640
915
  async function cleanupServers(supabase, deviceId) {
641
916
  for (const [key, managed] of runningServers) {
642
- console.log(chalk2.dim(` Stopping server: ${key}`));
917
+ console.log(chalk3.dim(` Stopping server: ${key}`));
643
918
  clearInterval(managed.logInterval);
644
919
  if (managed.process.exitCode === null) {
645
920
  killTree(managed.process);
646
921
  }
647
- if (managed.detectedPort) {
648
- teardownTailscaleServe(managed.detectedPort);
649
- }
650
922
  }
651
923
  runningServers.clear();
924
+ disconnectAllTunnels();
652
925
  await supabase.from("server_instances").update({
653
926
  status: "stopped",
927
+ tunnel_url: null,
654
928
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
655
929
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
656
930
  }).eq("device_id", deviceId).in("status", ["starting", "running"]);
@@ -658,12 +932,13 @@ async function cleanupServers(supabase, deviceId) {
658
932
  async function cleanupStaleServers(supabase, deviceId) {
659
933
  const { data, error } = await supabase.from("server_instances").update({
660
934
  status: "stopped",
935
+ tunnel_url: null,
661
936
  error_message: "Agent restarted",
662
937
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
663
938
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
664
939
  }).eq("device_id", deviceId).in("status", ["starting", "running"]).select("id");
665
940
  if (data && data.length > 0) {
666
- console.log(chalk2.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
941
+ console.log(chalk3.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
667
942
  }
668
943
  }
669
944
 
@@ -691,15 +966,15 @@ async function sleep(ms) {
691
966
  return new Promise((resolve3) => setTimeout(resolve3, ms));
692
967
  }
693
968
  program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
694
- console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
969
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("desktop agent\n"));
695
970
  const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
696
- console.log(chalk3.dim(" App:"), appUrl);
971
+ console.log(chalk4.dim(" App:"), appUrl);
697
972
  const deviceName = hostname();
698
973
  const platform = process.platform;
699
- console.log(chalk3.dim(" Device:"), deviceName);
700
- console.log(chalk3.dim(" Platform:"), platform);
974
+ console.log(chalk4.dim(" Device:"), deviceName);
975
+ console.log(chalk4.dim(" Platform:"), platform);
701
976
  console.log();
702
- console.log(chalk3.dim(" Requesting device code..."));
977
+ console.log(chalk4.dim(" Requesting device code..."));
703
978
  let code;
704
979
  let expiresAt;
705
980
  let supabaseUrl;
@@ -716,7 +991,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
716
991
  });
717
992
  if (!res.ok) {
718
993
  const text = await res.text();
719
- console.log(chalk3.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
994
+ console.log(chalk4.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
720
995
  process.exit(1);
721
996
  }
722
997
  const data = await res.json();
@@ -726,26 +1001,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
726
1001
  supabaseAnonKey = data.supabase_anon_key;
727
1002
  } catch (err) {
728
1003
  console.log(
729
- chalk3.red(" \u2717"),
1004
+ chalk4.red(" \u2717"),
730
1005
  `Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
731
1006
  );
732
1007
  process.exit(1);
733
1008
  }
734
1009
  console.log();
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`));
1010
+ console.log(chalk4.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`));
1011
+ console.log(chalk4.bold.cyan(` \u2502 \u2502`));
1012
+ console.log(chalk4.bold.cyan(` \u2502 Code: ${chalk4.white.bold(code)} \u2502`));
1013
+ console.log(chalk4.bold.cyan(` \u2502 \u2502`));
1014
+ console.log(chalk4.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`));
740
1015
  console.log();
741
1016
  const linkUrl = `${appUrl}/link?code=${code}`;
742
- console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
1017
+ console.log(chalk4.dim(" Opening browser to:"), chalk4.underline(linkUrl));
743
1018
  try {
744
1019
  const open = (await import("open")).default;
745
1020
  await open(linkUrl);
746
1021
  } catch {
747
- console.log(chalk3.yellow(" !"), "Could not open browser automatically.");
748
- console.log(chalk3.dim(" Open this URL manually:"), chalk3.underline(linkUrl));
1022
+ console.log(chalk4.yellow(" !"), "Could not open browser automatically.");
1023
+ console.log(chalk4.dim(" Open this URL manually:"), chalk4.underline(linkUrl));
749
1024
  }
750
1025
  console.log();
751
1026
  const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -757,11 +1032,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
757
1032
  const expiresTime = new Date(expiresAt).getTime();
758
1033
  while (!approved) {
759
1034
  if (Date.now() > expiresTime) {
760
- console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
1035
+ console.log(chalk4.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
761
1036
  process.exit(1);
762
1037
  }
763
1038
  process.stdout.write(
764
- `\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
1039
+ `\r ${chalk4.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
765
1040
  );
766
1041
  dotIndex++;
767
1042
  await sleep(2e3);
@@ -779,16 +1054,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
779
1054
  userId = data.user_id || "";
780
1055
  approved = true;
781
1056
  } else if (data.status === "expired") {
782
- console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
1057
+ console.log(chalk4.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
783
1058
  process.exit(1);
784
1059
  }
785
1060
  } catch {
786
1061
  }
787
1062
  }
788
1063
  process.stdout.write("\r" + " ".repeat(60) + "\r");
789
- console.log(chalk3.green(" \u2713"), "Device approved!");
1064
+ console.log(chalk4.green(" \u2713"), "Device approved!");
790
1065
  console.log();
791
- console.log(chalk3.dim(" Exchanging token for session..."));
1066
+ console.log(chalk4.dim(" Exchanging token for session..."));
792
1067
  let accessToken = "";
793
1068
  let refreshToken = "";
794
1069
  try {
@@ -800,11 +1075,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
800
1075
  type: "magiclink"
801
1076
  });
802
1077
  if (error) {
803
- console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
1078
+ console.log(chalk4.red(" \u2717"), `Auth failed: ${error.message}`);
804
1079
  process.exit(1);
805
1080
  }
806
1081
  if (!session.session) {
807
- console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
1082
+ console.log(chalk4.red(" \u2717"), "No session returned from auth exchange.");
808
1083
  process.exit(1);
809
1084
  }
810
1085
  accessToken = session.session.access_token;
@@ -812,12 +1087,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
812
1087
  userId = session.session.user?.id || userId;
813
1088
  } catch (err) {
814
1089
  console.log(
815
- chalk3.red(" \u2717"),
1090
+ chalk4.red(" \u2717"),
816
1091
  `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
817
1092
  );
818
1093
  process.exit(1);
819
1094
  }
820
- console.log(chalk3.green(" \u2713"), "Session established.");
1095
+ console.log(chalk4.green(" \u2713"), "Session established.");
821
1096
  const config = {
822
1097
  appUrl,
823
1098
  supabaseUrl,
@@ -830,66 +1105,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
830
1105
  projectsDir: join2(homedir(), ".tenux", "projects")
831
1106
  };
832
1107
  saveConfig(config);
833
- console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
1108
+ console.log(chalk4.green(" \u2713"), "Config saved to", chalk4.dim(getConfigPath()));
834
1109
  console.log();
835
1110
  const claude = detectClaudeCode();
836
1111
  if (claude.installed) {
837
- console.log(chalk3.green(" \u2713"), "Claude Code detected");
1112
+ console.log(chalk4.green(" \u2713"), "Claude Code detected");
838
1113
  if (claude.authenticated) {
839
- console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
1114
+ console.log(chalk4.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
840
1115
  } else {
841
1116
  console.log(
842
- chalk3.yellow(" !"),
1117
+ chalk4.yellow(" !"),
843
1118
  "Claude Code found but ~/.claude not detected.",
844
- chalk3.dim("Run `claude login` to authenticate.")
1119
+ chalk4.dim("Run `claude login` to authenticate.")
845
1120
  );
846
1121
  }
847
1122
  } else {
848
- console.log(chalk3.yellow(" !"), "Claude Code not found.");
1123
+ console.log(chalk4.yellow(" !"), "Claude Code not found.");
849
1124
  console.log(
850
- chalk3.dim(" Install it:"),
851
- chalk3.underline("https://docs.anthropic.com/en/docs/claude-code")
1125
+ chalk4.dim(" Install it:"),
1126
+ chalk4.underline("https://docs.anthropic.com/en/docs/claude-code")
852
1127
  );
853
1128
  console.log(
854
- chalk3.dim(" Then run:"),
855
- chalk3.cyan("claude login")
1129
+ chalk4.dim(" Then run:"),
1130
+ chalk4.cyan("claude login")
856
1131
  );
857
1132
  }
858
1133
  console.log();
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);
1134
+ console.log(chalk4.dim(" Device:"), deviceName);
1135
+ console.log(chalk4.dim(" Device ID:"), deviceId);
1136
+ console.log(chalk4.dim(" User ID:"), userId);
1137
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
863
1138
  console.log();
864
1139
  console.log(
865
- chalk3.dim(" Next: start the agent:"),
866
- chalk3.cyan("tenux start")
1140
+ chalk4.dim(" Next: start the agent:"),
1141
+ chalk4.cyan("tenux start")
867
1142
  );
868
1143
  console.log();
869
1144
  });
870
1145
  program.command("start").description("Start the agent and listen for commands").action(async () => {
871
1146
  if (!configExists()) {
872
- console.log(chalk3.red("\u2717"), "Not logged in. Run `tenux login` first.");
1147
+ console.log(chalk4.red("\u2717"), "Not logged in. Run `tenux login` first.");
873
1148
  process.exit(1);
874
1149
  }
875
1150
  const claude = detectClaudeCode();
876
1151
  if (!claude.installed) {
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"));
1152
+ console.log(chalk4.red("\u2717"), "Claude Code not found.");
1153
+ console.log(chalk4.dim(" Install it:"), chalk4.cyan("npm i -g @anthropic-ai/claude-code"));
1154
+ console.log(chalk4.dim(" Then run:"), chalk4.cyan("claude login"));
880
1155
  process.exit(1);
881
1156
  }
882
1157
  const config = loadConfig();
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);
1158
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("desktop agent\n"));
1159
+ console.log(chalk4.dim(" Device:"), config.deviceName);
1160
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
886
1161
  console.log();
887
1162
  const supabase = getSupabase();
888
1163
  try {
889
1164
  await initSupabaseSession();
890
1165
  } catch (err) {
891
- console.log(chalk3.red("\u2717"), `Session expired: ${err.message}`);
892
- console.log(chalk3.dim(" Run `tenux login` to re-authenticate."));
1166
+ console.log(chalk4.red("\u2717"), `Session expired: ${err.message}`);
1167
+ console.log(chalk4.dim(" Run `tenux login` to re-authenticate."));
893
1168
  process.exit(1);
894
1169
  }
895
1170
  await supabase.from("devices").update({
@@ -901,12 +1176,12 @@ program.command("start").description("Start the agent and listen for commands").
901
1176
  const heartbeat = setInterval(async () => {
902
1177
  const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
903
1178
  if (error) {
904
- console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
1179
+ console.log(chalk4.red("\u2717"), chalk4.dim(`Heartbeat failed: ${error.message}`));
905
1180
  }
906
1181
  }, 3e4);
907
1182
  const relay = new Relay(supabase);
908
1183
  const shutdown = async () => {
909
- console.log(chalk3.dim("\n Shutting down..."));
1184
+ console.log(chalk4.dim("\n Shutting down..."));
910
1185
  clearInterval(heartbeat);
911
1186
  await cleanupServers(supabase, config.deviceId);
912
1187
  await relay.stop();
@@ -914,41 +1189,41 @@ program.command("start").description("Start the agent and listen for commands").
914
1189
  process.exit(0);
915
1190
  };
916
1191
  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"));
1192
+ console.log(chalk4.yellow("\n \u26A1 Remote shutdown requested"));
918
1193
  await shutdown();
919
1194
  });
920
1195
  await relay.start();
921
- console.log(chalk3.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
1196
+ console.log(chalk4.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
922
1197
  process.on("SIGINT", shutdown);
923
1198
  process.on("SIGTERM", shutdown);
924
1199
  });
925
1200
  program.command("status").description("Show agent configuration and status").action(() => {
926
1201
  if (!configExists()) {
927
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1202
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
928
1203
  process.exit(1);
929
1204
  }
930
1205
  const config = loadConfig();
931
1206
  const claude = detectClaudeCode();
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);
1207
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("agent status\n"));
1208
+ console.log(chalk4.dim(" Config:"), getConfigPath());
1209
+ console.log(chalk4.dim(" App URL:"), config.appUrl);
1210
+ console.log(chalk4.dim(" Device:"), config.deviceName);
1211
+ console.log(chalk4.dim(" Device ID:"), config.deviceId);
1212
+ console.log(chalk4.dim(" User ID:"), config.userId);
1213
+ console.log(chalk4.dim(" Supabase:"), config.supabaseUrl);
1214
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
940
1215
  console.log(
941
- chalk3.dim(" Auth:"),
942
- config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
1216
+ chalk4.dim(" Auth:"),
1217
+ config.accessToken ? chalk4.green("authenticated") : chalk4.yellow("not authenticated")
943
1218
  );
944
1219
  console.log(
945
- chalk3.dim(" Claude Code:"),
946
- claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
1220
+ chalk4.dim(" Claude Code:"),
1221
+ claude.installed ? chalk4.green("installed") : chalk4.yellow("not installed")
947
1222
  );
948
1223
  if (claude.installed) {
949
1224
  console.log(
950
- chalk3.dim(" Claude Auth:"),
951
- claude.authenticated ? chalk3.green("yes (~/.claude exists)") : chalk3.yellow("not authenticated")
1225
+ chalk4.dim(" Claude Auth:"),
1226
+ claude.authenticated ? chalk4.green("yes (~/.claude exists)") : chalk4.yellow("not authenticated")
952
1227
  );
953
1228
  }
954
1229
  console.log();
@@ -956,7 +1231,7 @@ program.command("status").description("Show agent configuration and status").act
956
1231
  var configCmd = program.command("config").description("Manage agent configuration");
957
1232
  configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
958
1233
  if (!configExists()) {
959
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1234
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
960
1235
  process.exit(1);
961
1236
  }
962
1237
  const keyMap = {
@@ -967,17 +1242,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
967
1242
  const configKey = keyMap[key];
968
1243
  if (!configKey) {
969
1244
  console.log(
970
- chalk3.red("\u2717"),
1245
+ chalk4.red("\u2717"),
971
1246
  `Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
972
1247
  );
973
1248
  process.exit(1);
974
1249
  }
975
1250
  updateConfig({ [configKey]: value });
976
- console.log(chalk3.green("\u2713"), `Set ${key}`);
1251
+ console.log(chalk4.green("\u2713"), `Set ${key}`);
977
1252
  });
978
1253
  configCmd.command("get <key>").description("Get a config value").action((key) => {
979
1254
  if (!configExists()) {
980
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1255
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
981
1256
  process.exit(1);
982
1257
  }
983
1258
  const config = loadConfig();
@@ -992,12 +1267,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
992
1267
  const configKey = keyMap[key];
993
1268
  if (!configKey) {
994
1269
  console.log(
995
- chalk3.red("\u2717"),
1270
+ chalk4.red("\u2717"),
996
1271
  `Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
997
1272
  );
998
1273
  process.exit(1);
999
1274
  }
1000
1275
  const val = config[configKey];
1001
- console.log(val ?? chalk3.dim("(not set)"));
1276
+ console.log(val ?? chalk4.dim("(not set)"));
1002
1277
  });
1003
1278
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenux/cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Tenux — mobile-first IDE for 10x engineering",
5
5
  "author": "Antelogic LLC",
6
6
  "license": "MIT",