@tenux/cli 0.0.16 → 0.0.18
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/{chunk-MB4X6UQJ.js → chunk-QPS4CXAL.js} +2 -0
- package/dist/cli.js +401 -86
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -59,6 +59,7 @@ function getSupabase() {
|
|
|
59
59
|
accessToken: session.access_token,
|
|
60
60
|
refreshToken: session.refresh_token
|
|
61
61
|
});
|
|
62
|
+
client.realtime.setAuth(session.access_token);
|
|
62
63
|
}
|
|
63
64
|
});
|
|
64
65
|
return client;
|
|
@@ -75,6 +76,7 @@ async function initSupabaseSession() {
|
|
|
75
76
|
if (error) {
|
|
76
77
|
throw new Error(`Failed to restore session: ${error.message}`);
|
|
77
78
|
}
|
|
79
|
+
sb.realtime.setAuth(config.accessToken);
|
|
78
80
|
}
|
|
79
81
|
initialized = true;
|
|
80
82
|
}
|
package/dist/cli.js
CHANGED
|
@@ -9,50 +9,365 @@ import {
|
|
|
9
9
|
loadConfig,
|
|
10
10
|
saveConfig,
|
|
11
11
|
updateConfig
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QPS4CXAL.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
import { Command } from "commander";
|
|
16
|
-
import
|
|
16
|
+
import chalk2 from "chalk";
|
|
17
17
|
import { hostname } from "os";
|
|
18
18
|
import { homedir } from "os";
|
|
19
|
-
import { join, dirname } from "path";
|
|
20
|
-
import { existsSync, readFileSync } from "fs";
|
|
21
|
-
import { execSync } from "child_process";
|
|
19
|
+
import { join as join2, dirname } from "path";
|
|
20
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
21
|
+
import { execSync as execSync2 } from "child_process";
|
|
22
22
|
import { fileURLToPath } from "url";
|
|
23
23
|
import { createClient } from "@supabase/supabase-js";
|
|
24
|
+
|
|
25
|
+
// src/handlers/project.ts
|
|
26
|
+
import { spawn, execSync } from "child_process";
|
|
27
|
+
import { resolve, join } from "path";
|
|
28
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
29
|
+
import chalk from "chalk";
|
|
30
|
+
var LOCKFILE_PM = {
|
|
31
|
+
"bun.lockb": "bun",
|
|
32
|
+
"bun.lock": "bun",
|
|
33
|
+
"pnpm-lock.yaml": "pnpm",
|
|
34
|
+
"yarn.lock": "yarn",
|
|
35
|
+
"package-lock.json": "npm",
|
|
36
|
+
"requirements.txt": "pip",
|
|
37
|
+
"go.mod": "go",
|
|
38
|
+
"Cargo.toml": "cargo"
|
|
39
|
+
};
|
|
40
|
+
function detectPackageManager(dir) {
|
|
41
|
+
for (const [file, pm] of Object.entries(LOCKFILE_PM)) {
|
|
42
|
+
if (existsSync(join(dir, file))) return pm;
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(join(dir, "package.json"))) return "npm";
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function installCommand(pm) {
|
|
48
|
+
switch (pm) {
|
|
49
|
+
case "bun":
|
|
50
|
+
return ["bun", "install"];
|
|
51
|
+
case "pnpm":
|
|
52
|
+
return ["pnpm", "install"];
|
|
53
|
+
case "yarn":
|
|
54
|
+
return ["yarn"];
|
|
55
|
+
case "npm":
|
|
56
|
+
return ["npm", "install"];
|
|
57
|
+
case "pip":
|
|
58
|
+
return ["pip", "install", "-r", "requirements.txt"];
|
|
59
|
+
case "cargo":
|
|
60
|
+
return ["cargo", "build"];
|
|
61
|
+
case "go":
|
|
62
|
+
return ["go", "mod", "download"];
|
|
63
|
+
default:
|
|
64
|
+
return ["npm", "install"];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function run(cmd, args, cwd, timeoutMs = 3e5) {
|
|
68
|
+
return new Promise((resolve2, reject) => {
|
|
69
|
+
const isWindows = process.platform === "win32";
|
|
70
|
+
const proc = spawn(cmd, args, {
|
|
71
|
+
cwd,
|
|
72
|
+
shell: isWindows,
|
|
73
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
+
timeout: timeoutMs
|
|
75
|
+
});
|
|
76
|
+
let stdout = "";
|
|
77
|
+
let stderr = "";
|
|
78
|
+
proc.stdout.on("data", (d) => {
|
|
79
|
+
stdout += d.toString();
|
|
80
|
+
});
|
|
81
|
+
proc.stderr.on("data", (d) => {
|
|
82
|
+
stderr += d.toString();
|
|
83
|
+
});
|
|
84
|
+
proc.on("close", (code) => {
|
|
85
|
+
if (code === 0) resolve2(stdout);
|
|
86
|
+
else reject(new Error(stderr.trim().slice(0, 500) || `Exit code ${code}`));
|
|
87
|
+
});
|
|
88
|
+
proc.on("error", reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function updateProject(supabase, projectId, fields) {
|
|
92
|
+
await supabase.from("projects").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", projectId);
|
|
93
|
+
}
|
|
94
|
+
async function handleProjectClone(command, supabase) {
|
|
95
|
+
const { url, name, project_id, github_token } = command.payload;
|
|
96
|
+
const config = loadConfig();
|
|
97
|
+
if (!existsSync(config.projectsDir)) mkdirSync(config.projectsDir, { recursive: true });
|
|
98
|
+
const targetDir = resolve(config.projectsDir, name);
|
|
99
|
+
await updateProject(supabase, project_id, { status: "cloning" });
|
|
100
|
+
let cloneUrl = url;
|
|
101
|
+
if (github_token && url.startsWith("https://github.com/")) {
|
|
102
|
+
cloneUrl = url.replace("https://github.com/", `https://${github_token}@github.com/`);
|
|
103
|
+
}
|
|
104
|
+
console.log(chalk.blue("\u2192"), `Cloning ${url} \u2192 ${name}`);
|
|
105
|
+
try {
|
|
106
|
+
await run("git", ["clone", cloneUrl, targetDir], config.projectsDir, 3e5);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
console.log(chalk.red("\u2717"), `Clone failed: ${msg}`);
|
|
110
|
+
await updateProject(supabase, project_id, {
|
|
111
|
+
status: "error",
|
|
112
|
+
error_message: msg
|
|
113
|
+
});
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
const pm = detectPackageManager(targetDir);
|
|
117
|
+
await updateProject(supabase, project_id, {
|
|
118
|
+
status: "installing",
|
|
119
|
+
package_manager: pm,
|
|
120
|
+
local_path: targetDir,
|
|
121
|
+
path: name
|
|
122
|
+
});
|
|
123
|
+
if (pm) {
|
|
124
|
+
console.log(chalk.blue("\u2192"), `Installing with ${pm}\u2026`);
|
|
125
|
+
try {
|
|
126
|
+
const [cmd, ...args] = installCommand(pm);
|
|
127
|
+
await run(cmd, args, targetDir, 6e5);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
console.log(chalk.yellow("!"), `Install warning: ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await updateProject(supabase, project_id, { status: "ready" });
|
|
134
|
+
console.log(chalk.green("\u2713"), `Project ${name} ready`);
|
|
135
|
+
await supabase.from("commands").update({
|
|
136
|
+
status: "done",
|
|
137
|
+
result: { path: name, package_manager: pm },
|
|
138
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
139
|
+
}).eq("id", command.id);
|
|
140
|
+
}
|
|
141
|
+
var TEMPLATES = {
|
|
142
|
+
nextjs: {
|
|
143
|
+
cmd: "npx",
|
|
144
|
+
args: (name) => ["create-next-app@latest", name, "--yes"],
|
|
145
|
+
pm: "npm"
|
|
146
|
+
},
|
|
147
|
+
"react-vite": {
|
|
148
|
+
cmd: "npm",
|
|
149
|
+
args: (name) => ["create", "vite@latest", name, "--", "--template", "react-ts"],
|
|
150
|
+
pm: "npm"
|
|
151
|
+
},
|
|
152
|
+
express: {
|
|
153
|
+
cmd: "npx",
|
|
154
|
+
args: (name) => ["express-generator", name],
|
|
155
|
+
pm: "npm"
|
|
156
|
+
},
|
|
157
|
+
"node-ts": {
|
|
158
|
+
cmd: "npx",
|
|
159
|
+
args: (name) => ["create-ts-node", name, "--yes"],
|
|
160
|
+
pm: "npm"
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
async function handleProjectCreate(command, supabase) {
|
|
164
|
+
const { template, name, project_id } = command.payload;
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
if (!existsSync(config.projectsDir)) mkdirSync(config.projectsDir, { recursive: true });
|
|
167
|
+
const tmpl = TEMPLATES[template];
|
|
168
|
+
if (!tmpl) {
|
|
169
|
+
await updateProject(supabase, project_id, {
|
|
170
|
+
status: "error",
|
|
171
|
+
error_message: `Unknown template: ${template}`
|
|
172
|
+
});
|
|
173
|
+
throw new Error(`Unknown template: ${template}`);
|
|
174
|
+
}
|
|
175
|
+
await updateProject(supabase, project_id, { status: "cloning" });
|
|
176
|
+
console.log(chalk.blue("\u2192"), `Creating ${template} project: ${name}`);
|
|
177
|
+
try {
|
|
178
|
+
const isWindows = process.platform === "win32";
|
|
179
|
+
const cmd = isWindows && tmpl.cmd === "npx" ? "npx.cmd" : tmpl.cmd;
|
|
180
|
+
await run(cmd, tmpl.args(name), config.projectsDir, 3e5);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
console.log(chalk.red("\u2717"), `Template creation failed: ${msg}`);
|
|
184
|
+
await updateProject(supabase, project_id, {
|
|
185
|
+
status: "error",
|
|
186
|
+
error_message: msg
|
|
187
|
+
});
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
const targetDir = resolve(config.projectsDir, name);
|
|
191
|
+
const pm = detectPackageManager(targetDir) || tmpl.pm || "npm";
|
|
192
|
+
await updateProject(supabase, project_id, {
|
|
193
|
+
status: "installing",
|
|
194
|
+
package_manager: pm,
|
|
195
|
+
local_path: targetDir,
|
|
196
|
+
path: name
|
|
197
|
+
});
|
|
198
|
+
if (pm && existsSync(join(targetDir, "package.json"))) {
|
|
199
|
+
try {
|
|
200
|
+
const [installCmd, ...installArgs] = installCommand(pm);
|
|
201
|
+
await run(installCmd, installArgs, targetDir, 6e5);
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await updateProject(supabase, project_id, { status: "ready" });
|
|
206
|
+
console.log(chalk.green("\u2713"), `Project ${name} ready`);
|
|
207
|
+
await supabase.from("commands").update({
|
|
208
|
+
status: "done",
|
|
209
|
+
result: { path: name, package_manager: pm, template },
|
|
210
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
211
|
+
}).eq("id", command.id);
|
|
212
|
+
}
|
|
213
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
214
|
+
"node_modules",
|
|
215
|
+
".git",
|
|
216
|
+
"__pycache__",
|
|
217
|
+
".next",
|
|
218
|
+
"dist",
|
|
219
|
+
"build",
|
|
220
|
+
".turbo",
|
|
221
|
+
".cache",
|
|
222
|
+
".vercel",
|
|
223
|
+
".output",
|
|
224
|
+
"coverage",
|
|
225
|
+
".svelte-kit",
|
|
226
|
+
"target",
|
|
227
|
+
"venv",
|
|
228
|
+
".venv",
|
|
229
|
+
"env",
|
|
230
|
+
".env"
|
|
231
|
+
]);
|
|
232
|
+
function buildTree(dir, depth, maxEntries, count) {
|
|
233
|
+
if (depth <= 0 || count.n >= maxEntries) return [];
|
|
234
|
+
let entries;
|
|
235
|
+
try {
|
|
236
|
+
entries = readdirSync(dir).sort();
|
|
237
|
+
} catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const nodes = [];
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (count.n >= maxEntries) break;
|
|
243
|
+
if (entry.startsWith(".") && entry !== ".env.example" && entry !== ".gitignore") continue;
|
|
244
|
+
const full = join(dir, entry);
|
|
245
|
+
let stat;
|
|
246
|
+
try {
|
|
247
|
+
stat = statSync(full);
|
|
248
|
+
} catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
count.n++;
|
|
252
|
+
if (stat.isDirectory()) {
|
|
253
|
+
if (SKIP_DIRS.has(entry)) {
|
|
254
|
+
nodes.push({ name: entry, type: "dir" });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const children = buildTree(full, depth - 1, maxEntries, count);
|
|
258
|
+
nodes.push({ name: entry, type: "dir", children });
|
|
259
|
+
} else {
|
|
260
|
+
nodes.push({ name: entry, type: "file" });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return nodes;
|
|
264
|
+
}
|
|
265
|
+
async function handleProjectTree(command, supabase) {
|
|
266
|
+
const { path: projectPath, depth = 4 } = command.payload;
|
|
267
|
+
const config = loadConfig();
|
|
268
|
+
const fullPath = resolve(config.projectsDir, projectPath);
|
|
269
|
+
if (!existsSync(fullPath)) {
|
|
270
|
+
await supabase.from("commands").update({
|
|
271
|
+
status: "error",
|
|
272
|
+
result: { error: `Project path not found: ${projectPath}` },
|
|
273
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
274
|
+
}).eq("id", command.id);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const count = { n: 0 };
|
|
278
|
+
const tree = buildTree(fullPath, depth, 500, count);
|
|
279
|
+
await supabase.from("commands").update({
|
|
280
|
+
status: "done",
|
|
281
|
+
result: { tree, total: count.n },
|
|
282
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
283
|
+
}).eq("id", command.id);
|
|
284
|
+
}
|
|
285
|
+
async function handleProjectGitStatus(command, supabase) {
|
|
286
|
+
const { path: projectPath } = command.payload;
|
|
287
|
+
const config = loadConfig();
|
|
288
|
+
const fullPath = resolve(config.projectsDir, projectPath);
|
|
289
|
+
if (!existsSync(fullPath)) {
|
|
290
|
+
await supabase.from("commands").update({
|
|
291
|
+
status: "error",
|
|
292
|
+
result: { error: `Project path not found: ${projectPath}` },
|
|
293
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
294
|
+
}).eq("id", command.id);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
let branch = "unknown";
|
|
299
|
+
try {
|
|
300
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: fullPath }).toString().trim();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
let files = [];
|
|
304
|
+
try {
|
|
305
|
+
const raw = execSync("git status --porcelain", { cwd: fullPath }).toString();
|
|
306
|
+
files = raw.split("\n").filter(Boolean).map((line) => {
|
|
307
|
+
const x = line[0];
|
|
308
|
+
const y = line[1];
|
|
309
|
+
const file = line.slice(3);
|
|
310
|
+
const staged = x !== " " && x !== "?";
|
|
311
|
+
return { file, status: line.slice(0, 2).trim(), staged };
|
|
312
|
+
});
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
let ahead = 0;
|
|
316
|
+
let behind = 0;
|
|
317
|
+
try {
|
|
318
|
+
const ab = execSync("git rev-list --left-right --count HEAD...@{upstream}", { cwd: fullPath }).toString().trim().split(/\s+/);
|
|
319
|
+
ahead = parseInt(ab[0]) || 0;
|
|
320
|
+
behind = parseInt(ab[1]) || 0;
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
await supabase.from("commands").update({
|
|
324
|
+
status: "done",
|
|
325
|
+
result: { branch, files, ahead, behind, clean: files.length === 0 },
|
|
326
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
327
|
+
}).eq("id", command.id);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
330
|
+
await supabase.from("commands").update({
|
|
331
|
+
status: "error",
|
|
332
|
+
result: { error: msg },
|
|
333
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
334
|
+
}).eq("id", command.id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/cli.ts
|
|
24
339
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
var pkg = JSON.parse(readFileSync(
|
|
340
|
+
var pkg = JSON.parse(readFileSync(join2(__dirname, "..", "package.json"), "utf-8"));
|
|
26
341
|
var program = new Command();
|
|
27
342
|
program.name("tenux").description("Tenux desktop agent \u2014 bridges your machine to the Tenux mobile IDE").version(pkg.version);
|
|
28
343
|
function detectClaudeCode() {
|
|
29
344
|
let installed = false;
|
|
30
345
|
try {
|
|
31
346
|
if (process.platform === "win32") {
|
|
32
|
-
|
|
347
|
+
execSync2("where claude", { stdio: "ignore" });
|
|
33
348
|
} else {
|
|
34
|
-
|
|
349
|
+
execSync2("which claude", { stdio: "ignore" });
|
|
35
350
|
}
|
|
36
351
|
installed = true;
|
|
37
352
|
} catch {
|
|
38
353
|
installed = false;
|
|
39
354
|
}
|
|
40
|
-
const authenticated =
|
|
355
|
+
const authenticated = existsSync2(join2(homedir(), ".claude"));
|
|
41
356
|
return { installed, authenticated };
|
|
42
357
|
}
|
|
43
358
|
async function sleep(ms) {
|
|
44
|
-
return new Promise((
|
|
359
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
45
360
|
}
|
|
46
361
|
program.command("login").description("Authenticate with Tenux via browser-based device auth").option("--url <url>", "Tenux app URL").action(async (opts) => {
|
|
47
|
-
console.log(
|
|
362
|
+
console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
|
|
48
363
|
const appUrl = (opts.url ?? process.env.TENUX_APP_URL ?? "https://tenux.dev").replace(/\/+$/, "");
|
|
49
|
-
console.log(
|
|
364
|
+
console.log(chalk2.dim(" App:"), appUrl);
|
|
50
365
|
const deviceName = hostname();
|
|
51
366
|
const platform = process.platform;
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(
|
|
367
|
+
console.log(chalk2.dim(" Device:"), deviceName);
|
|
368
|
+
console.log(chalk2.dim(" Platform:"), platform);
|
|
54
369
|
console.log();
|
|
55
|
-
console.log(
|
|
370
|
+
console.log(chalk2.dim(" Requesting device code..."));
|
|
56
371
|
let code;
|
|
57
372
|
let expiresAt;
|
|
58
373
|
let supabaseUrl;
|
|
@@ -69,7 +384,7 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
69
384
|
});
|
|
70
385
|
if (!res.ok) {
|
|
71
386
|
const text = await res.text();
|
|
72
|
-
console.log(
|
|
387
|
+
console.log(chalk2.red(" \u2717"), `Failed to get device code: ${res.status} ${text}`);
|
|
73
388
|
process.exit(1);
|
|
74
389
|
}
|
|
75
390
|
const data = await res.json();
|
|
@@ -79,26 +394,26 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
79
394
|
supabaseAnonKey = data.supabase_anon_key;
|
|
80
395
|
} catch (err) {
|
|
81
396
|
console.log(
|
|
82
|
-
|
|
397
|
+
chalk2.red(" \u2717"),
|
|
83
398
|
`Could not reach ${appUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
84
399
|
);
|
|
85
400
|
process.exit(1);
|
|
86
401
|
}
|
|
87
402
|
console.log();
|
|
88
|
-
console.log(
|
|
89
|
-
console.log(
|
|
90
|
-
console.log(
|
|
91
|
-
console.log(
|
|
92
|
-
console.log(
|
|
403
|
+
console.log(chalk2.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`));
|
|
404
|
+
console.log(chalk2.bold.cyan(` \u2502 \u2502`));
|
|
405
|
+
console.log(chalk2.bold.cyan(` \u2502 Code: ${chalk2.white.bold(code)} \u2502`));
|
|
406
|
+
console.log(chalk2.bold.cyan(` \u2502 \u2502`));
|
|
407
|
+
console.log(chalk2.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`));
|
|
93
408
|
console.log();
|
|
94
409
|
const linkUrl = `${appUrl}/link?code=${code}`;
|
|
95
|
-
console.log(
|
|
410
|
+
console.log(chalk2.dim(" Opening browser to:"), chalk2.underline(linkUrl));
|
|
96
411
|
try {
|
|
97
412
|
const open = (await import("open")).default;
|
|
98
413
|
await open(linkUrl);
|
|
99
414
|
} catch {
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
415
|
+
console.log(chalk2.yellow(" !"), "Could not open browser automatically.");
|
|
416
|
+
console.log(chalk2.dim(" Open this URL manually:"), chalk2.underline(linkUrl));
|
|
102
417
|
}
|
|
103
418
|
console.log();
|
|
104
419
|
const dots = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -110,11 +425,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
110
425
|
const expiresTime = new Date(expiresAt).getTime();
|
|
111
426
|
while (!approved) {
|
|
112
427
|
if (Date.now() > expiresTime) {
|
|
113
|
-
console.log(
|
|
428
|
+
console.log(chalk2.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
114
429
|
process.exit(1);
|
|
115
430
|
}
|
|
116
431
|
process.stdout.write(
|
|
117
|
-
`\r ${
|
|
432
|
+
`\r ${chalk2.cyan(dots[dotIndex % dots.length])} Waiting for approval...`
|
|
118
433
|
);
|
|
119
434
|
dotIndex++;
|
|
120
435
|
await sleep(2e3);
|
|
@@ -132,16 +447,16 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
132
447
|
userId = data.user_id || "";
|
|
133
448
|
approved = true;
|
|
134
449
|
} else if (data.status === "expired") {
|
|
135
|
-
console.log(
|
|
450
|
+
console.log(chalk2.red("\n \u2717"), "Device code expired. Please run `tenux login` again.");
|
|
136
451
|
process.exit(1);
|
|
137
452
|
}
|
|
138
453
|
} catch {
|
|
139
454
|
}
|
|
140
455
|
}
|
|
141
456
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
142
|
-
console.log(
|
|
457
|
+
console.log(chalk2.green(" \u2713"), "Device approved!");
|
|
143
458
|
console.log();
|
|
144
|
-
console.log(
|
|
459
|
+
console.log(chalk2.dim(" Exchanging token for session..."));
|
|
145
460
|
let accessToken = "";
|
|
146
461
|
let refreshToken = "";
|
|
147
462
|
try {
|
|
@@ -153,11 +468,11 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
153
468
|
type: "magiclink"
|
|
154
469
|
});
|
|
155
470
|
if (error) {
|
|
156
|
-
console.log(
|
|
471
|
+
console.log(chalk2.red(" \u2717"), `Auth failed: ${error.message}`);
|
|
157
472
|
process.exit(1);
|
|
158
473
|
}
|
|
159
474
|
if (!session.session) {
|
|
160
|
-
console.log(
|
|
475
|
+
console.log(chalk2.red(" \u2717"), "No session returned from auth exchange.");
|
|
161
476
|
process.exit(1);
|
|
162
477
|
}
|
|
163
478
|
accessToken = session.session.access_token;
|
|
@@ -165,12 +480,12 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
165
480
|
userId = session.session.user?.id || userId;
|
|
166
481
|
} catch (err) {
|
|
167
482
|
console.log(
|
|
168
|
-
|
|
483
|
+
chalk2.red(" \u2717"),
|
|
169
484
|
`Token exchange failed: ${err instanceof Error ? err.message : String(err)}`
|
|
170
485
|
);
|
|
171
486
|
process.exit(1);
|
|
172
487
|
}
|
|
173
|
-
console.log(
|
|
488
|
+
console.log(chalk2.green(" \u2713"), "Session established.");
|
|
174
489
|
const config = {
|
|
175
490
|
appUrl,
|
|
176
491
|
supabaseUrl,
|
|
@@ -180,69 +495,69 @@ program.command("login").description("Authenticate with Tenux via browser-based
|
|
|
180
495
|
deviceId,
|
|
181
496
|
deviceName,
|
|
182
497
|
userId,
|
|
183
|
-
projectsDir:
|
|
498
|
+
projectsDir: join2(homedir(), ".tenux", "projects")
|
|
184
499
|
};
|
|
185
500
|
saveConfig(config);
|
|
186
|
-
console.log(
|
|
501
|
+
console.log(chalk2.green(" \u2713"), "Config saved to", chalk2.dim(getConfigPath()));
|
|
187
502
|
console.log();
|
|
188
503
|
const claude = detectClaudeCode();
|
|
189
504
|
if (claude.installed) {
|
|
190
|
-
console.log(
|
|
505
|
+
console.log(chalk2.green(" \u2713"), "Claude Code detected");
|
|
191
506
|
if (claude.authenticated) {
|
|
192
|
-
console.log(
|
|
507
|
+
console.log(chalk2.green(" \u2713"), "Claude Code authenticated (~/.claude exists)");
|
|
193
508
|
} else {
|
|
194
509
|
console.log(
|
|
195
|
-
|
|
510
|
+
chalk2.yellow(" !"),
|
|
196
511
|
"Claude Code found but ~/.claude not detected.",
|
|
197
|
-
|
|
512
|
+
chalk2.dim("Run `claude login` to authenticate.")
|
|
198
513
|
);
|
|
199
514
|
}
|
|
200
515
|
} else {
|
|
201
|
-
console.log(
|
|
516
|
+
console.log(chalk2.yellow(" !"), "Claude Code not found.");
|
|
202
517
|
console.log(
|
|
203
|
-
|
|
204
|
-
|
|
518
|
+
chalk2.dim(" Install it:"),
|
|
519
|
+
chalk2.underline("https://docs.anthropic.com/en/docs/claude-code")
|
|
205
520
|
);
|
|
206
521
|
console.log(
|
|
207
|
-
|
|
208
|
-
|
|
522
|
+
chalk2.dim(" Then run:"),
|
|
523
|
+
chalk2.cyan("claude login")
|
|
209
524
|
);
|
|
210
525
|
}
|
|
211
526
|
console.log();
|
|
212
|
-
console.log(
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(
|
|
215
|
-
console.log(
|
|
527
|
+
console.log(chalk2.dim(" Device:"), deviceName);
|
|
528
|
+
console.log(chalk2.dim(" Device ID:"), deviceId);
|
|
529
|
+
console.log(chalk2.dim(" User ID:"), userId);
|
|
530
|
+
console.log(chalk2.dim(" Projects:"), config.projectsDir);
|
|
216
531
|
console.log();
|
|
217
532
|
console.log(
|
|
218
|
-
|
|
219
|
-
|
|
533
|
+
chalk2.dim(" Next: start the agent:"),
|
|
534
|
+
chalk2.cyan("tenux start")
|
|
220
535
|
);
|
|
221
536
|
console.log();
|
|
222
537
|
});
|
|
223
538
|
program.command("start").description("Start the agent and listen for commands").action(async () => {
|
|
224
539
|
if (!configExists()) {
|
|
225
|
-
console.log(
|
|
540
|
+
console.log(chalk2.red("\u2717"), "Not logged in. Run `tenux login` first.");
|
|
226
541
|
process.exit(1);
|
|
227
542
|
}
|
|
228
543
|
const claude = detectClaudeCode();
|
|
229
544
|
if (!claude.installed) {
|
|
230
|
-
console.log(
|
|
231
|
-
console.log(
|
|
232
|
-
console.log(
|
|
545
|
+
console.log(chalk2.red("\u2717"), "Claude Code not found.");
|
|
546
|
+
console.log(chalk2.dim(" Install it:"), chalk2.cyan("npm i -g @anthropic-ai/claude-code"));
|
|
547
|
+
console.log(chalk2.dim(" Then run:"), chalk2.cyan("claude login"));
|
|
233
548
|
process.exit(1);
|
|
234
549
|
}
|
|
235
550
|
const config = loadConfig();
|
|
236
|
-
console.log(
|
|
237
|
-
console.log(
|
|
238
|
-
console.log(
|
|
551
|
+
console.log(chalk2.bold("\n tenux"), chalk2.dim("desktop agent\n"));
|
|
552
|
+
console.log(chalk2.dim(" Device:"), config.deviceName);
|
|
553
|
+
console.log(chalk2.dim(" Projects:"), config.projectsDir);
|
|
239
554
|
console.log();
|
|
240
555
|
const supabase = getSupabase();
|
|
241
556
|
try {
|
|
242
557
|
await initSupabaseSession();
|
|
243
558
|
} catch (err) {
|
|
244
|
-
console.log(
|
|
245
|
-
console.log(
|
|
559
|
+
console.log(chalk2.red("\u2717"), `Session expired: ${err.message}`);
|
|
560
|
+
console.log(chalk2.dim(" Run `tenux login` to re-authenticate."));
|
|
246
561
|
process.exit(1);
|
|
247
562
|
}
|
|
248
563
|
await supabase.from("devices").update({
|
|
@@ -253,53 +568,53 @@ program.command("start").description("Start the agent and listen for commands").
|
|
|
253
568
|
const heartbeat = setInterval(async () => {
|
|
254
569
|
const { error } = await supabase.from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString(), is_online: true }).eq("id", config.deviceId);
|
|
255
570
|
if (error) {
|
|
256
|
-
console.log(
|
|
571
|
+
console.log(chalk2.red("\u2717"), chalk2.dim(`Heartbeat failed: ${error.message}`));
|
|
257
572
|
}
|
|
258
573
|
}, 3e4);
|
|
259
574
|
const relay = new Relay(supabase);
|
|
260
575
|
const shutdown = async () => {
|
|
261
|
-
console.log(
|
|
576
|
+
console.log(chalk2.dim("\n Shutting down..."));
|
|
262
577
|
clearInterval(heartbeat);
|
|
263
578
|
await relay.stop();
|
|
264
579
|
await supabase.from("devices").update({ is_online: false }).eq("id", config.deviceId);
|
|
265
580
|
process.exit(0);
|
|
266
581
|
};
|
|
267
|
-
relay.on("claude.query", handleClaudeQuery).on("agent.shutdown", async () => {
|
|
268
|
-
console.log(
|
|
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(chalk2.yellow("\n \u26A1 Remote shutdown requested"));
|
|
269
584
|
await shutdown();
|
|
270
585
|
});
|
|
271
586
|
await relay.start();
|
|
272
|
-
console.log(
|
|
587
|
+
console.log(chalk2.green(" \u2713"), "Agent running. Press Ctrl+C to stop.\n");
|
|
273
588
|
process.on("SIGINT", shutdown);
|
|
274
589
|
process.on("SIGTERM", shutdown);
|
|
275
590
|
});
|
|
276
591
|
program.command("status").description("Show agent configuration and status").action(() => {
|
|
277
592
|
if (!configExists()) {
|
|
278
|
-
console.log(
|
|
593
|
+
console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
279
594
|
process.exit(1);
|
|
280
595
|
}
|
|
281
596
|
const config = loadConfig();
|
|
282
597
|
const claude = detectClaudeCode();
|
|
283
|
-
console.log(
|
|
284
|
-
console.log(
|
|
285
|
-
console.log(
|
|
286
|
-
console.log(
|
|
287
|
-
console.log(
|
|
288
|
-
console.log(
|
|
289
|
-
console.log(
|
|
290
|
-
console.log(
|
|
598
|
+
console.log(chalk2.bold("\n tenux"), chalk2.dim("agent status\n"));
|
|
599
|
+
console.log(chalk2.dim(" Config:"), getConfigPath());
|
|
600
|
+
console.log(chalk2.dim(" App URL:"), config.appUrl);
|
|
601
|
+
console.log(chalk2.dim(" Device:"), config.deviceName);
|
|
602
|
+
console.log(chalk2.dim(" Device ID:"), config.deviceId);
|
|
603
|
+
console.log(chalk2.dim(" User ID:"), config.userId);
|
|
604
|
+
console.log(chalk2.dim(" Supabase:"), config.supabaseUrl);
|
|
605
|
+
console.log(chalk2.dim(" Projects:"), config.projectsDir);
|
|
291
606
|
console.log(
|
|
292
|
-
|
|
293
|
-
config.accessToken ?
|
|
607
|
+
chalk2.dim(" Auth:"),
|
|
608
|
+
config.accessToken ? chalk2.green("authenticated") : chalk2.yellow("not authenticated")
|
|
294
609
|
);
|
|
295
610
|
console.log(
|
|
296
|
-
|
|
297
|
-
claude.installed ?
|
|
611
|
+
chalk2.dim(" Claude Code:"),
|
|
612
|
+
claude.installed ? chalk2.green("installed") : chalk2.yellow("not installed")
|
|
298
613
|
);
|
|
299
614
|
if (claude.installed) {
|
|
300
615
|
console.log(
|
|
301
|
-
|
|
302
|
-
claude.authenticated ?
|
|
616
|
+
chalk2.dim(" Claude Auth:"),
|
|
617
|
+
claude.authenticated ? chalk2.green("yes (~/.claude exists)") : chalk2.yellow("not authenticated")
|
|
303
618
|
);
|
|
304
619
|
}
|
|
305
620
|
console.log();
|
|
@@ -307,7 +622,7 @@ program.command("status").description("Show agent configuration and status").act
|
|
|
307
622
|
var configCmd = program.command("config").description("Manage agent configuration");
|
|
308
623
|
configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
|
|
309
624
|
if (!configExists()) {
|
|
310
|
-
console.log(
|
|
625
|
+
console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
311
626
|
process.exit(1);
|
|
312
627
|
}
|
|
313
628
|
const keyMap = {
|
|
@@ -318,17 +633,17 @@ configCmd.command("set <key> <value>").description("Set a config value").action(
|
|
|
318
633
|
const configKey = keyMap[key];
|
|
319
634
|
if (!configKey) {
|
|
320
635
|
console.log(
|
|
321
|
-
|
|
636
|
+
chalk2.red("\u2717"),
|
|
322
637
|
`Unknown config key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
323
638
|
);
|
|
324
639
|
process.exit(1);
|
|
325
640
|
}
|
|
326
641
|
updateConfig({ [configKey]: value });
|
|
327
|
-
console.log(
|
|
642
|
+
console.log(chalk2.green("\u2713"), `Set ${key}`);
|
|
328
643
|
});
|
|
329
644
|
configCmd.command("get <key>").description("Get a config value").action((key) => {
|
|
330
645
|
if (!configExists()) {
|
|
331
|
-
console.log(
|
|
646
|
+
console.log(chalk2.red("\u2717"), "Not configured. Run `tenux login` first.");
|
|
332
647
|
process.exit(1);
|
|
333
648
|
}
|
|
334
649
|
const config = loadConfig();
|
|
@@ -343,12 +658,12 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
343
658
|
const configKey = keyMap[key];
|
|
344
659
|
if (!configKey) {
|
|
345
660
|
console.log(
|
|
346
|
-
|
|
661
|
+
chalk2.red("\u2717"),
|
|
347
662
|
`Unknown key: ${key}. Valid keys: ${Object.keys(keyMap).join(", ")}`
|
|
348
663
|
);
|
|
349
664
|
process.exit(1);
|
|
350
665
|
}
|
|
351
666
|
const val = config[configKey];
|
|
352
|
-
console.log(val ??
|
|
667
|
+
console.log(val ?? chalk2.dim("(not set)"));
|
|
353
668
|
});
|
|
354
669
|
program.parse();
|
package/dist/index.js
CHANGED