@tenux/cli 0.0.21 → 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 +648 -84
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,12 +13,12 @@ import {
13
13
 
14
14
  // src/cli.ts
15
15
  import { Command } from "commander";
16
- import chalk2 from "chalk";
16
+ import chalk4 from "chalk";
17
17
  import { hostname } from "os";
18
18
  import { homedir } from "os";
19
19
  import { join as join2, dirname } from "path";
20
- import { existsSync as existsSync2, readFileSync } from "fs";
21
- import { execSync as execSync2 } from "child_process";
20
+ import { existsSync as existsSync3, readFileSync } from "fs";
21
+ import { execSync as execSync3 } from "child_process";
22
22
  import { fileURLToPath } from "url";
23
23
  import { createClient } from "@supabase/supabase-js";
24
24
 
@@ -65,7 +65,7 @@ function installCommand(pm) {
65
65
  }
66
66
  }
67
67
  function run(cmd, args, cwd, timeoutMs = 3e5) {
68
- return new Promise((resolve2, reject) => {
68
+ return new Promise((resolve3, reject) => {
69
69
  const isWindows = process.platform === "win32";
70
70
  const proc = spawn(cmd, args, {
71
71
  cwd,
@@ -82,7 +82,7 @@ function run(cmd, args, cwd, timeoutMs = 3e5) {
82
82
  stderr += d.toString();
83
83
  });
84
84
  proc.on("close", (code) => {
85
- if (code === 0) resolve2(stdout);
85
+ if (code === 0) resolve3(stdout);
86
86
  else reject(new Error(stderr.trim().slice(0, 500) || `Exit code ${code}`));
87
87
  });
88
88
  proc.on("error", reject);
@@ -380,6 +380,568 @@ async function handleProjectDelete(command, supabase) {
380
380
  }
381
381
  }
382
382
 
383
+ // src/handlers/server.ts
384
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
385
+ import { resolve as resolve2 } from "path";
386
+ import { existsSync as existsSync2 } from "fs";
387
+
388
+ // src/lib/tunnel.ts
389
+ import http from "http";
390
+ import WebSocket from "ws";
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";
683
+ var MAX_LOG_LINES = 500;
684
+ var LOG_PUSH_INTERVAL = 2e3;
685
+ var runningServers = /* @__PURE__ */ new Map();
686
+ function makeKey(projectName, commandName) {
687
+ return `${projectName}::${commandName}`;
688
+ }
689
+ function killTree(child) {
690
+ if (child.exitCode !== null) return;
691
+ const pid = child.pid;
692
+ if (!pid) {
693
+ child.kill("SIGTERM");
694
+ return;
695
+ }
696
+ try {
697
+ if (process.platform === "win32") {
698
+ execSync2(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
699
+ } else {
700
+ process.kill(-pid, "SIGTERM");
701
+ }
702
+ } catch {
703
+ child.kill("SIGTERM");
704
+ }
705
+ }
706
+ async function handleServerStart(command, supabase) {
707
+ const payload = command.payload;
708
+ const config = loadConfig();
709
+ const fullPath = payload.project_path.startsWith("/") || payload.project_path.match(/^[A-Z]:\\/i) ? payload.project_path : resolve2(config.projectsDir, payload.project_path);
710
+ if (!existsSync2(fullPath)) {
711
+ throw new Error(`Project path not found: ${fullPath}`);
712
+ }
713
+ const key = makeKey(payload.project_name, payload.command_name);
714
+ const existing = runningServers.get(key);
715
+ if (existing) {
716
+ clearInterval(existing.logInterval);
717
+ killTree(existing.process);
718
+ if (existing.tunnelUrl) disconnectTunnel(existing.instanceId);
719
+ runningServers.delete(key);
720
+ }
721
+ const { data: instance, error: upsertErr } = await supabase.from("server_instances").upsert(
722
+ {
723
+ user_id: command.user_id,
724
+ device_id: command.device_id,
725
+ project_id: payload.project_id,
726
+ command_name: payload.command_name,
727
+ command: payload.command,
728
+ status: "starting",
729
+ port: payload.port ?? null,
730
+ pid: null,
731
+ detected_port: null,
732
+ tunnel_url: null,
733
+ logs: [],
734
+ error_message: null,
735
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
736
+ stopped_at: null,
737
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
738
+ },
739
+ { onConflict: "device_id,project_id,command_name" }
740
+ ).select().single();
741
+ if (upsertErr || !instance) {
742
+ throw new Error(`Failed to create server instance: ${upsertErr?.message}`);
743
+ }
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 ?? "";
749
+ const env = { ...process.env, FORCE_COLOR: "0" };
750
+ delete env.PORT;
751
+ delete env.NODE_OPTIONS;
752
+ const parts = payload.command.split(/\s+/).filter(Boolean);
753
+ const bin = parts[0];
754
+ const cmdArgs = parts.slice(1);
755
+ const isWindows = process.platform === "win32";
756
+ const child = spawn2(bin, cmdArgs, {
757
+ cwd: fullPath,
758
+ shell: isWindows,
759
+ stdio: ["ignore", "pipe", "pipe"],
760
+ env
761
+ });
762
+ const logs = [];
763
+ let detectedPort = false;
764
+ let logDirty = false;
765
+ const pushLog = (text) => {
766
+ const lines = text.split("\n");
767
+ for (const line of lines) {
768
+ if (line.trim()) {
769
+ logs.push(line);
770
+ if (logs.length > MAX_LOG_LINES) logs.shift();
771
+ logDirty = true;
772
+ if (!detectedPort) {
773
+ 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);
774
+ if (portMatch) {
775
+ const actualPort = parseInt(portMatch[1], 10);
776
+ detectedPort = true;
777
+ managed.detectedPort = actualPort;
778
+ const tunnelUrl = connectTunnel(
779
+ instance.id,
780
+ actualPort,
781
+ accessToken,
782
+ command.user_id
783
+ );
784
+ managed.tunnelUrl = tunnelUrl;
785
+ supabase.from("server_instances").update({
786
+ status: "running",
787
+ detected_port: actualPort,
788
+ tunnel_url: tunnelUrl,
789
+ pid: child.pid ?? null,
790
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
791
+ }).eq("id", instance.id).then(() => {
792
+ console.log(chalk3.green("\u2713"), `Server running on :${actualPort}`);
793
+ console.log(chalk3.green("\u2713"), `Tunnel: ${tunnelUrl}`);
794
+ });
795
+ }
796
+ }
797
+ }
798
+ }
799
+ };
800
+ child.stdout.on("data", (chunk) => pushLog(chunk.toString()));
801
+ child.stderr.on("data", (chunk) => pushLog(chunk.toString()));
802
+ const logInterval = setInterval(async () => {
803
+ if (!logDirty) return;
804
+ logDirty = false;
805
+ await supabase.from("server_instances").update({
806
+ logs: logs.slice(-100),
807
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
808
+ }).eq("id", instance.id);
809
+ }, LOG_PUSH_INTERVAL);
810
+ const managed = {
811
+ instanceId: instance.id,
812
+ projectName: payload.project_name,
813
+ commandName: payload.command_name,
814
+ command: payload.command,
815
+ process: child,
816
+ logs,
817
+ logInterval
818
+ };
819
+ child.on("close", async (code) => {
820
+ clearInterval(logInterval);
821
+ logs.push(`[Process exited with code ${code}]`);
822
+ disconnectTunnel(managed.instanceId);
823
+ runningServers.delete(key);
824
+ await supabase.from("server_instances").update({
825
+ status: code === 0 ? "stopped" : "error",
826
+ error_message: code !== 0 ? `Exited with code ${code}` : null,
827
+ logs: logs.slice(-100),
828
+ tunnel_url: null,
829
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
830
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
831
+ }).eq("id", instance.id);
832
+ console.log(
833
+ code === 0 ? chalk3.dim(" Server stopped") : chalk3.yellow("!"),
834
+ `${payload.project_name}::${payload.command_name} exited (code ${code})`
835
+ );
836
+ });
837
+ runningServers.set(key, managed);
838
+ await supabase.from("server_instances").update({
839
+ pid: child.pid ?? null,
840
+ status: "starting",
841
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
842
+ }).eq("id", instance.id);
843
+ setTimeout(async () => {
844
+ if (!detectedPort) {
845
+ const configuredPort = payload.port;
846
+ if (configuredPort) {
847
+ managed.detectedPort = configuredPort;
848
+ const tunnelUrl = connectTunnel(
849
+ instance.id,
850
+ configuredPort,
851
+ accessToken,
852
+ command.user_id
853
+ );
854
+ managed.tunnelUrl = tunnelUrl;
855
+ await supabase.from("server_instances").update({
856
+ status: "running",
857
+ detected_port: configuredPort,
858
+ tunnel_url: tunnelUrl,
859
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
860
+ }).eq("id", instance.id);
861
+ } else {
862
+ await supabase.from("server_instances").update({ status: "running", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", instance.id);
863
+ }
864
+ }
865
+ }, 8e3);
866
+ await supabase.from("commands").update({
867
+ status: "done",
868
+ result: { server_instance_id: instance.id },
869
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
870
+ }).eq("id", command.id);
871
+ }
872
+ async function handleServerStop(command, supabase) {
873
+ const payload = command.payload;
874
+ const key = makeKey(payload.project_name, payload.command_name);
875
+ const managed = runningServers.get(key);
876
+ if (managed) {
877
+ console.log(chalk3.blue("\u2192"), `Stopping server: ${key}`);
878
+ clearInterval(managed.logInterval);
879
+ killTree(managed.process);
880
+ } else {
881
+ if (payload.server_instance_id) {
882
+ await supabase.from("server_instances").update({
883
+ status: "stopped",
884
+ tunnel_url: null,
885
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
886
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
887
+ }).eq("id", payload.server_instance_id);
888
+ }
889
+ }
890
+ await supabase.from("commands").update({
891
+ status: "done",
892
+ result: { stopped: true },
893
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
894
+ }).eq("id", command.id);
895
+ }
896
+ async function handleServerStatus(command, supabase) {
897
+ const config = loadConfig();
898
+ const servers = [];
899
+ for (const [key, managed] of runningServers) {
900
+ servers.push({
901
+ key,
902
+ running: managed.process.exitCode === null,
903
+ pid: managed.process.pid,
904
+ detectedPort: managed.detectedPort,
905
+ tunnelUrl: managed.tunnelUrl,
906
+ logCount: managed.logs.length
907
+ });
908
+ }
909
+ await supabase.from("commands").update({
910
+ status: "done",
911
+ result: { servers, deviceId: config.deviceId },
912
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
913
+ }).eq("id", command.id);
914
+ }
915
+ async function cleanupServers(supabase, deviceId) {
916
+ for (const [key, managed] of runningServers) {
917
+ console.log(chalk3.dim(` Stopping server: ${key}`));
918
+ clearInterval(managed.logInterval);
919
+ if (managed.process.exitCode === null) {
920
+ killTree(managed.process);
921
+ }
922
+ }
923
+ runningServers.clear();
924
+ disconnectAllTunnels();
925
+ await supabase.from("server_instances").update({
926
+ status: "stopped",
927
+ tunnel_url: null,
928
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
929
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
930
+ }).eq("device_id", deviceId).in("status", ["starting", "running"]);
931
+ }
932
+ async function cleanupStaleServers(supabase, deviceId) {
933
+ const { data, error } = await supabase.from("server_instances").update({
934
+ status: "stopped",
935
+ tunnel_url: null,
936
+ error_message: "Agent restarted",
937
+ stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
938
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
939
+ }).eq("device_id", deviceId).in("status", ["starting", "running"]).select("id");
940
+ if (data && data.length > 0) {
941
+ console.log(chalk3.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
942
+ }
943
+ }
944
+
383
945
  // src/cli.ts
384
946
  var __dirname = dirname(fileURLToPath(import.meta.url));
385
947
  var pkg = JSON.parse(readFileSync(join2(__dirname, "..", "package.json"), "utf-8"));
@@ -389,30 +951,30 @@ function detectClaudeCode() {
389
951
  let installed = false;
390
952
  try {
391
953
  if (process.platform === "win32") {
392
- execSync2("where claude", { stdio: "ignore" });
954
+ execSync3("where claude", { stdio: "ignore" });
393
955
  } else {
394
- execSync2("which claude", { stdio: "ignore" });
956
+ execSync3("which claude", { stdio: "ignore" });
395
957
  }
396
958
  installed = true;
397
959
  } catch {
398
960
  installed = false;
399
961
  }
400
- const authenticated = existsSync2(join2(homedir(), ".claude"));
962
+ const authenticated = existsSync3(join2(homedir(), ".claude"));
401
963
  return { installed, authenticated };
402
964
  }
403
965
  async function sleep(ms) {
404
- return new Promise((resolve2) => setTimeout(resolve2, ms));
966
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
405
967
  }
406
968
  program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
407
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
969
+ console.log(chalk4.bold("\n tenux"), chalk4.dim("desktop agent\n"));
408
970
  const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
409
- console.log(chalk2.dim(" App:"), appUrl);
971
+ console.log(chalk4.dim(" App:"), appUrl);
410
972
  const deviceName = hostname();
411
973
  const platform = process.platform;
412
- console.log(chalk2.dim(" Device:"), deviceName);
413
- console.log(chalk2.dim(" Platform:"), platform);
974
+ console.log(chalk4.dim(" Device:"), deviceName);
975
+ console.log(chalk4.dim(" Platform:"), platform);
414
976
  console.log();
415
- console.log(chalk2.dim(" Requesting device code..."));
977
+ console.log(chalk4.dim(" Requesting device code..."));
416
978
  let code;
417
979
  let expiresAt;
418
980
  let supabaseUrl;
@@ -429,7 +991,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
429
991
  });
430
992
  if (!res.ok) {
431
993
  const text = await res.text();
432
- console.log(chalk2.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
994
+ console.log(chalk4.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
433
995
  process.exit(1);
434
996
  }
435
997
  const data = await res.json();
@@ -439,26 +1001,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
439
1001
  supabaseAnonKey = data.supabase_anon_key;
440
1002
  } catch (err) {
441
1003
  console.log(
442
- chalk2.red(" \u2717"),
1004
+ chalk4.red(" \u2717"),
443
1005
  `Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
444
1006
  );
445
1007
  process.exit(1);
446
1008
  }
447
1009
  console.log();
448
- console.log(chalk2.bold.cyan(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
449
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
450
- console.log(chalk2.bold.cyan(` \u2502 Code: ${chalk2.white.bold(code)} \u2502`));
451
- console.log(chalk2.bold.cyan(` \u2502 \u2502`));
452
- console.log(chalk2.bold.cyan(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
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`));
453
1015
  console.log();
454
1016
  const linkUrl = `${appUrl}/link?code=${code}`;
455
- console.log(chalk2.dim(" Opening browser to:"), chalk2.underline(linkUrl));
1017
+ console.log(chalk4.dim(" Opening browser to:"), chalk4.underline(linkUrl));
456
1018
  try {
457
1019
  const open = (await import("open")).default;
458
1020
  await open(linkUrl);
459
1021
  } catch {
460
- console.log(chalk2.yellow(" !"), "Could not open browser automatically.");
461
- console.log(chalk2.dim(" Open this URL manually:"), chalk2.underline(linkUrl));
1022
+ console.log(chalk4.yellow(" !"), "Could not open browser automatically.");
1023
+ console.log(chalk4.dim(" Open this URL manually:"), chalk4.underline(linkUrl));
462
1024
  }
463
1025
  console.log();
464
1026
  const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -470,11 +1032,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
470
1032
  const expiresTime = new Date(expiresAt).getTime();
471
1033
  while (!approved) {
472
1034
  if (Date.now() > expiresTime) {
473
- console.log(chalk2.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.");
474
1036
  process.exit(1);
475
1037
  }
476
1038
  process.stdout.write(
477
- `\r ${chalk2.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
1039
+ `\r ${chalk4.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
478
1040
  );
479
1041
  dotIndex++;
480
1042
  await sleep(2e3);
@@ -492,16 +1054,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
492
1054
  userId = data.user_id || "";
493
1055
  approved = true;
494
1056
  } else if (data.status === "expired") {
495
- console.log(chalk2.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.");
496
1058
  process.exit(1);
497
1059
  }
498
1060
  } catch {
499
1061
  }
500
1062
  }
501
1063
  process.stdout.write("\r" + " ".repeat(60) + "\r");
502
- console.log(chalk2.green(" \u2713"), "Device approved!");
1064
+ console.log(chalk4.green(" \u2713"), "Device approved!");
503
1065
  console.log();
504
- console.log(chalk2.dim(" Exchanging token for session..."));
1066
+ console.log(chalk4.dim(" Exchanging token for session..."));
505
1067
  let accessToken = "";
506
1068
  let refreshToken = "";
507
1069
  try {
@@ -513,11 +1075,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
513
1075
  type: "magiclink"
514
1076
  });
515
1077
  if (error) {
516
- console.log(chalk2.red(" \u2717"), `Auth failed: ${error.message}`);
1078
+ console.log(chalk4.red(" \u2717"), `Auth failed: ${error.message}`);
517
1079
  process.exit(1);
518
1080
  }
519
1081
  if (!session.session) {
520
- console.log(chalk2.red(" \u2717"), "No session returned from auth exchange.");
1082
+ console.log(chalk4.red(" \u2717"), "No session returned from auth exchange.");
521
1083
  process.exit(1);
522
1084
  }
523
1085
  accessToken = session.session.access_token;
@@ -525,12 +1087,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
525
1087
  userId = session.session.user?.id || userId;
526
1088
  } catch (err) {
527
1089
  console.log(
528
- chalk2.red(" \u2717"),
1090
+ chalk4.red(" \u2717"),
529
1091
  `Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
530
1092
  );
531
1093
  process.exit(1);
532
1094
  }
533
- console.log(chalk2.green(" \u2713"), "Session established.");
1095
+ console.log(chalk4.green(" \u2713"), "Session established.");
534
1096
  const config = {
535
1097
  appUrl,
536
1098
  supabaseUrl,
@@ -543,66 +1105,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
543
1105
  projectsDir: join2(homedir(), ".tenux", "projects")
544
1106
  };
545
1107
  saveConfig(config);
546
- console.log(chalk2.green(" \u2713"), "Config saved to", chalk2.dim(getConfigPath()));
1108
+ console.log(chalk4.green(" \u2713"), "Config saved to", chalk4.dim(getConfigPath()));
547
1109
  console.log();
548
1110
  const claude = detectClaudeCode();
549
1111
  if (claude.installed) {
550
- console.log(chalk2.green(" \u2713"), "Claude Code detected");
1112
+ console.log(chalk4.green(" \u2713"), "Claude Code detected");
551
1113
  if (claude.authenticated) {
552
- console.log(chalk2.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
1114
+ console.log(chalk4.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
553
1115
  } else {
554
1116
  console.log(
555
- chalk2.yellow(" !"),
1117
+ chalk4.yellow(" !"),
556
1118
  "Claude Code found but ~/.claude not detected.",
557
- chalk2.dim("Run `claude login` to authenticate.")
1119
+ chalk4.dim("Run `claude login` to authenticate.")
558
1120
  );
559
1121
  }
560
1122
  } else {
561
- console.log(chalk2.yellow(" !"), "Claude Code not found.");
1123
+ console.log(chalk4.yellow(" !"), "Claude Code not found.");
562
1124
  console.log(
563
- chalk2.dim(" Install it:"),
564
- chalk2.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")
565
1127
  );
566
1128
  console.log(
567
- chalk2.dim(" Then run:"),
568
- chalk2.cyan("claude login")
1129
+ chalk4.dim(" Then run:"),
1130
+ chalk4.cyan("claude login")
569
1131
  );
570
1132
  }
571
1133
  console.log();
572
- console.log(chalk2.dim(" Device:"), deviceName);
573
- console.log(chalk2.dim(" Device ID:"), deviceId);
574
- console.log(chalk2.dim(" User ID:"), userId);
575
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
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);
576
1138
  console.log();
577
1139
  console.log(
578
- chalk2.dim(" Next: start the agent:"),
579
- chalk2.cyan("tenux start")
1140
+ chalk4.dim(" Next: start the agent:"),
1141
+ chalk4.cyan("tenux start")
580
1142
  );
581
1143
  console.log();
582
1144
  });
583
1145
  program.command("start").description("Start the agent and listen for commands").action(async () => {
584
1146
  if (!configExists()) {
585
- console.log(chalk2.red("\u2717"), "Not logged in. Run `tenux login` first.");
1147
+ console.log(chalk4.red("\u2717"), "Not logged in. Run `tenux login` first.");
586
1148
  process.exit(1);
587
1149
  }
588
1150
  const claude = detectClaudeCode();
589
1151
  if (!claude.installed) {
590
- console.log(chalk2.red("\u2717"), "Claude Code not found.");
591
- console.log(chalk2.dim(" Install it:"), chalk2.cyan("npm i -g @anthropic-ai/claude-code"));
592
- console.log(chalk2.dim(" Then run:"), chalk2.cyan("claude login"));
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"));
593
1155
  process.exit(1);
594
1156
  }
595
1157
  const config = loadConfig();
596
- console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
597
- console.log(chalk2.dim(" Device:"), config.deviceName);
598
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
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);
599
1161
  console.log();
600
1162
  const supabase = getSupabase();
601
1163
  try {
602
1164
  await initSupabaseSession();
603
1165
  } catch (err) {
604
- console.log(chalk2.red("\u2717"), `Session expired: ${err.message}`);
605
- console.log(chalk2.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."));
606
1168
  process.exit(1);
607
1169
  }
608
1170
  await supabase.from("devices").update({
@@ -610,56 +1172,58 @@ program.command("start").description("Start the agent and listen for commands").
610
1172
  is_online: true,
611
1173
  last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
612
1174
  }).eq("id", config.deviceId);
1175
+ await cleanupStaleServers(supabase, config.deviceId);
613
1176
  const heartbeat = setInterval(async () => {
614
1177
  const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
615
1178
  if (error) {
616
- console.log(chalk2.red("\u2717"), chalk2.dim(`Heartbeat failed: ${error.message}`));
1179
+ console.log(chalk4.red("\u2717"), chalk4.dim(`Heartbeat failed: ${error.message}`));
617
1180
  }
618
1181
  }, 3e4);
619
1182
  const relay = new Relay(supabase);
620
1183
  const shutdown = async () => {
621
- console.log(chalk2.dim("\n Shutting down..."));
1184
+ console.log(chalk4.dim("\n Shutting down..."));
622
1185
  clearInterval(heartbeat);
1186
+ await cleanupServers(supabase, config.deviceId);
623
1187
  await relay.stop();
624
1188
  await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
625
1189
  process.exit(0);
626
1190
  };
627
- relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("project.delete", handleProjectDelete).on("agent.shutdown", async () => {
628
- console.log(chalk2.yellow("\n \u26A1 Remote shutdown requested"));
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 () => {
1192
+ console.log(chalk4.yellow("\n \u26A1 Remote shutdown requested"));
629
1193
  await shutdown();
630
1194
  });
631
1195
  await relay.start();
632
- console.log(chalk2.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
1196
+ console.log(chalk4.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
633
1197
  process.on("SIGINT", shutdown);
634
1198
  process.on("SIGTERM", shutdown);
635
1199
  });
636
1200
  program.command("status").description("Show agent configuration and status").action(() => {
637
1201
  if (!configExists()) {
638
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
1202
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
639
1203
  process.exit(1);
640
1204
  }
641
1205
  const config = loadConfig();
642
1206
  const claude = detectClaudeCode();
643
- console.log(chalk2.bold("\n tenux"), chalk2.dim("agent status\n"));
644
- console.log(chalk2.dim(" Config:"), getConfigPath());
645
- console.log(chalk2.dim(" App URL:"), config.appUrl);
646
- console.log(chalk2.dim(" Device:"), config.deviceName);
647
- console.log(chalk2.dim(" Device ID:"), config.deviceId);
648
- console.log(chalk2.dim(" User ID:"), config.userId);
649
- console.log(chalk2.dim(" Supabase:"), config.supabaseUrl);
650
- console.log(chalk2.dim(" Projects:"), config.projectsDir);
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);
651
1215
  console.log(
652
- chalk2.dim(" Auth:"),
653
- config.accessToken ? chalk2.green("authenticated") : chalk2.yellow("not authenticated")
1216
+ chalk4.dim(" Auth:"),
1217
+ config.accessToken ? chalk4.green("authenticated") : chalk4.yellow("not authenticated")
654
1218
  );
655
1219
  console.log(
656
- chalk2.dim(" Claude Code:"),
657
- claude.installed ? chalk2.green("installed") : chalk2.yellow("not installed")
1220
+ chalk4.dim(" Claude Code:"),
1221
+ claude.installed ? chalk4.green("installed") : chalk4.yellow("not installed")
658
1222
  );
659
1223
  if (claude.installed) {
660
1224
  console.log(
661
- chalk2.dim(" Claude Auth:"),
662
- claude.authenticated ? chalk2.green("yes (~/.claude exists)") : chalk2.yellow("not authenticated")
1225
+ chalk4.dim(" Claude Auth:"),
1226
+ claude.authenticated ? chalk4.green("yes (~/.claude exists)") : chalk4.yellow("not authenticated")
663
1227
  );
664
1228
  }
665
1229
  console.log();
@@ -667,7 +1231,7 @@ program.command("status").description("Show agent configuration and status").act
667
1231
  var configCmd = program.command("config").description("Manage agent configuration");
668
1232
  configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
669
1233
  if (!configExists()) {
670
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
1234
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
671
1235
  process.exit(1);
672
1236
  }
673
1237
  const keyMap = {
@@ -678,17 +1242,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
678
1242
  const configKey = keyMap[key];
679
1243
  if (!configKey) {
680
1244
  console.log(
681
- chalk2.red("\u2717"),
1245
+ chalk4.red("\u2717"),
682
1246
  `Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
683
1247
  );
684
1248
  process.exit(1);
685
1249
  }
686
1250
  updateConfig({ [configKey]: value });
687
- console.log(chalk2.green("\u2713"), `Set ${key}`);
1251
+ console.log(chalk4.green("\u2713"), `Set ${key}`);
688
1252
  });
689
1253
  configCmd.command("get <key>").description("Get a config value").action((key) => {
690
1254
  if (!configExists()) {
691
- console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
1255
+ console.log(chalk4.red("\u2717"), "Not configured. Run `tenux login` first.");
692
1256
  process.exit(1);
693
1257
  }
694
1258
  const config = loadConfig();
@@ -703,12 +1267,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
703
1267
  const configKey = keyMap[key];
704
1268
  if (!configKey) {
705
1269
  console.log(
706
- chalk2.red("\u2717"),
1270
+ chalk4.red("\u2717"),
707
1271
  `Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
708
1272
  );
709
1273
  process.exit(1);
710
1274
  }
711
1275
  const val = config[configKey];
712
- console.log(val ?? chalk2.dim("(not set)"));
1276
+ console.log(val ?? chalk4.dim("(not set)"));
713
1277
  });
714
1278
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenux/cli",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Tenux — mobile-first IDE for 10x engineering",
5
5
  "author": "Antelogic LLC",
6
6
  "license": "MIT",