@tenux/cli 0.0.21 → 0.0.22
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 +373 -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 chalk3 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,293 @@ async function handleProjectDelete(command, supabase) {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
// src/handlers/server.ts
|
|
384
|
+
import { spawn as spawn2, exec, execSync as execSync2 } from "child_process";
|
|
385
|
+
import { resolve as resolve2 } from "path";
|
|
386
|
+
import { existsSync as existsSync2 } from "fs";
|
|
387
|
+
import chalk2 from "chalk";
|
|
388
|
+
var MAX_LOG_LINES = 500;
|
|
389
|
+
var LOG_PUSH_INTERVAL = 2e3;
|
|
390
|
+
var runningServers = /* @__PURE__ */ new Map();
|
|
391
|
+
function makeKey(projectName, commandName) {
|
|
392
|
+
return `${projectName}::${commandName}`;
|
|
393
|
+
}
|
|
394
|
+
function killTree(child) {
|
|
395
|
+
if (child.exitCode !== null) return;
|
|
396
|
+
const pid = child.pid;
|
|
397
|
+
if (!pid) {
|
|
398
|
+
child.kill("SIGTERM");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
if (process.platform === "win32") {
|
|
403
|
+
execSync2(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
|
404
|
+
} else {
|
|
405
|
+
process.kill(-pid, "SIGTERM");
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
child.kill("SIGTERM");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
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
|
+
async function handleServerStart(command, supabase) {
|
|
445
|
+
const payload = command.payload;
|
|
446
|
+
const config = loadConfig();
|
|
447
|
+
const fullPath = payload.project_path.startsWith("/") || payload.project_path.match(/^[A-Z]:\\/i) ? payload.project_path : resolve2(config.projectsDir, payload.project_path);
|
|
448
|
+
if (!existsSync2(fullPath)) {
|
|
449
|
+
throw new Error(`Project path not found: ${fullPath}`);
|
|
450
|
+
}
|
|
451
|
+
const key = makeKey(payload.project_name, payload.command_name);
|
|
452
|
+
const existing = runningServers.get(key);
|
|
453
|
+
if (existing) {
|
|
454
|
+
clearInterval(existing.logInterval);
|
|
455
|
+
killTree(existing.process);
|
|
456
|
+
runningServers.delete(key);
|
|
457
|
+
}
|
|
458
|
+
const { data: instance, error: upsertErr } = await supabase.from("server_instances").upsert(
|
|
459
|
+
{
|
|
460
|
+
user_id: command.user_id,
|
|
461
|
+
device_id: command.device_id,
|
|
462
|
+
project_id: payload.project_id,
|
|
463
|
+
command_name: payload.command_name,
|
|
464
|
+
command: payload.command,
|
|
465
|
+
status: "starting",
|
|
466
|
+
port: payload.port ?? null,
|
|
467
|
+
pid: null,
|
|
468
|
+
detected_port: null,
|
|
469
|
+
tailscale_url: null,
|
|
470
|
+
logs: [],
|
|
471
|
+
error_message: null,
|
|
472
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
473
|
+
stopped_at: null,
|
|
474
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
475
|
+
},
|
|
476
|
+
{ onConflict: "device_id,project_id,command_name" }
|
|
477
|
+
).select().single();
|
|
478
|
+
if (upsertErr || !instance) {
|
|
479
|
+
throw new Error(`Failed to create server instance: ${upsertErr?.message}`);
|
|
480
|
+
}
|
|
481
|
+
console.log(chalk2.blue("\u2192"), `Starting server: ${payload.project_name}::${payload.command_name}`);
|
|
482
|
+
console.log(chalk2.dim(` cmd: ${payload.command}`));
|
|
483
|
+
console.log(chalk2.dim(` cwd: ${fullPath}`));
|
|
484
|
+
const env = { ...process.env, FORCE_COLOR: "0" };
|
|
485
|
+
delete env.PORT;
|
|
486
|
+
delete env.NODE_OPTIONS;
|
|
487
|
+
const parts = payload.command.split(/\s+/).filter(Boolean);
|
|
488
|
+
const bin = parts[0];
|
|
489
|
+
const cmdArgs = parts.slice(1);
|
|
490
|
+
const isWindows = process.platform === "win32";
|
|
491
|
+
const child = spawn2(bin, cmdArgs, {
|
|
492
|
+
cwd: fullPath,
|
|
493
|
+
shell: isWindows,
|
|
494
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
495
|
+
env
|
|
496
|
+
});
|
|
497
|
+
const logs = [];
|
|
498
|
+
let detectedPort = false;
|
|
499
|
+
let logDirty = false;
|
|
500
|
+
const pushLog = (text) => {
|
|
501
|
+
const lines = text.split("\n");
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
if (line.trim()) {
|
|
504
|
+
logs.push(line);
|
|
505
|
+
if (logs.length > MAX_LOG_LINES) logs.shift();
|
|
506
|
+
logDirty = true;
|
|
507
|
+
if (!detectedPort) {
|
|
508
|
+
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);
|
|
509
|
+
if (portMatch) {
|
|
510
|
+
const actualPort = parseInt(portMatch[1], 10);
|
|
511
|
+
detectedPort = true;
|
|
512
|
+
managed.detectedPort = actualPort;
|
|
513
|
+
setupTailscaleServe(actualPort);
|
|
514
|
+
const hostname2 = getTailscaleHostname();
|
|
515
|
+
const tsUrl = hostname2 ? `https://${hostname2}:${actualPort}` : null;
|
|
516
|
+
supabase.from("server_instances").update({
|
|
517
|
+
status: "running",
|
|
518
|
+
detected_port: actualPort,
|
|
519
|
+
tailscale_url: tsUrl,
|
|
520
|
+
pid: child.pid ?? null,
|
|
521
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
522
|
+
}).eq("id", instance.id).then(() => {
|
|
523
|
+
console.log(chalk2.green("\u2713"), `Server running on :${actualPort}`);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
child.stdout.on("data", (chunk) => pushLog(chunk.toString()));
|
|
531
|
+
child.stderr.on("data", (chunk) => pushLog(chunk.toString()));
|
|
532
|
+
const logInterval = setInterval(async () => {
|
|
533
|
+
if (!logDirty) return;
|
|
534
|
+
logDirty = false;
|
|
535
|
+
await supabase.from("server_instances").update({
|
|
536
|
+
logs: logs.slice(-100),
|
|
537
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
538
|
+
}).eq("id", instance.id);
|
|
539
|
+
}, LOG_PUSH_INTERVAL);
|
|
540
|
+
const managed = {
|
|
541
|
+
instanceId: instance.id,
|
|
542
|
+
projectName: payload.project_name,
|
|
543
|
+
commandName: payload.command_name,
|
|
544
|
+
command: payload.command,
|
|
545
|
+
process: child,
|
|
546
|
+
logs,
|
|
547
|
+
logInterval
|
|
548
|
+
};
|
|
549
|
+
child.on("close", async (code) => {
|
|
550
|
+
clearInterval(logInterval);
|
|
551
|
+
logs.push(`[Process exited with code ${code}]`);
|
|
552
|
+
if (managed.detectedPort) {
|
|
553
|
+
teardownTailscaleServe(managed.detectedPort);
|
|
554
|
+
}
|
|
555
|
+
runningServers.delete(key);
|
|
556
|
+
await supabase.from("server_instances").update({
|
|
557
|
+
status: code === 0 ? "stopped" : "error",
|
|
558
|
+
error_message: code !== 0 ? `Exited with code ${code}` : null,
|
|
559
|
+
logs: logs.slice(-100),
|
|
560
|
+
stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
561
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
562
|
+
}).eq("id", instance.id);
|
|
563
|
+
console.log(
|
|
564
|
+
code === 0 ? chalk2.dim(" Server stopped") : chalk2.yellow("!"),
|
|
565
|
+
`${payload.project_name}::${payload.command_name} exited (code ${code})`
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
runningServers.set(key, managed);
|
|
569
|
+
await supabase.from("server_instances").update({
|
|
570
|
+
pid: child.pid ?? null,
|
|
571
|
+
status: "starting",
|
|
572
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
573
|
+
}).eq("id", instance.id);
|
|
574
|
+
setTimeout(async () => {
|
|
575
|
+
if (!detectedPort) {
|
|
576
|
+
const configuredPort = payload.port;
|
|
577
|
+
if (configuredPort) {
|
|
578
|
+
managed.detectedPort = configuredPort;
|
|
579
|
+
setupTailscaleServe(configuredPort);
|
|
580
|
+
const hostname2 = getTailscaleHostname();
|
|
581
|
+
const tsUrl = hostname2 ? `https://${hostname2}:${configuredPort}` : null;
|
|
582
|
+
await supabase.from("server_instances").update({
|
|
583
|
+
status: "running",
|
|
584
|
+
detected_port: configuredPort,
|
|
585
|
+
tailscale_url: tsUrl,
|
|
586
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
587
|
+
}).eq("id", instance.id);
|
|
588
|
+
} else {
|
|
589
|
+
await supabase.from("server_instances").update({ status: "running", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", instance.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}, 8e3);
|
|
593
|
+
await supabase.from("commands").update({
|
|
594
|
+
status: "done",
|
|
595
|
+
result: { server_instance_id: instance.id },
|
|
596
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
597
|
+
}).eq("id", command.id);
|
|
598
|
+
}
|
|
599
|
+
async function handleServerStop(command, supabase) {
|
|
600
|
+
const payload = command.payload;
|
|
601
|
+
const key = makeKey(payload.project_name, payload.command_name);
|
|
602
|
+
const managed = runningServers.get(key);
|
|
603
|
+
if (managed) {
|
|
604
|
+
console.log(chalk2.blue("\u2192"), `Stopping server: ${key}`);
|
|
605
|
+
clearInterval(managed.logInterval);
|
|
606
|
+
killTree(managed.process);
|
|
607
|
+
} else {
|
|
608
|
+
if (payload.server_instance_id) {
|
|
609
|
+
await supabase.from("server_instances").update({
|
|
610
|
+
status: "stopped",
|
|
611
|
+
stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
612
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
613
|
+
}).eq("id", payload.server_instance_id);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
await supabase.from("commands").update({
|
|
617
|
+
status: "done",
|
|
618
|
+
result: { stopped: true },
|
|
619
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
620
|
+
}).eq("id", command.id);
|
|
621
|
+
}
|
|
622
|
+
async function handleServerStatus(command, supabase) {
|
|
623
|
+
const config = loadConfig();
|
|
624
|
+
const servers = [];
|
|
625
|
+
for (const [key, managed] of runningServers) {
|
|
626
|
+
servers.push({
|
|
627
|
+
key,
|
|
628
|
+
running: managed.process.exitCode === null,
|
|
629
|
+
pid: managed.process.pid,
|
|
630
|
+
detectedPort: managed.detectedPort,
|
|
631
|
+
logCount: managed.logs.length
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
await supabase.from("commands").update({
|
|
635
|
+
status: "done",
|
|
636
|
+
result: { servers, deviceId: config.deviceId },
|
|
637
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
638
|
+
}).eq("id", command.id);
|
|
639
|
+
}
|
|
640
|
+
async function cleanupServers(supabase, deviceId) {
|
|
641
|
+
for (const [key, managed] of runningServers) {
|
|
642
|
+
console.log(chalk2.dim(` Stopping server: ${key}`));
|
|
643
|
+
clearInterval(managed.logInterval);
|
|
644
|
+
if (managed.process.exitCode === null) {
|
|
645
|
+
killTree(managed.process);
|
|
646
|
+
}
|
|
647
|
+
if (managed.detectedPort) {
|
|
648
|
+
teardownTailscaleServe(managed.detectedPort);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
runningServers.clear();
|
|
652
|
+
await supabase.from("server_instances").update({
|
|
653
|
+
status: "stopped",
|
|
654
|
+
stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
656
|
+
}).eq("device_id", deviceId).in("status", ["starting", "running"]);
|
|
657
|
+
}
|
|
658
|
+
async function cleanupStaleServers(supabase, deviceId) {
|
|
659
|
+
const { data, error } = await supabase.from("server_instances").update({
|
|
660
|
+
status: "stopped",
|
|
661
|
+
error_message: "Agent restarted",
|
|
662
|
+
stopped_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
663
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
664
|
+
}).eq("device_id", deviceId).in("status", ["starting", "running"]).select("id");
|
|
665
|
+
if (data && data.length > 0) {
|
|
666
|
+
console.log(chalk2.yellow("!"), `Cleaned up ${data.length} stale server instance(s)`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
383
670
|
// src/cli.ts
|
|
384
671
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
385
672
|
var pkg = JSON.parse(readFileSync(join2(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -389,30 +676,30 @@ function detectClaudeCode() {
|
|
|
389
676
|
let installed = false;
|
|
390
677
|
try {
|
|
391
678
|
if (process.platform === "win32") {
|
|
392
|
-
|
|
679
|
+
execSync3("where claude", { stdio: "ignore" });
|
|
393
680
|
} else {
|
|
394
|
-
|
|
681
|
+
execSync3("which claude", { stdio: "ignore" });
|
|
395
682
|
}
|
|
396
683
|
installed = true;
|
|
397
684
|
} catch {
|
|
398
685
|
installed = false;
|
|
399
686
|
}
|
|
400
|
-
const authenticated =
|
|
687
|
+
const authenticated = existsSync3(join2(homedir(), ".claude"));
|
|
401
688
|
return { installed, authenticated };
|
|
402
689
|
}
|
|
403
690
|
async function sleep(ms) {
|
|
404
|
-
return new Promise((
|
|
691
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
405
692
|
}
|
|
406
693
|
program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
|
|
407
|
-
console.log(
|
|
694
|
+
console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
|
|
408
695
|
const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
|
|
409
|
-
console.log(
|
|
696
|
+
console.log(chalk3.dim(" App:"), appUrl);
|
|
410
697
|
const deviceName = hostname();
|
|
411
698
|
const platform = process.platform;
|
|
412
|
-
console.log(
|
|
413
|
-
console.log(
|
|
699
|
+
console.log(chalk3.dim(" Device:"), deviceName);
|
|
700
|
+
console.log(chalk3.dim(" Platform:"), platform);
|
|
414
701
|
console.log();
|
|
415
|
-
console.log(
|
|
702
|
+
console.log(chalk3.dim(" Requesting device code..."));
|
|
416
703
|
let code;
|
|
417
704
|
let expiresAt;
|
|
418
705
|
let supabaseUrl;
|
|
@@ -429,7 +716,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
429
716
|
});
|
|
430
717
|
if (!res.ok) {
|
|
431
718
|
const text = await res.text();
|
|
432
|
-
console.log(
|
|
719
|
+
console.log(chalk3.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
|
|
433
720
|
process.exit(1);
|
|
434
721
|
}
|
|
435
722
|
const data = await res.json();
|
|
@@ -439,26 +726,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
439
726
|
supabaseAnonKey = data.supabase_anon_key;
|
|
440
727
|
} catch (err) {
|
|
441
728
|
console.log(
|
|
442
|
-
|
|
729
|
+
chalk3.red(" \u2717"),
|
|
443
730
|
`Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
444
731
|
);
|
|
445
732
|
process.exit(1);
|
|
446
733
|
}
|
|
447
734
|
console.log();
|
|
448
|
-
console.log(
|
|
449
|
-
console.log(
|
|
450
|
-
console.log(
|
|
451
|
-
console.log(
|
|
452
|
-
console.log(
|
|
735
|
+
console.log(chalk3.bold.cyan(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
|
|
736
|
+
console.log(chalk3.bold.cyan(` \u2502 \u2502`));
|
|
737
|
+
console.log(chalk3.bold.cyan(` \u2502 Code: ${chalk3.white.bold(code)} \u2502`));
|
|
738
|
+
console.log(chalk3.bold.cyan(` \u2502 \u2502`));
|
|
739
|
+
console.log(chalk3.bold.cyan(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
|
|
453
740
|
console.log();
|
|
454
741
|
const linkUrl = `${appUrl}/link?code=${code}`;
|
|
455
|
-
console.log(
|
|
742
|
+
console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
|
|
456
743
|
try {
|
|
457
744
|
const open = (await import("open")).default;
|
|
458
745
|
await open(linkUrl);
|
|
459
746
|
} catch {
|
|
460
|
-
console.log(
|
|
461
|
-
console.log(
|
|
747
|
+
console.log(chalk3.yellow(" !"), "Could not open browser automatically.");
|
|
748
|
+
console.log(chalk3.dim(" Open this URL manually:"), chalk3.underline(linkUrl));
|
|
462
749
|
}
|
|
463
750
|
console.log();
|
|
464
751
|
const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -470,11 +757,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
470
757
|
const expiresTime = new Date(expiresAt).getTime();
|
|
471
758
|
while (!approved) {
|
|
472
759
|
if (Date.now() > expiresTime) {
|
|
473
|
-
console.log(
|
|
760
|
+
console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
474
761
|
process.exit(1);
|
|
475
762
|
}
|
|
476
763
|
process.stdout.write(
|
|
477
|
-
`\r ${
|
|
764
|
+
`\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
|
|
478
765
|
);
|
|
479
766
|
dotIndex++;
|
|
480
767
|
await sleep(2e3);
|
|
@@ -492,16 +779,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
492
779
|
userId = data.user_id || "";
|
|
493
780
|
approved = true;
|
|
494
781
|
} else if (data.status === "expired") {
|
|
495
|
-
console.log(
|
|
782
|
+
console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
496
783
|
process.exit(1);
|
|
497
784
|
}
|
|
498
785
|
} catch {
|
|
499
786
|
}
|
|
500
787
|
}
|
|
501
788
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
502
|
-
console.log(
|
|
789
|
+
console.log(chalk3.green(" \u2713"), "Device approved!");
|
|
503
790
|
console.log();
|
|
504
|
-
console.log(
|
|
791
|
+
console.log(chalk3.dim(" Exchanging token for session..."));
|
|
505
792
|
let accessToken = "";
|
|
506
793
|
let refreshToken = "";
|
|
507
794
|
try {
|
|
@@ -513,11 +800,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
513
800
|
type: "magiclink"
|
|
514
801
|
});
|
|
515
802
|
if (error) {
|
|
516
|
-
console.log(
|
|
803
|
+
console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
|
|
517
804
|
process.exit(1);
|
|
518
805
|
}
|
|
519
806
|
if (!session.session) {
|
|
520
|
-
console.log(
|
|
807
|
+
console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
|
|
521
808
|
process.exit(1);
|
|
522
809
|
}
|
|
523
810
|
accessToken = session.session.access_token;
|
|
@@ -525,12 +812,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
525
812
|
userId = session.session.user?.id || userId;
|
|
526
813
|
} catch (err) {
|
|
527
814
|
console.log(
|
|
528
|
-
|
|
815
|
+
chalk3.red(" \u2717"),
|
|
529
816
|
`Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
|
|
530
817
|
);
|
|
531
818
|
process.exit(1);
|
|
532
819
|
}
|
|
533
|
-
console.log(
|
|
820
|
+
console.log(chalk3.green(" \u2713"), "Session established.");
|
|
534
821
|
const config = {
|
|
535
822
|
appUrl,
|
|
536
823
|
supabaseUrl,
|
|
@@ -543,66 +830,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
543
830
|
projectsDir: join2(homedir(), ".tenux", "projects")
|
|
544
831
|
};
|
|
545
832
|
saveConfig(config);
|
|
546
|
-
console.log(
|
|
833
|
+
console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
|
|
547
834
|
console.log();
|
|
548
835
|
const claude = detectClaudeCode();
|
|
549
836
|
if (claude.installed) {
|
|
550
|
-
console.log(
|
|
837
|
+
console.log(chalk3.green(" \u2713"), "Claude Code detected");
|
|
551
838
|
if (claude.authenticated) {
|
|
552
|
-
console.log(
|
|
839
|
+
console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
|
|
553
840
|
} else {
|
|
554
841
|
console.log(
|
|
555
|
-
|
|
842
|
+
chalk3.yellow(" !"),
|
|
556
843
|
"Claude Code found but ~/.claude not detected.",
|
|
557
|
-
|
|
844
|
+
chalk3.dim("Run `claude login` to authenticate.")
|
|
558
845
|
);
|
|
559
846
|
}
|
|
560
847
|
} else {
|
|
561
|
-
console.log(
|
|
848
|
+
console.log(chalk3.yellow(" !"), "Claude Code not found.");
|
|
562
849
|
console.log(
|
|
563
|
-
|
|
564
|
-
|
|
850
|
+
chalk3.dim(" Install it:"),
|
|
851
|
+
chalk3.underline("https://docs.anthropic.com/en/docs/claude-code")
|
|
565
852
|
);
|
|
566
853
|
console.log(
|
|
567
|
-
|
|
568
|
-
|
|
854
|
+
chalk3.dim(" Then run:"),
|
|
855
|
+
chalk3.cyan("claude login")
|
|
569
856
|
);
|
|
570
857
|
}
|
|
571
858
|
console.log();
|
|
572
|
-
console.log(
|
|
573
|
-
console.log(
|
|
574
|
-
console.log(
|
|
575
|
-
console.log(
|
|
859
|
+
console.log(chalk3.dim(" Device:"), deviceName);
|
|
860
|
+
console.log(chalk3.dim(" Device ID:"), deviceId);
|
|
861
|
+
console.log(chalk3.dim(" User ID:"), userId);
|
|
862
|
+
console.log(chalk3.dim(" Projects:"), config.projectsDir);
|
|
576
863
|
console.log();
|
|
577
864
|
console.log(
|
|
578
|
-
|
|
579
|
-
|
|
865
|
+
chalk3.dim(" Next: start the agent:"),
|
|
866
|
+
chalk3.cyan("tenux start")
|
|
580
867
|
);
|
|
581
868
|
console.log();
|
|
582
869
|
});
|
|
583
870
|
program.command("start").description("Start the agent and listen for commands").action(async () => {
|
|
584
871
|
if (!configExists()) {
|
|
585
|
-
console.log(
|
|
872
|
+
console.log(chalk3.red("\u2717"), "Not logged in. Run `tenux login` first.");
|
|
586
873
|
process.exit(1);
|
|
587
874
|
}
|
|
588
875
|
const claude = detectClaudeCode();
|
|
589
876
|
if (!claude.installed) {
|
|
590
|
-
console.log(
|
|
591
|
-
console.log(
|
|
592
|
-
console.log(
|
|
877
|
+
console.log(chalk3.red("\u2717"), "Claude Code not found.");
|
|
878
|
+
console.log(chalk3.dim(" Install it:"), chalk3.cyan("npm i -g @anthropic-ai/claude-code"));
|
|
879
|
+
console.log(chalk3.dim(" Then run:"), chalk3.cyan("claude login"));
|
|
593
880
|
process.exit(1);
|
|
594
881
|
}
|
|
595
882
|
const config = loadConfig();
|
|
596
|
-
console.log(
|
|
597
|
-
console.log(
|
|
598
|
-
console.log(
|
|
883
|
+
console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
|
|
884
|
+
console.log(chalk3.dim(" Device:"), config.deviceName);
|
|
885
|
+
console.log(chalk3.dim(" Projects:"), config.projectsDir);
|
|
599
886
|
console.log();
|
|
600
887
|
const supabase = getSupabase();
|
|
601
888
|
try {
|
|
602
889
|
await initSupabaseSession();
|
|
603
890
|
} catch (err) {
|
|
604
|
-
console.log(
|
|
605
|
-
console.log(
|
|
891
|
+
console.log(chalk3.red("\u2717"), `Session expired: ${err.message}`);
|
|
892
|
+
console.log(chalk3.dim(" Run `tenux login` to re-authenticate."));
|
|
606
893
|
process.exit(1);
|
|
607
894
|
}
|
|
608
895
|
await supabase.from("devices").update({
|
|
@@ -610,56 +897,58 @@ program.command("start").description("Start the agent and listen for commands").
|
|
|
610
897
|
is_online: true,
|
|
611
898
|
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
612
899
|
}).eq("id", config.deviceId);
|
|
900
|
+
await cleanupStaleServers(supabase, config.deviceId);
|
|
613
901
|
const heartbeat = setInterval(async () => {
|
|
614
902
|
const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
|
|
615
903
|
if (error) {
|
|
616
|
-
console.log(
|
|
904
|
+
console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
|
|
617
905
|
}
|
|
618
906
|
}, 3e4);
|
|
619
907
|
const relay = new Relay(supabase);
|
|
620
908
|
const shutdown = async () => {
|
|
621
|
-
console.log(
|
|
909
|
+
console.log(chalk3.dim("\n Shutting down..."));
|
|
622
910
|
clearInterval(heartbeat);
|
|
911
|
+
await cleanupServers(supabase, config.deviceId);
|
|
623
912
|
await relay.stop();
|
|
624
913
|
await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
|
|
625
914
|
process.exit(0);
|
|
626
915
|
};
|
|
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(
|
|
916
|
+
relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("project.delete", handleProjectDelete).on("server.start", handleServerStart).on("server.stop", handleServerStop).on("server.status", handleServerStatus).on("agent.shutdown", async () => {
|
|
917
|
+
console.log(chalk3.yellow("\n \u26A1 Remote shutdown requested"));
|
|
629
918
|
await shutdown();
|
|
630
919
|
});
|
|
631
920
|
await relay.start();
|
|
632
|
-
console.log(
|
|
921
|
+
console.log(chalk3.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
|
|
633
922
|
process.on("SIGINT", shutdown);
|
|
634
923
|
process.on("SIGTERM", shutdown);
|
|
635
924
|
});
|
|
636
925
|
program.command("status").description("Show agent configuration and status").action(() => {
|
|
637
926
|
if (!configExists()) {
|
|
638
|
-
console.log(
|
|
927
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
639
928
|
process.exit(1);
|
|
640
929
|
}
|
|
641
930
|
const config = loadConfig();
|
|
642
931
|
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(
|
|
932
|
+
console.log(chalk3.bold("\n tenux"), chalk3.dim("agent status\n"));
|
|
933
|
+
console.log(chalk3.dim(" Config:"), getConfigPath());
|
|
934
|
+
console.log(chalk3.dim(" App URL:"), config.appUrl);
|
|
935
|
+
console.log(chalk3.dim(" Device:"), config.deviceName);
|
|
936
|
+
console.log(chalk3.dim(" Device ID:"), config.deviceId);
|
|
937
|
+
console.log(chalk3.dim(" User ID:"), config.userId);
|
|
938
|
+
console.log(chalk3.dim(" Supabase:"), config.supabaseUrl);
|
|
939
|
+
console.log(chalk3.dim(" Projects:"), config.projectsDir);
|
|
651
940
|
console.log(
|
|
652
|
-
|
|
653
|
-
config.accessToken ?
|
|
941
|
+
chalk3.dim(" Auth:"),
|
|
942
|
+
config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
|
|
654
943
|
);
|
|
655
944
|
console.log(
|
|
656
|
-
|
|
657
|
-
claude.installed ?
|
|
945
|
+
chalk3.dim(" Claude Code:"),
|
|
946
|
+
claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
|
|
658
947
|
);
|
|
659
948
|
if (claude.installed) {
|
|
660
949
|
console.log(
|
|
661
|
-
|
|
662
|
-
claude.authenticated ?
|
|
950
|
+
chalk3.dim(" Claude Auth:"),
|
|
951
|
+
claude.authenticated ? chalk3.green("yes (~/.claude exists)") : chalk3.yellow("not authenticated")
|
|
663
952
|
);
|
|
664
953
|
}
|
|
665
954
|
console.log();
|
|
@@ -667,7 +956,7 @@ program.command("status").description("Show agent configuration and status").act
|
|
|
667
956
|
var configCmd = program.command("config").description("Manage agent configuration");
|
|
668
957
|
configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
|
|
669
958
|
if (!configExists()) {
|
|
670
|
-
console.log(
|
|
959
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
671
960
|
process.exit(1);
|
|
672
961
|
}
|
|
673
962
|
const keyMap = {
|
|
@@ -678,17 +967,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
|
|
|
678
967
|
const configKey = keyMap[key];
|
|
679
968
|
if (!configKey) {
|
|
680
969
|
console.log(
|
|
681
|
-
|
|
970
|
+
chalk3.red("\u2717"),
|
|
682
971
|
`Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
683
972
|
);
|
|
684
973
|
process.exit(1);
|
|
685
974
|
}
|
|
686
975
|
updateConfig({ [configKey]: value });
|
|
687
|
-
console.log(
|
|
976
|
+
console.log(chalk3.green("\u2713"), `Set ${key}`);
|
|
688
977
|
});
|
|
689
978
|
configCmd.command("get <key>").description("Get a config value").action((key) => {
|
|
690
979
|
if (!configExists()) {
|
|
691
|
-
console.log(
|
|
980
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
692
981
|
process.exit(1);
|
|
693
982
|
}
|
|
694
983
|
const config = loadConfig();
|
|
@@ -703,12 +992,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
703
992
|
const configKey = keyMap[key];
|
|
704
993
|
if (!configKey) {
|
|
705
994
|
console.log(
|
|
706
|
-
|
|
995
|
+
chalk3.red("\u2717"),
|
|
707
996
|
`Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
708
997
|
);
|
|
709
998
|
process.exit(1);
|
|
710
999
|
}
|
|
711
1000
|
const val = config[configKey];
|
|
712
|
-
console.log(val ??
|
|
1001
|
+
console.log(val ?? chalk3.dim("(not set)"));
|
|
713
1002
|
});
|
|
714
1003
|
program.parse();
|