@tenux/cli 0.0.22 → 0.0.24

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 +420 -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,318 @@ 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
+ let ab;
507
+ if (data instanceof ArrayBuffer) {
508
+ ab = data;
509
+ } else if (Buffer.isBuffer(data)) {
510
+ ab = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
511
+ } else {
512
+ return;
513
+ }
514
+ handleRelayMessage(conn, ab);
515
+ });
516
+ ws.on("unexpected-response", (_req, res) => {
517
+ console.log(chalk2.red("\u2717"), `[tunnel] Upgrade rejected (HTTP ${res.statusCode})`);
518
+ if (!conn.closed) {
519
+ conn.reconnectTimer = setTimeout(connect, 5e3);
520
+ }
521
+ });
522
+ ws.on("close", () => {
523
+ if (conn.closed) return;
524
+ console.log(chalk2.yellow("!"), `[tunnel] Disconnected, reconnecting in 3s...`);
525
+ conn.reconnectTimer = setTimeout(connect, 3e3);
526
+ });
527
+ ws.on("error", (err) => {
528
+ console.log(chalk2.yellow("!"), `[tunnel] Error: ${err.message}`);
529
+ });
530
+ }
531
+ connect();
532
+ activeTunnels.set(instanceId, conn);
533
+ return tunnelUrl;
534
+ }
535
+ function disconnectTunnel(instanceId) {
536
+ const conn = activeTunnels.get(instanceId);
537
+ if (!conn) return;
538
+ conn.closed = true;
539
+ if (conn.reconnectTimer) clearTimeout(conn.reconnectTimer);
540
+ for (const [, localWs] of conn.localWsSockets) {
541
+ try {
542
+ localWs.close();
543
+ } catch {
544
+ }
545
+ }
546
+ conn.localWsSockets.clear();
547
+ try {
548
+ conn.ws.close(1e3, "tunnel disconnect");
549
+ } catch {
550
+ }
551
+ activeTunnels.delete(instanceId);
552
+ console.log(chalk2.dim(" [tunnel] Disconnected:", instanceId));
553
+ }
554
+ function disconnectAllTunnels() {
555
+ for (const instanceId of activeTunnels.keys()) {
556
+ disconnectTunnel(instanceId);
557
+ }
558
+ }
559
+ function handleRelayMessage(conn, data) {
560
+ const type = getMessageType(data);
561
+ switch (type) {
562
+ case MSG_HTTP_REQUEST:
563
+ handleHttpRequest(conn, data);
564
+ break;
565
+ case MSG_WS_OPEN:
566
+ handleWsOpen(conn, data);
567
+ break;
568
+ case MSG_WS_FRAME:
569
+ handleWsFrame(conn, data);
570
+ break;
571
+ case MSG_WS_CLOSE:
572
+ handleWsClose(conn, data);
573
+ break;
574
+ }
575
+ }
576
+ function handleHttpRequest(conn, data) {
577
+ const req = decodeHttpRequest(data);
578
+ const options = {
579
+ hostname: "127.0.0.1",
580
+ port: conn.localPort,
581
+ path: req.path,
582
+ method: req.method,
583
+ headers: {
584
+ ...req.headers,
585
+ host: `127.0.0.1:${conn.localPort}`
586
+ }
587
+ };
588
+ const localReq = http.request(options, (localRes) => {
589
+ const chunks = [];
590
+ localRes.on("data", (chunk) => {
591
+ chunks.push(chunk);
592
+ });
593
+ localRes.on("end", () => {
594
+ const body = Buffer.concat(chunks);
595
+ const headers = {};
596
+ for (const [key, value] of Object.entries(localRes.headers)) {
597
+ if (value) {
598
+ headers[key] = Array.isArray(value) ? value.join(", ") : value;
599
+ }
600
+ }
601
+ const responseMsg = encodeHttpResponse(req.requestId, localRes.statusCode ?? 500, headers, body);
602
+ try {
603
+ if (conn.ws.readyState === WebSocket.OPEN) {
604
+ conn.ws.send(responseMsg);
605
+ }
606
+ } catch (err) {
607
+ console.log(chalk2.yellow("!"), `[tunnel] Failed to send response: ${err.message}`);
608
+ }
609
+ });
610
+ });
611
+ localReq.on("error", (err) => {
612
+ const body = Buffer.from(`Local server error: ${err.message}`);
613
+ const headers = { "content-type": "text/plain" };
614
+ const responseMsg = encodeHttpResponse(req.requestId, 502, headers, body);
615
+ try {
616
+ if (conn.ws.readyState === WebSocket.OPEN) {
617
+ conn.ws.send(responseMsg);
618
+ }
619
+ } catch {
620
+ }
621
+ });
622
+ if (req.body.length > 0) {
623
+ localReq.write(req.body);
624
+ }
625
+ localReq.end();
626
+ }
627
+ function handleWsOpen(conn, data) {
628
+ const { channelId, path, headers } = decodeWsOpen(data);
629
+ const wsUrl = `ws://127.0.0.1:${conn.localPort}${path}`;
630
+ const localWs = new WebSocket(wsUrl, {
631
+ headers: {
632
+ ...headers,
633
+ host: `127.0.0.1:${conn.localPort}`
634
+ }
635
+ });
636
+ localWs.binaryType = "arraybuffer";
637
+ conn.localWsSockets.set(channelId, localWs);
638
+ localWs.on("message", (data2) => {
639
+ const payload = Buffer.isBuffer(data2) ? data2 : Buffer.from(data2);
640
+ const frame = encodeWsFrame(channelId, payload);
641
+ try {
642
+ if (conn.ws.readyState === WebSocket.OPEN) {
643
+ conn.ws.send(frame);
644
+ }
645
+ } catch {
646
+ }
647
+ });
648
+ localWs.on("close", (code) => {
649
+ conn.localWsSockets.delete(channelId);
650
+ const closeMsg = encodeWsClose(channelId, code || 1e3);
651
+ try {
652
+ if (conn.ws.readyState === WebSocket.OPEN) {
653
+ conn.ws.send(closeMsg);
654
+ }
655
+ } catch {
656
+ }
657
+ });
658
+ localWs.on("error", (err) => {
659
+ console.log(chalk2.dim(` [tunnel] Local WS error (channel ${channelId}): ${err.message}`));
660
+ conn.localWsSockets.delete(channelId);
661
+ const closeMsg = encodeWsClose(channelId, 1011);
662
+ try {
663
+ if (conn.ws.readyState === WebSocket.OPEN) {
664
+ conn.ws.send(closeMsg);
665
+ }
666
+ } catch {
667
+ }
668
+ });
669
+ }
670
+ function handleWsFrame(conn, data) {
671
+ const channelId = getChannelId(data);
672
+ const payload = getWsFramePayload(data);
673
+ const localWs = conn.localWsSockets.get(channelId);
674
+ if (localWs && localWs.readyState === WebSocket.OPEN) {
675
+ try {
676
+ localWs.send(payload);
677
+ } catch {
678
+ }
679
+ }
680
+ }
681
+ function handleWsClose(conn, data) {
682
+ const channelId = getChannelId(data);
683
+ const closeCode = getWsCloseCode(data);
684
+ const localWs = conn.localWsSockets.get(channelId);
685
+ if (localWs) {
686
+ conn.localWsSockets.delete(channelId);
687
+ try {
688
+ localWs.close(closeCode);
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+
694
+ // src/handlers/server.ts
695
+ import chalk3 from "chalk";
388
696
  var MAX_LOG_LINES = 500;
389
697
  var LOG_PUSH_INTERVAL = 2e3;
390
698
  var runningServers = /* @__PURE__ */ new Map();
@@ -408,39 +716,6 @@ function killTree(child) {
408
716
  child.kill("SIGTERM");
409
717
  }
410
718
  }
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
719
  async function handleServerStart(command, supabase) {
445
720
  const payload = command.payload;
446
721
  const config = loadConfig();
@@ -453,6 +728,7 @@ async function handleServerStart(command, supabase) {
453
728
  if (existing) {
454
729
  clearInterval(existing.logInterval);
455
730
  killTree(existing.process);
731
+ if (existing.tunnelUrl) disconnectTunnel(existing.instanceId);
456
732
  runningServers.delete(key);
457
733
  }
458
734
  const { data: instance, error: upsertErr } = await supabase.from("server_instances").upsert(
@@ -466,7 +742,7 @@ async function handleServerStart(command, supabase) {
466
742
  port: payload.port ?? null,
467
743
  pid: null,
468
744
  detected_port: null,
469
- tailscale_url: null,
745
+ tunnel_url: null,
470
746
  logs: [],
471
747
  error_message: null,
472
748
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -478,9 +754,11 @@ async function handleServerStart(command, supabase) {
478
754
  if (upsertErr || !instance) {
479
755
  throw new Error(`Failed to create server instance: ${upsertErr?.message}`);
480
756
  }
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}`));
757
+ console.log(chalk3.blue("\u2192"), `Starting server: ${payload.project_name}::${payload.command_name}`);
758
+ console.log(chalk3.dim(` cmd: ${payload.command}`));
759
+ console.log(chalk3.dim(` cwd: ${fullPath}`));
760
+ const { data: sessionData } = await supabase.auth.getSession();
761
+ const accessToken = sessionData?.session?.access_token ?? "";
484
762
  const env = { ...process.env, FORCE_COLOR: "0" };
485
763
  delete env.PORT;
486
764
  delete env.NODE_OPTIONS;
@@ -510,17 +788,22 @@ async function handleServerStart(command, supabase) {
510
788
  const actualPort = parseInt(portMatch[1], 10);
511
789
  detectedPort = true;
512
790
  managed.detectedPort = actualPort;
513
- setupTailscaleServe(actualPort);
514
- const hostname2 = getTailscaleHostname();
515
- const tsUrl = hostname2 ? `https://${hostname2}:${actualPort}` : null;
791
+ const tunnelUrl = connectTunnel(
792
+ instance.id,
793
+ actualPort,
794
+ accessToken,
795
+ command.user_id
796
+ );
797
+ managed.tunnelUrl = tunnelUrl;
516
798
  supabase.from("server_instances").update({
517
799
  status: "running",
518
800
  detected_port: actualPort,
519
- tailscale_url: tsUrl,
801
+ tunnel_url: tunnelUrl,
520
802
  pid: child.pid ?? null,
521
803
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
522
804
  }).eq("id", instance.id).then(() => {
523
- console.log(chalk2.green("\u2713"), `Server running on :${actualPort}`);
805
+ console.log(chalk3.green("\u2713"), `Server running on :${actualPort}`);
806
+ console.log(chalk3.green("\u2713"), `Tunnel: ${tunnelUrl}`);
524
807
  });
525
808
  }
526
809
  }
@@ -549,19 +832,18 @@ async function handleServerStart(command, supabase) {
549
832
  child.on("close", async (code) => {
550
833
  clearInterval(logInterval);
551
834
  logs.push(`[Process exited with code ${code}]`);
552
- if (managed.detectedPort) {
553
- teardownTailscaleServe(managed.detectedPort);
554
- }
835
+ disconnectTunnel(managed.instanceId);
555
836
  runningServers.delete(key);
556
837
  await supabase.from("server_instances").update({
557
838
  status: code === 0 ? "stopped" : "error",
558
839
  error_message: code !== 0 ? `Exited with code ${code}` : null,
559
840
  logs: logs.slice(-100),
841
+ tunnel_url: null,
560
842
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
561
843
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
562
844
  }).eq("id", instance.id);
563
845
  console.log(
564
- code === 0 ? chalk2.dim(" Server stopped") : chalk2.yellow("!"),
846
+ code === 0 ? chalk3.dim(" Server stopped") : chalk3.yellow("!"),
565
847
  `${payload.project_name}::${payload.command_name} exited (code ${code})`
566
848
  );
567
849
  });
@@ -576,13 +858,17 @@ async function handleServerStart(command, supabase) {
576
858
  const configuredPort = payload.port;
577
859
  if (configuredPort) {
578
860
  managed.detectedPort = configuredPort;
579
- setupTailscaleServe(configuredPort);
580
- const hostname2 = getTailscaleHostname();
581
- const tsUrl = hostname2 ? `https://${hostname2}:${configuredPort}` : null;
861
+ const tunnelUrl = connectTunnel(
862
+ instance.id,
863
+ configuredPort,
864
+ accessToken,
865
+ command.user_id
866
+ );
867
+ managed.tunnelUrl = tunnelUrl;
582
868
  await supabase.from("server_instances").update({
583
869
  status: "running",
584
870
  detected_port: configuredPort,
585
- tailscale_url: tsUrl,
871
+ tunnel_url: tunnelUrl,
586
872
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
587
873
  }).eq("id", instance.id);
588
874
  } else {
@@ -601,13 +887,14 @@ async function handleServerStop(command, supabase) {
601
887
  const key = makeKey(payload.project_name, payload.command_name);
602
888
  const managed = runningServers.get(key);
603
889
  if (managed) {
604
- console.log(chalk2.blue("\u2192"), `Stopping server: ${key}`);
890
+ console.log(chalk3.blue("\u2192"), `Stopping server: ${key}`);
605
891
  clearInterval(managed.logInterval);
606
892
  killTree(managed.process);
607
893
  } else {
608
894
  if (payload.server_instance_id) {
609
895
  await supabase.from("server_instances").update({
610
896
  status: "stopped",
897
+ tunnel_url: null,
611
898
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
612
899
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
613
900
  }).eq("id", payload.server_instance_id);
@@ -628,6 +915,7 @@ async function handleServerStatus(command, supabase) {
628
915
  running: managed.process.exitCode === null,
629
916
  pid: managed.process.pid,
630
917
  detectedPort: managed.detectedPort,
918
+ tunnelUrl: managed.tunnelUrl,
631
919
  logCount: managed.logs.length
632
920
  });
633
921
  }
@@ -639,18 +927,17 @@ async function handleServerStatus(command, supabase) {
639
927
  }
640
928
  async function cleanupServers(supabase, deviceId) {
641
929
  for (const [key, managed] of runningServers) {
642
- console.log(chalk2.dim(` Stopping server: ${key}`));
930
+ console.log(chalk3.dim(` Stopping server: ${key}`));
643
931
  clearInterval(managed.logInterval);
644
932
  if (managed.process.exitCode === null) {
645
933
  killTree(managed.process);
646
934
  }
647
- if (managed.detectedPort) {
648
- teardownTailscaleServe(managed.detectedPort);
649
- }
650
935
  }
651
936
  runningServers.clear();
937
+ disconnectAllTunnels();
652
938
  await supabase.from("server_instances").update({
653
939
  status: "stopped",
940
+ tunnel_url: null,
654
941
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
655
942
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
656
943
  }).eq("device_id", deviceId).in("status", ["starting", "running"]);
@@ -658,12 +945,13 @@ async function cleanupServers(supabase, deviceId) {
658
945
  async function cleanupStaleServers(supabase, deviceId) {
659
946
  const { data, error } = await supabase.from("server_instances").update({
660
947
  status: "stopped",
948
+ tunnel_url: null,
661
949
  error_message: "Agent restarted",
662
950
  stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
663
951
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
664
952
  }).eq("device_id", deviceId).in("status", ["starting", "running"]).select("id");
665
953
  if (data && data.length > 0) {
666
- console.log(chalk2.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
954
+ console.log(chalk3.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
667
955
  }
668
956
  }
669
957
 
@@ -691,15 +979,15 @@ async function sleep(ms) {
691
979
  return new Promise((resolve3) => setTimeout(resolve3, ms));
692
980
  }
693
981
  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"));
982
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("desktop agent\n"));
695
983
  const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
696
- console.log(chalk3.dim(" App:"), appUrl);
984
+ console.log(chalk4.dim(" App:"), appUrl);
697
985
  const deviceName = hostname();
698
986
  const platform = process.platform;
699
- console.log(chalk3.dim(" Device:"), deviceName);
700
- console.log(chalk3.dim(" Platform:"), platform);
987
+ console.log(chalk4.dim(" Device:"), deviceName);
988
+ console.log(chalk4.dim(" Platform:"), platform);
701
989
  console.log();
702
- console.log(chalk3.dim(" Requesting device code..."));
990
+ console.log(chalk4.dim(" Requesting device code..."));
703
991
  let code;
704
992
  let expiresAt;
705
993
  let supabaseUrl;
@@ -716,7 +1004,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
716
1004
  });
717
1005
  if (!res.ok) {
718
1006
  const text = await res.text();
719
- console.log(chalk3.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
1007
+ console.log(chalk4.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
720
1008
  process.exit(1);
721
1009
  }
722
1010
  const data = await res.json();
@@ -726,26 +1014,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
726
1014
  supabaseAnonKey = data.supabase_anon_key;
727
1015
  } catch (err) {
728
1016
  console.log(
729
- chalk3.red(" \u2717"),
1017
+ chalk4.red(" \u2717"),
730
1018
  `Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
731
1019
  );
732
1020
  process.exit(1);
733
1021
  }
734
1022
  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`));
1023
+ 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`));
1024
+ console.log(chalk4.bold.cyan(` \u2502 \u2502`));
1025
+ console.log(chalk4.bold.cyan(` \u2502 Code: ${chalk4.white.bold(code)} \u2502`));
1026
+ console.log(chalk4.bold.cyan(` \u2502 \u2502`));
1027
+ 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
1028
  console.log();
741
1029
  const linkUrl = `${appUrl}/link?code=${code}`;
742
- console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
1030
+ console.log(chalk4.dim(" Opening browser to:"), chalk4.underline(linkUrl));
743
1031
  try {
744
1032
  const open = (await import("open")).default;
745
1033
  await open(linkUrl);
746
1034
  } catch {
747
- console.log(chalk3.yellow(" !"), "Could not open browser automatically.");
748
- console.log(chalk3.dim(" Open this URL manually:"), chalk3.underline(linkUrl));
1035
+ console.log(chalk4.yellow(" !"), "Could not open browser automatically.");
1036
+ console.log(chalk4.dim(" Open this URL manually:"), chalk4.underline(linkUrl));
749
1037
  }
750
1038
  console.log();
751
1039
  const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -757,11 +1045,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
757
1045
  const expiresTime = new Date(expiresAt).getTime();
758
1046
  while (!approved) {
759
1047
  if (Date.now() > expiresTime) {
760
- console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
1048
+ console.log(chalk4.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
761
1049
  process.exit(1);
762
1050
  }
763
1051
  process.stdout.write(
764
- `\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
1052
+ `\r ${chalk4.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
765
1053
  );
766
1054
  dotIndex++;
767
1055
  await sleep(2e3);
@@ -779,16 +1067,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
779
1067
  userId = data.user_id || "";
780
1068
  approved = true;
781
1069
  } else if (data.status === "expired") {
782
- console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
1070
+ console.log(chalk4.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
783
1071
  process.exit(1);
784
1072
  }
785
1073
  } catch {
786
1074
  }
787
1075
  }
788
1076
  process.stdout.write("\r" + " ".repeat(60) + "\r");
789
- console.log(chalk3.green(" \u2713"), "Device approved!");
1077
+ console.log(chalk4.green(" \u2713"), "Device approved!");
790
1078
  console.log();
791
- console.log(chalk3.dim(" Exchanging token for session..."));
1079
+ console.log(chalk4.dim(" Exchanging token for session..."));
792
1080
  let accessToken = "";
793
1081
  let refreshToken = "";
794
1082
  try {
@@ -800,11 +1088,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
800
1088
  type: "magiclink"
801
1089
  });
802
1090
  if (error) {
803
- console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
1091
+ console.log(chalk4.red(" \u2717"), `Auth failed: ${error.message}`);
804
1092
  process.exit(1);
805
1093
  }
806
1094
  if (!session.session) {
807
- console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
1095
+ console.log(chalk4.red(" \u2717"), "No session returned from auth exchange.");
808
1096
  process.exit(1);
809
1097
  }
810
1098
  accessToken = session.session.access_token;
@@ -812,12 +1100,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
812
1100
  userId = session.session.user?.id || userId;
813
1101
  } catch (err) {
814
1102
  console.log(
815
- chalk3.red(" \u2717"),
1103
+ chalk4.red(" \u2717"),
816
1104
  `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
817
1105
  );
818
1106
  process.exit(1);
819
1107
  }
820
- console.log(chalk3.green(" \u2713"), "Session established.");
1108
+ console.log(chalk4.green(" \u2713"), "Session established.");
821
1109
  const config = {
822
1110
  appUrl,
823
1111
  supabaseUrl,
@@ -830,66 +1118,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
830
1118
  projectsDir: join2(homedir(), ".tenux", "projects")
831
1119
  };
832
1120
  saveConfig(config);
833
- console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
1121
+ console.log(chalk4.green(" \u2713"), "Config saved to", chalk4.dim(getConfigPath()));
834
1122
  console.log();
835
1123
  const claude = detectClaudeCode();
836
1124
  if (claude.installed) {
837
- console.log(chalk3.green(" \u2713"), "Claude Code detected");
1125
+ console.log(chalk4.green(" \u2713"), "Claude Code detected");
838
1126
  if (claude.authenticated) {
839
- console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
1127
+ console.log(chalk4.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
840
1128
  } else {
841
1129
  console.log(
842
- chalk3.yellow(" !"),
1130
+ chalk4.yellow(" !"),
843
1131
  "Claude Code found but ~/.claude not detected.",
844
- chalk3.dim("Run `claude login` to authenticate.")
1132
+ chalk4.dim("Run `claude login` to authenticate.")
845
1133
  );
846
1134
  }
847
1135
  } else {
848
- console.log(chalk3.yellow(" !"), "Claude Code not found.");
1136
+ console.log(chalk4.yellow(" !"), "Claude Code not found.");
849
1137
  console.log(
850
- chalk3.dim(" Install it:"),
851
- chalk3.underline("https://docs.anthropic.com/en/docs/claude-code")
1138
+ chalk4.dim(" Install it:"),
1139
+ chalk4.underline("https://docs.anthropic.com/en/docs/claude-code")
852
1140
  );
853
1141
  console.log(
854
- chalk3.dim(" Then run:"),
855
- chalk3.cyan("claude login")
1142
+ chalk4.dim(" Then run:"),
1143
+ chalk4.cyan("claude login")
856
1144
  );
857
1145
  }
858
1146
  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);
1147
+ console.log(chalk4.dim(" Device:"), deviceName);
1148
+ console.log(chalk4.dim(" Device ID:"), deviceId);
1149
+ console.log(chalk4.dim(" User ID:"), userId);
1150
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
863
1151
  console.log();
864
1152
  console.log(
865
- chalk3.dim(" Next: start the agent:"),
866
- chalk3.cyan("tenux start")
1153
+ chalk4.dim(" Next: start the agent:"),
1154
+ chalk4.cyan("tenux start")
867
1155
  );
868
1156
  console.log();
869
1157
  });
870
1158
  program.command("start").description("Start the agent and listen for commands").action(async () => {
871
1159
  if (!configExists()) {
872
- console.log(chalk3.red("\u2717"), "Not logged in. Run `tenux login` first.");
1160
+ console.log(chalk4.red("\u2717"), "Not logged in. Run `tenux login` first.");
873
1161
  process.exit(1);
874
1162
  }
875
1163
  const claude = detectClaudeCode();
876
1164
  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"));
1165
+ console.log(chalk4.red("\u2717"), "Claude Code not found.");
1166
+ console.log(chalk4.dim(" Install it:"), chalk4.cyan("npm i -g @anthropic-ai/claude-code"));
1167
+ console.log(chalk4.dim(" Then run:"), chalk4.cyan("claude login"));
880
1168
  process.exit(1);
881
1169
  }
882
1170
  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);
1171
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("desktop agent\n"));
1172
+ console.log(chalk4.dim(" Device:"), config.deviceName);
1173
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
886
1174
  console.log();
887
1175
  const supabase = getSupabase();
888
1176
  try {
889
1177
  await initSupabaseSession();
890
1178
  } catch (err) {
891
- console.log(chalk3.red("\u2717"), `Session expired: ${err.message}`);
892
- console.log(chalk3.dim(" Run `tenux login` to re-authenticate."));
1179
+ console.log(chalk4.red("\u2717"), `Session expired: ${err.message}`);
1180
+ console.log(chalk4.dim(" Run `tenux login` to re-authenticate."));
893
1181
  process.exit(1);
894
1182
  }
895
1183
  await supabase.from("devices").update({
@@ -901,12 +1189,12 @@ program.command("start").description("Start the agent and listen for commands").
901
1189
  const heartbeat = setInterval(async () => {
902
1190
  const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
903
1191
  if (error) {
904
- console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
1192
+ console.log(chalk4.red("\u2717"), chalk4.dim(`Heartbeat failed: ${error.message}`));
905
1193
  }
906
1194
  }, 3e4);
907
1195
  const relay = new Relay(supabase);
908
1196
  const shutdown = async () => {
909
- console.log(chalk3.dim("\n Shutting down..."));
1197
+ console.log(chalk4.dim("\n Shutting down..."));
910
1198
  clearInterval(heartbeat);
911
1199
  await cleanupServers(supabase, config.deviceId);
912
1200
  await relay.stop();
@@ -914,41 +1202,41 @@ program.command("start").description("Start the agent and listen for commands").
914
1202
  process.exit(0);
915
1203
  };
916
1204
  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"));
1205
+ console.log(chalk4.yellow("\n \u26A1 Remote shutdown requested"));
918
1206
  await shutdown();
919
1207
  });
920
1208
  await relay.start();
921
- console.log(chalk3.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
1209
+ console.log(chalk4.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
922
1210
  process.on("SIGINT", shutdown);
923
1211
  process.on("SIGTERM", shutdown);
924
1212
  });
925
1213
  program.command("status").description("Show agent configuration and status").action(() => {
926
1214
  if (!configExists()) {
927
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1215
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
928
1216
  process.exit(1);
929
1217
  }
930
1218
  const config = loadConfig();
931
1219
  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);
1220
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("agent status\n"));
1221
+ console.log(chalk4.dim(" Config:"), getConfigPath());
1222
+ console.log(chalk4.dim(" App URL:"), config.appUrl);
1223
+ console.log(chalk4.dim(" Device:"), config.deviceName);
1224
+ console.log(chalk4.dim(" Device ID:"), config.deviceId);
1225
+ console.log(chalk4.dim(" User ID:"), config.userId);
1226
+ console.log(chalk4.dim(" Supabase:"), config.supabaseUrl);
1227
+ console.log(chalk4.dim(" Projects:"), config.projectsDir);
940
1228
  console.log(
941
- chalk3.dim(" Auth:"),
942
- config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
1229
+ chalk4.dim(" Auth:"),
1230
+ config.accessToken ? chalk4.green("authenticated") : chalk4.yellow("not authenticated")
943
1231
  );
944
1232
  console.log(
945
- chalk3.dim(" Claude Code:"),
946
- claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
1233
+ chalk4.dim(" Claude Code:"),
1234
+ claude.installed ? chalk4.green("installed") : chalk4.yellow("not installed")
947
1235
  );
948
1236
  if (claude.installed) {
949
1237
  console.log(
950
- chalk3.dim(" Claude Auth:"),
951
- claude.authenticated ? chalk3.green("yes (~/.claude exists)") : chalk3.yellow("not authenticated")
1238
+ chalk4.dim(" Claude Auth:"),
1239
+ claude.authenticated ? chalk4.green("yes (~/.claude exists)") : chalk4.yellow("not authenticated")
952
1240
  );
953
1241
  }
954
1242
  console.log();
@@ -956,7 +1244,7 @@ program.command("status").description("Show agent configuration and status").act
956
1244
  var configCmd = program.command("config").description("Manage agent configuration");
957
1245
  configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
958
1246
  if (!configExists()) {
959
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1247
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
960
1248
  process.exit(1);
961
1249
  }
962
1250
  const keyMap = {
@@ -967,17 +1255,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
967
1255
  const configKey = keyMap[key];
968
1256
  if (!configKey) {
969
1257
  console.log(
970
- chalk3.red("\u2717"),
1258
+ chalk4.red("\u2717"),
971
1259
  `Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
972
1260
  );
973
1261
  process.exit(1);
974
1262
  }
975
1263
  updateConfig({ [configKey]: value });
976
- console.log(chalk3.green("\u2713"), `Set ${key}`);
1264
+ console.log(chalk4.green("\u2713"), `Set ${key}`);
977
1265
  });
978
1266
  configCmd.command("get <key>").description("Get a config value").action((key) => {
979
1267
  if (!configExists()) {
980
- console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
1268
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
981
1269
  process.exit(1);
982
1270
  }
983
1271
  const config = loadConfig();
@@ -992,12 +1280,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
992
1280
  const configKey = keyMap[key];
993
1281
  if (!configKey) {
994
1282
  console.log(
995
- chalk3.red("\u2717"),
1283
+ chalk4.red("\u2717"),
996
1284
  `Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
997
1285
  );
998
1286
  process.exit(1);
999
1287
  }
1000
1288
  const val = config[configKey];
1001
- console.log(val ?? chalk3.dim("(not set)"));
1289
+ console.log(val ?? chalk4.dim("(not set)"));
1002
1290
  });
1003
1291
  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.24",
4
4
  "description": "Tenux — mobile-first IDE for 10x engineering",
5
5
  "author": "Antelogic LLC",
6
6
  "license": "MIT",