@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.
- package/dist/cli.js +420 -132
- 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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
482
|
-
console.log(
|
|
483
|
-
console.log(
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
984
|
+
console.log(chalk4.dim(" App:"), appUrl);
|
|
697
985
|
const deviceName = hostname();
|
|
698
986
|
const platform = process.platform;
|
|
699
|
-
console.log(
|
|
700
|
-
console.log(
|
|
987
|
+
console.log(chalk4.dim(" Device:"), deviceName);
|
|
988
|
+
console.log(chalk4.dim(" Platform:"), platform);
|
|
701
989
|
console.log();
|
|
702
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
736
|
-
console.log(
|
|
737
|
-
console.log(
|
|
738
|
-
console.log(
|
|
739
|
-
console.log(
|
|
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(
|
|
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(
|
|
748
|
-
console.log(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
1077
|
+
console.log(chalk4.green(" \u2713"), "Device approved!");
|
|
790
1078
|
console.log();
|
|
791
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1125
|
+
console.log(chalk4.green(" \u2713"), "Claude Code detected");
|
|
838
1126
|
if (claude.authenticated) {
|
|
839
|
-
console.log(
|
|
1127
|
+
console.log(chalk4.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
|
|
840
1128
|
} else {
|
|
841
1129
|
console.log(
|
|
842
|
-
|
|
1130
|
+
chalk4.yellow(" !"),
|
|
843
1131
|
"Claude Code found but ~/.claude not detected.",
|
|
844
|
-
|
|
1132
|
+
chalk4.dim("Run `claude login` to authenticate.")
|
|
845
1133
|
);
|
|
846
1134
|
}
|
|
847
1135
|
} else {
|
|
848
|
-
console.log(
|
|
1136
|
+
console.log(chalk4.yellow(" !"), "Claude Code not found.");
|
|
849
1137
|
console.log(
|
|
850
|
-
|
|
851
|
-
|
|
1138
|
+
chalk4.dim(" Install it:"),
|
|
1139
|
+
chalk4.underline("https://docs.anthropic.com/en/docs/claude-code")
|
|
852
1140
|
);
|
|
853
1141
|
console.log(
|
|
854
|
-
|
|
855
|
-
|
|
1142
|
+
chalk4.dim(" Then run:"),
|
|
1143
|
+
chalk4.cyan("claude login")
|
|
856
1144
|
);
|
|
857
1145
|
}
|
|
858
1146
|
console.log();
|
|
859
|
-
console.log(
|
|
860
|
-
console.log(
|
|
861
|
-
console.log(
|
|
862
|
-
console.log(
|
|
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
|
-
|
|
866
|
-
|
|
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(
|
|
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(
|
|
878
|
-
console.log(
|
|
879
|
-
console.log(
|
|
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(
|
|
884
|
-
console.log(
|
|
885
|
-
console.log(
|
|
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(
|
|
892
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
1205
|
+
console.log(chalk4.yellow("\n \u26A1 Remote shutdown requested"));
|
|
918
1206
|
await shutdown();
|
|
919
1207
|
});
|
|
920
1208
|
await relay.start();
|
|
921
|
-
console.log(
|
|
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(
|
|
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(
|
|
933
|
-
console.log(
|
|
934
|
-
console.log(
|
|
935
|
-
console.log(
|
|
936
|
-
console.log(
|
|
937
|
-
console.log(
|
|
938
|
-
console.log(
|
|
939
|
-
console.log(
|
|
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
|
-
|
|
942
|
-
config.accessToken ?
|
|
1229
|
+
chalk4.dim(" Auth:"),
|
|
1230
|
+
config.accessToken ? chalk4.green("authenticated") : chalk4.yellow("not authenticated")
|
|
943
1231
|
);
|
|
944
1232
|
console.log(
|
|
945
|
-
|
|
946
|
-
claude.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
|
-
|
|
951
|
-
claude.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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
1289
|
+
console.log(val ?? chalk4.dim("(not set)"));
|
|
1002
1290
|
});
|
|
1003
1291
|
program.parse();
|