@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.
- package/dist/cli.js +648 -84
- 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
|
|
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
|
|
21
|
-
import { execSync as
|
|
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((
|
|
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)
|
|
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
|
-
|
|
954
|
+
execSync3("where claude", { stdio: "ignore" });
|
|
393
955
|
} else {
|
|
394
|
-
|
|
956
|
+
execSync3("which claude", { stdio: "ignore" });
|
|
395
957
|
}
|
|
396
958
|
installed = true;
|
|
397
959
|
} catch {
|
|
398
960
|
installed = false;
|
|
399
961
|
}
|
|
400
|
-
const authenticated =
|
|
962
|
+
const authenticated = existsSync3(join2(homedir(), ".claude"));
|
|
401
963
|
return { installed, authenticated };
|
|
402
964
|
}
|
|
403
965
|
async function sleep(ms) {
|
|
404
|
-
return new Promise((
|
|
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(
|
|
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(
|
|
971
|
+
console.log(chalk4.dim(" App:"), appUrl);
|
|
410
972
|
const deviceName = hostname();
|
|
411
973
|
const platform = process.platform;
|
|
412
|
-
console.log(
|
|
413
|
-
console.log(
|
|
974
|
+
console.log(chalk4.dim(" Device:"), deviceName);
|
|
975
|
+
console.log(chalk4.dim(" Platform:"), platform);
|
|
414
976
|
console.log();
|
|
415
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
449
|
-
console.log(
|
|
450
|
-
console.log(
|
|
451
|
-
console.log(
|
|
452
|
-
console.log(
|
|
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(
|
|
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(
|
|
461
|
-
console.log(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
1064
|
+
console.log(chalk4.green(" \u2713"), "Device approved!");
|
|
503
1065
|
console.log();
|
|
504
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1112
|
+
console.log(chalk4.green(" \u2713"), "Claude Code detected");
|
|
551
1113
|
if (claude.authenticated) {
|
|
552
|
-
console.log(
|
|
1114
|
+
console.log(chalk4.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
|
|
553
1115
|
} else {
|
|
554
1116
|
console.log(
|
|
555
|
-
|
|
1117
|
+
chalk4.yellow(" !"),
|
|
556
1118
|
"Claude Code found but ~/.claude not detected.",
|
|
557
|
-
|
|
1119
|
+
chalk4.dim("Run `claude login` to authenticate.")
|
|
558
1120
|
);
|
|
559
1121
|
}
|
|
560
1122
|
} else {
|
|
561
|
-
console.log(
|
|
1123
|
+
console.log(chalk4.yellow(" !"), "Claude Code not found.");
|
|
562
1124
|
console.log(
|
|
563
|
-
|
|
564
|
-
|
|
1125
|
+
chalk4.dim(" Install it:"),
|
|
1126
|
+
chalk4.underline("https://docs.anthropic.com/en/docs/claude-code")
|
|
565
1127
|
);
|
|
566
1128
|
console.log(
|
|
567
|
-
|
|
568
|
-
|
|
1129
|
+
chalk4.dim(" Then run:"),
|
|
1130
|
+
chalk4.cyan("claude login")
|
|
569
1131
|
);
|
|
570
1132
|
}
|
|
571
1133
|
console.log();
|
|
572
|
-
console.log(
|
|
573
|
-
console.log(
|
|
574
|
-
console.log(
|
|
575
|
-
console.log(
|
|
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
|
-
|
|
579
|
-
|
|
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(
|
|
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(
|
|
591
|
-
console.log(
|
|
592
|
-
console.log(
|
|
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(
|
|
597
|
-
console.log(
|
|
598
|
-
console.log(
|
|
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(
|
|
605
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
644
|
-
console.log(
|
|
645
|
-
console.log(
|
|
646
|
-
console.log(
|
|
647
|
-
console.log(
|
|
648
|
-
console.log(
|
|
649
|
-
console.log(
|
|
650
|
-
console.log(
|
|
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
|
-
|
|
653
|
-
config.accessToken ?
|
|
1216
|
+
chalk4.dim(" Auth:"),
|
|
1217
|
+
config.accessToken ? chalk4.green("authenticated") : chalk4.yellow("not authenticated")
|
|
654
1218
|
);
|
|
655
1219
|
console.log(
|
|
656
|
-
|
|
657
|
-
claude.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
|
-
|
|
662
|
-
claude.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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
1276
|
+
console.log(val ?? chalk4.dim("(not set)"));
|
|
713
1277
|
});
|
|
714
1278
|
program.parse();
|