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