@tenux/cli 0.0.20 → 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 +419 -85
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,19 +13,19 @@ 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
|
|
|
25
25
|
// src/handlers/project.ts
|
|
26
26
|
import { spawn, execSync } from "child_process";
|
|
27
27
|
import { resolve, join } from "path";
|
|
28
|
-
import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
28
|
+
import { existsSync, mkdirSync, readdirSync, statSync, rmSync } from "fs";
|
|
29
29
|
import chalk from "chalk";
|
|
30
30
|
var LOCKFILE_PM = {
|
|
31
31
|
"bun.lockb": "bun",
|
|
@@ -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);
|
|
@@ -334,6 +334,338 @@ async function handleProjectGitStatus(command, supabase) {
|
|
|
334
334
|
}).eq("id", command.id);
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
+
async function handleProjectDelete(command, supabase) {
|
|
338
|
+
const { name, path: projectPath, project_id } = command.payload;
|
|
339
|
+
const config = loadConfig();
|
|
340
|
+
const targetDir = resolve(config.projectsDir, projectPath);
|
|
341
|
+
const normalizedTarget = resolve(targetDir);
|
|
342
|
+
const normalizedProjects = resolve(config.projectsDir);
|
|
343
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
344
|
+
if (!normalizedTarget.startsWith(normalizedProjects + sep) && normalizedTarget !== normalizedProjects) {
|
|
345
|
+
const msg = `Refusing to delete \u2014 path escapes projects dir: ${projectPath}`;
|
|
346
|
+
console.log(chalk.red("\u2717"), msg);
|
|
347
|
+
await supabase.from("commands").update({
|
|
348
|
+
status: "error",
|
|
349
|
+
result: { error: msg },
|
|
350
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
351
|
+
}).eq("id", command.id);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!existsSync(targetDir)) {
|
|
355
|
+
console.log(chalk.yellow("!"), `Project directory not found: ${projectPath} (already deleted?)`);
|
|
356
|
+
await supabase.from("commands").update({
|
|
357
|
+
status: "done",
|
|
358
|
+
result: { deleted: false, reason: "directory_not_found" },
|
|
359
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
360
|
+
}).eq("id", command.id);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
console.log(chalk.blue("\u2192"), `Deleting project directory: ${name} (${targetDir})`);
|
|
364
|
+
try {
|
|
365
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
366
|
+
console.log(chalk.green("\u2713"), `Deleted ${name}`);
|
|
367
|
+
await supabase.from("commands").update({
|
|
368
|
+
status: "done",
|
|
369
|
+
result: { deleted: true, path: projectPath },
|
|
370
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
371
|
+
}).eq("id", command.id);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
374
|
+
console.log(chalk.red("\u2717"), `Delete failed: ${msg}`);
|
|
375
|
+
await supabase.from("commands").update({
|
|
376
|
+
status: "error",
|
|
377
|
+
result: { error: msg },
|
|
378
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
379
|
+
}).eq("id", command.id);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
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
|
+
}
|
|
337
669
|
|
|
338
670
|
// src/cli.ts
|
|
339
671
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -344,30 +676,30 @@ function detectClaudeCode() {
|
|
|
344
676
|
let installed = false;
|
|
345
677
|
try {
|
|
346
678
|
if (process.platform === "win32") {
|
|
347
|
-
|
|
679
|
+
execSync3("where claude", { stdio: "ignore" });
|
|
348
680
|
} else {
|
|
349
|
-
|
|
681
|
+
execSync3("which claude", { stdio: "ignore" });
|
|
350
682
|
}
|
|
351
683
|
installed = true;
|
|
352
684
|
} catch {
|
|
353
685
|
installed = false;
|
|
354
686
|
}
|
|
355
|
-
const authenticated =
|
|
687
|
+
const authenticated = existsSync3(join2(homedir(), ".claude"));
|
|
356
688
|
return { installed, authenticated };
|
|
357
689
|
}
|
|
358
690
|
async function sleep(ms) {
|
|
359
|
-
return new Promise((
|
|
691
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
360
692
|
}
|
|
361
693
|
program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
|
|
362
|
-
console.log(
|
|
694
|
+
console.log(chalk3.bold("\n tenux"), chalk3.dim("desktop agent\n"));
|
|
363
695
|
const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
|
|
364
|
-
console.log(
|
|
696
|
+
console.log(chalk3.dim(" App:"), appUrl);
|
|
365
697
|
const deviceName = hostname();
|
|
366
698
|
const platform = process.platform;
|
|
367
|
-
console.log(
|
|
368
|
-
console.log(
|
|
699
|
+
console.log(chalk3.dim(" Device:"), deviceName);
|
|
700
|
+
console.log(chalk3.dim(" Platform:"), platform);
|
|
369
701
|
console.log();
|
|
370
|
-
console.log(
|
|
702
|
+
console.log(chalk3.dim(" Requesting device code..."));
|
|
371
703
|
let code;
|
|
372
704
|
let expiresAt;
|
|
373
705
|
let supabaseUrl;
|
|
@@ -384,7 +716,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
384
716
|
});
|
|
385
717
|
if (!res.ok) {
|
|
386
718
|
const text = await res.text();
|
|
387
|
-
console.log(
|
|
719
|
+
console.log(chalk3.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
|
|
388
720
|
process.exit(1);
|
|
389
721
|
}
|
|
390
722
|
const data = await res.json();
|
|
@@ -394,26 +726,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
394
726
|
supabaseAnonKey = data.supabase_anon_key;
|
|
395
727
|
} catch (err) {
|
|
396
728
|
console.log(
|
|
397
|
-
|
|
729
|
+
chalk3.red(" \u2717"),
|
|
398
730
|
`Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
399
731
|
);
|
|
400
732
|
process.exit(1);
|
|
401
733
|
}
|
|
402
734
|
console.log();
|
|
403
|
-
console.log(
|
|
404
|
-
console.log(
|
|
405
|
-
console.log(
|
|
406
|
-
console.log(
|
|
407
|
-
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`));
|
|
408
740
|
console.log();
|
|
409
741
|
const linkUrl = `${appUrl}/link?code=${code}`;
|
|
410
|
-
console.log(
|
|
742
|
+
console.log(chalk3.dim(" Opening browser to:"), chalk3.underline(linkUrl));
|
|
411
743
|
try {
|
|
412
744
|
const open = (await import("open")).default;
|
|
413
745
|
await open(linkUrl);
|
|
414
746
|
} catch {
|
|
415
|
-
console.log(
|
|
416
|
-
console.log(
|
|
747
|
+
console.log(chalk3.yellow(" !"), "Could not open browser automatically.");
|
|
748
|
+
console.log(chalk3.dim(" Open this URL manually:"), chalk3.underline(linkUrl));
|
|
417
749
|
}
|
|
418
750
|
console.log();
|
|
419
751
|
const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -425,11 +757,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
425
757
|
const expiresTime = new Date(expiresAt).getTime();
|
|
426
758
|
while (!approved) {
|
|
427
759
|
if (Date.now() > expiresTime) {
|
|
428
|
-
console.log(
|
|
760
|
+
console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
429
761
|
process.exit(1);
|
|
430
762
|
}
|
|
431
763
|
process.stdout.write(
|
|
432
|
-
`\r ${
|
|
764
|
+
`\r ${chalk3.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
|
|
433
765
|
);
|
|
434
766
|
dotIndex++;
|
|
435
767
|
await sleep(2e3);
|
|
@@ -447,16 +779,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
447
779
|
userId = data.user_id || "";
|
|
448
780
|
approved = true;
|
|
449
781
|
} else if (data.status === "expired") {
|
|
450
|
-
console.log(
|
|
782
|
+
console.log(chalk3.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
451
783
|
process.exit(1);
|
|
452
784
|
}
|
|
453
785
|
} catch {
|
|
454
786
|
}
|
|
455
787
|
}
|
|
456
788
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
457
|
-
console.log(
|
|
789
|
+
console.log(chalk3.green(" \u2713"), "Device approved!");
|
|
458
790
|
console.log();
|
|
459
|
-
console.log(
|
|
791
|
+
console.log(chalk3.dim(" Exchanging token for session..."));
|
|
460
792
|
let accessToken = "";
|
|
461
793
|
let refreshToken = "";
|
|
462
794
|
try {
|
|
@@ -468,11 +800,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
468
800
|
type: "magiclink"
|
|
469
801
|
});
|
|
470
802
|
if (error) {
|
|
471
|
-
console.log(
|
|
803
|
+
console.log(chalk3.red(" \u2717"), `Auth failed: ${error.message}`);
|
|
472
804
|
process.exit(1);
|
|
473
805
|
}
|
|
474
806
|
if (!session.session) {
|
|
475
|
-
console.log(
|
|
807
|
+
console.log(chalk3.red(" \u2717"), "No session returned from auth exchange.");
|
|
476
808
|
process.exit(1);
|
|
477
809
|
}
|
|
478
810
|
accessToken = session.session.access_token;
|
|
@@ -480,12 +812,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
480
812
|
userId = session.session.user?.id || userId;
|
|
481
813
|
} catch (err) {
|
|
482
814
|
console.log(
|
|
483
|
-
|
|
815
|
+
chalk3.red(" \u2717"),
|
|
484
816
|
`Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
|
|
485
817
|
);
|
|
486
818
|
process.exit(1);
|
|
487
819
|
}
|
|
488
|
-
console.log(
|
|
820
|
+
console.log(chalk3.green(" \u2713"), "Session established.");
|
|
489
821
|
const config = {
|
|
490
822
|
appUrl,
|
|
491
823
|
supabaseUrl,
|
|
@@ -498,66 +830,66 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
498
830
|
projectsDir: join2(homedir(), ".tenux", "projects")
|
|
499
831
|
};
|
|
500
832
|
saveConfig(config);
|
|
501
|
-
console.log(
|
|
833
|
+
console.log(chalk3.green(" \u2713"), "Config saved to", chalk3.dim(getConfigPath()));
|
|
502
834
|
console.log();
|
|
503
835
|
const claude = detectClaudeCode();
|
|
504
836
|
if (claude.installed) {
|
|
505
|
-
console.log(
|
|
837
|
+
console.log(chalk3.green(" \u2713"), "Claude Code detected");
|
|
506
838
|
if (claude.authenticated) {
|
|
507
|
-
console.log(
|
|
839
|
+
console.log(chalk3.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
|
|
508
840
|
} else {
|
|
509
841
|
console.log(
|
|
510
|
-
|
|
842
|
+
chalk3.yellow(" !"),
|
|
511
843
|
"Claude Code found but ~/.claude not detected.",
|
|
512
|
-
|
|
844
|
+
chalk3.dim("Run `claude login` to authenticate.")
|
|
513
845
|
);
|
|
514
846
|
}
|
|
515
847
|
} else {
|
|
516
|
-
console.log(
|
|
848
|
+
console.log(chalk3.yellow(" !"), "Claude Code not found.");
|
|
517
849
|
console.log(
|
|
518
|
-
|
|
519
|
-
|
|
850
|
+
chalk3.dim(" Install it:"),
|
|
851
|
+
chalk3.underline("https://docs.anthropic.com/en/docs/claude-code")
|
|
520
852
|
);
|
|
521
853
|
console.log(
|
|
522
|
-
|
|
523
|
-
|
|
854
|
+
chalk3.dim(" Then run:"),
|
|
855
|
+
chalk3.cyan("claude login")
|
|
524
856
|
);
|
|
525
857
|
}
|
|
526
858
|
console.log();
|
|
527
|
-
console.log(
|
|
528
|
-
console.log(
|
|
529
|
-
console.log(
|
|
530
|
-
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);
|
|
531
863
|
console.log();
|
|
532
864
|
console.log(
|
|
533
|
-
|
|
534
|
-
|
|
865
|
+
chalk3.dim(" Next: start the agent:"),
|
|
866
|
+
chalk3.cyan("tenux start")
|
|
535
867
|
);
|
|
536
868
|
console.log();
|
|
537
869
|
});
|
|
538
870
|
program.command("start").description("Start the agent and listen for commands").action(async () => {
|
|
539
871
|
if (!configExists()) {
|
|
540
|
-
console.log(
|
|
872
|
+
console.log(chalk3.red("\u2717"), "Not logged in. Run `tenux login` first.");
|
|
541
873
|
process.exit(1);
|
|
542
874
|
}
|
|
543
875
|
const claude = detectClaudeCode();
|
|
544
876
|
if (!claude.installed) {
|
|
545
|
-
console.log(
|
|
546
|
-
console.log(
|
|
547
|
-
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"));
|
|
548
880
|
process.exit(1);
|
|
549
881
|
}
|
|
550
882
|
const config = loadConfig();
|
|
551
|
-
console.log(
|
|
552
|
-
console.log(
|
|
553
|
-
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);
|
|
554
886
|
console.log();
|
|
555
887
|
const supabase = getSupabase();
|
|
556
888
|
try {
|
|
557
889
|
await initSupabaseSession();
|
|
558
890
|
} catch (err) {
|
|
559
|
-
console.log(
|
|
560
|
-
console.log(
|
|
891
|
+
console.log(chalk3.red("\u2717"), `Session expired: ${err.message}`);
|
|
892
|
+
console.log(chalk3.dim(" Run `tenux login` to re-authenticate."));
|
|
561
893
|
process.exit(1);
|
|
562
894
|
}
|
|
563
895
|
await supabase.from("devices").update({
|
|
@@ -565,56 +897,58 @@ program.command("start").description("Start the agent and listen for commands").
|
|
|
565
897
|
is_online: true,
|
|
566
898
|
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
567
899
|
}).eq("id", config.deviceId);
|
|
900
|
+
await cleanupStaleServers(supabase, config.deviceId);
|
|
568
901
|
const heartbeat = setInterval(async () => {
|
|
569
902
|
const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
|
|
570
903
|
if (error) {
|
|
571
|
-
console.log(
|
|
904
|
+
console.log(chalk3.red("\u2717"), chalk3.dim(`Heartbeat failed: ${error.message}`));
|
|
572
905
|
}
|
|
573
906
|
}, 3e4);
|
|
574
907
|
const relay = new Relay(supabase);
|
|
575
908
|
const shutdown = async () => {
|
|
576
|
-
console.log(
|
|
909
|
+
console.log(chalk3.dim("\n Shutting down..."));
|
|
577
910
|
clearInterval(heartbeat);
|
|
911
|
+
await cleanupServers(supabase, config.deviceId);
|
|
578
912
|
await relay.stop();
|
|
579
913
|
await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
|
|
580
914
|
process.exit(0);
|
|
581
915
|
};
|
|
582
|
-
relay.on("claude.query", handleClaudeQuery).on("project.clone", handleProjectClone).on("project.create", handleProjectCreate).on("project.tree", handleProjectTree).on("project.git_status", handleProjectGitStatus).on("agent.shutdown", async () => {
|
|
583
|
-
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"));
|
|
584
918
|
await shutdown();
|
|
585
919
|
});
|
|
586
920
|
await relay.start();
|
|
587
|
-
console.log(
|
|
921
|
+
console.log(chalk3.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
|
|
588
922
|
process.on("SIGINT", shutdown);
|
|
589
923
|
process.on("SIGTERM", shutdown);
|
|
590
924
|
});
|
|
591
925
|
program.command("status").description("Show agent configuration and status").action(() => {
|
|
592
926
|
if (!configExists()) {
|
|
593
|
-
console.log(
|
|
927
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
594
928
|
process.exit(1);
|
|
595
929
|
}
|
|
596
930
|
const config = loadConfig();
|
|
597
931
|
const claude = detectClaudeCode();
|
|
598
|
-
console.log(
|
|
599
|
-
console.log(
|
|
600
|
-
console.log(
|
|
601
|
-
console.log(
|
|
602
|
-
console.log(
|
|
603
|
-
console.log(
|
|
604
|
-
console.log(
|
|
605
|
-
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);
|
|
606
940
|
console.log(
|
|
607
|
-
|
|
608
|
-
config.accessToken ?
|
|
941
|
+
chalk3.dim(" Auth:"),
|
|
942
|
+
config.accessToken ? chalk3.green("authenticated") : chalk3.yellow("not authenticated")
|
|
609
943
|
);
|
|
610
944
|
console.log(
|
|
611
|
-
|
|
612
|
-
claude.installed ?
|
|
945
|
+
chalk3.dim(" Claude Code:"),
|
|
946
|
+
claude.installed ? chalk3.green("installed") : chalk3.yellow("not installed")
|
|
613
947
|
);
|
|
614
948
|
if (claude.installed) {
|
|
615
949
|
console.log(
|
|
616
|
-
|
|
617
|
-
claude.authenticated ?
|
|
950
|
+
chalk3.dim(" Claude Auth:"),
|
|
951
|
+
claude.authenticated ? chalk3.green("yes (~/.claude exists)") : chalk3.yellow("not authenticated")
|
|
618
952
|
);
|
|
619
953
|
}
|
|
620
954
|
console.log();
|
|
@@ -622,7 +956,7 @@ program.command("status").description("Show agent configuration and status").act
|
|
|
622
956
|
var configCmd = program.command("config").description("Manage agent configuration");
|
|
623
957
|
configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
|
|
624
958
|
if (!configExists()) {
|
|
625
|
-
console.log(
|
|
959
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
626
960
|
process.exit(1);
|
|
627
961
|
}
|
|
628
962
|
const keyMap = {
|
|
@@ -633,17 +967,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
|
|
|
633
967
|
const configKey = keyMap[key];
|
|
634
968
|
if (!configKey) {
|
|
635
969
|
console.log(
|
|
636
|
-
|
|
970
|
+
chalk3.red("\u2717"),
|
|
637
971
|
`Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
638
972
|
);
|
|
639
973
|
process.exit(1);
|
|
640
974
|
}
|
|
641
975
|
updateConfig({ [configKey]: value });
|
|
642
|
-
console.log(
|
|
976
|
+
console.log(chalk3.green("\u2713"), `Set ${key}`);
|
|
643
977
|
});
|
|
644
978
|
configCmd.command("get <key>").description("Get a config value").action((key) => {
|
|
645
979
|
if (!configExists()) {
|
|
646
|
-
console.log(
|
|
980
|
+
console.log(chalk3.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
647
981
|
process.exit(1);
|
|
648
982
|
}
|
|
649
983
|
const config = loadConfig();
|
|
@@ -658,12 +992,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
658
992
|
const configKey = keyMap[key];
|
|
659
993
|
if (!configKey) {
|
|
660
994
|
console.log(
|
|
661
|
-
|
|
995
|
+
chalk3.red("\u2717"),
|
|
662
996
|
`Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
663
997
|
);
|
|
664
998
|
process.exit(1);
|
|
665
999
|
}
|
|
666
1000
|
const val = config[configKey];
|
|
667
|
-
console.log(val ??
|
|
1001
|
+
console.log(val ?? chalk3.dim("(not set)"));
|
|
668
1002
|
});
|
|
669
1003
|
program.parse();
|