lunel-cli 0.1.75 → 0.1.77
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/index.js +209 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,7 +24,25 @@ const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
|
|
|
24
24
|
"https://raw.githubusercontent.com/ssbharambe-m/pty-releases/refs/heads/main/info.json";
|
|
25
25
|
const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
|
|
26
26
|
// Root directory - sandbox all file operations to this
|
|
27
|
-
const ROOT_DIR =
|
|
27
|
+
const ROOT_DIR = (() => {
|
|
28
|
+
try {
|
|
29
|
+
return fssync.realpathSync(process.cwd());
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return process.cwd();
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
const CLI_CONFIG_PATH = (() => {
|
|
36
|
+
if (process.platform === "darwin") {
|
|
37
|
+
return path.join(os.homedir(), "Library", "Application Support", "lunel", "config.json");
|
|
38
|
+
}
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
41
|
+
return path.join(appData, "lunel", "config.json");
|
|
42
|
+
}
|
|
43
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
44
|
+
return path.join(xdgConfig, "lunel", "config.json");
|
|
45
|
+
})();
|
|
28
46
|
// Terminal sessions (managed by Rust PTY binary)
|
|
29
47
|
const terminals = new Set();
|
|
30
48
|
// PTY binary process
|
|
@@ -242,6 +260,7 @@ function parseExtraPortsFromArgs(args) {
|
|
|
242
260
|
}
|
|
243
261
|
const EXTRA_PORTS = parseExtraPortsFromArgs(CLI_ARGS);
|
|
244
262
|
const SCAN_PORTS = Array.from(new Set([...DEV_PORTS, ...EXTRA_PORTS])).sort((a, b) => a - b);
|
|
263
|
+
const RUN_ABCD_MODE = CLI_ARGS.includes("--run-abcd");
|
|
245
264
|
function samePortSet(a, b) {
|
|
246
265
|
if (a.length !== b.length)
|
|
247
266
|
return false;
|
|
@@ -312,6 +331,104 @@ function assertSafePath(requestedPath) {
|
|
|
312
331
|
}
|
|
313
332
|
return safePath;
|
|
314
333
|
}
|
|
334
|
+
function generatePersistentSecret(length) {
|
|
335
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
336
|
+
const bytes = crypto.randomBytes(length);
|
|
337
|
+
let out = "";
|
|
338
|
+
for (let i = 0; i < length; i++) {
|
|
339
|
+
out += alphabet[bytes[i] % alphabet.length];
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function normalizePairingRoot(input) {
|
|
344
|
+
try {
|
|
345
|
+
return fssync.realpathSync(input);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return path.resolve(input);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function readCliConfig() {
|
|
352
|
+
try {
|
|
353
|
+
const raw = await fs.readFile(CLI_CONFIG_PATH, "utf-8");
|
|
354
|
+
const parsed = JSON.parse(raw);
|
|
355
|
+
const pairings = Array.isArray(parsed.pairings)
|
|
356
|
+
? parsed.pairings
|
|
357
|
+
.filter((entry) => {
|
|
358
|
+
return Boolean(entry &&
|
|
359
|
+
typeof entry.secret === "string" &&
|
|
360
|
+
typeof entry.root === "string" &&
|
|
361
|
+
typeof entry.hostname === "string" &&
|
|
362
|
+
typeof entry.phoneId === "string" &&
|
|
363
|
+
typeof entry.pairedAt === "number" &&
|
|
364
|
+
typeof entry.lastUsedAt === "number");
|
|
365
|
+
})
|
|
366
|
+
.map((entry) => ({
|
|
367
|
+
...entry,
|
|
368
|
+
root: normalizePairingRoot(entry.root),
|
|
369
|
+
}))
|
|
370
|
+
: [];
|
|
371
|
+
return {
|
|
372
|
+
version: 1,
|
|
373
|
+
deviceId: typeof parsed.deviceId === "string" && parsed.deviceId ? parsed.deviceId : generatePersistentSecret(32),
|
|
374
|
+
pairings,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return {
|
|
379
|
+
version: 1,
|
|
380
|
+
deviceId: generatePersistentSecret(32),
|
|
381
|
+
pairings: [],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function writeCliConfig(config) {
|
|
386
|
+
await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
|
|
387
|
+
await fs.writeFile(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
388
|
+
}
|
|
389
|
+
let cliConfigPromise = null;
|
|
390
|
+
async function getCliConfig() {
|
|
391
|
+
if (!cliConfigPromise) {
|
|
392
|
+
cliConfigPromise = readCliConfig();
|
|
393
|
+
}
|
|
394
|
+
return await cliConfigPromise;
|
|
395
|
+
}
|
|
396
|
+
async function persistCliConfig(mutator) {
|
|
397
|
+
const current = await getCliConfig();
|
|
398
|
+
const next = mutator({
|
|
399
|
+
version: 1,
|
|
400
|
+
deviceId: current.deviceId,
|
|
401
|
+
pairings: [...current.pairings],
|
|
402
|
+
});
|
|
403
|
+
cliConfigPromise = Promise.resolve(next);
|
|
404
|
+
await writeCliConfig(next);
|
|
405
|
+
return next;
|
|
406
|
+
}
|
|
407
|
+
async function getSavedPairingForRoot(root) {
|
|
408
|
+
const normalizedRoot = normalizePairingRoot(root);
|
|
409
|
+
const config = await getCliConfig();
|
|
410
|
+
const matches = config.pairings
|
|
411
|
+
.filter((entry) => entry.root === normalizedRoot)
|
|
412
|
+
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
413
|
+
return matches[0] || null;
|
|
414
|
+
}
|
|
415
|
+
async function savePairing(pairing) {
|
|
416
|
+
await persistCliConfig((config) => {
|
|
417
|
+
const deduped = config.pairings.filter((entry) => entry.secret !== pairing.secret);
|
|
418
|
+
deduped.push(pairing);
|
|
419
|
+
deduped.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
420
|
+
return {
|
|
421
|
+
...config,
|
|
422
|
+
pairings: deduped.slice(0, 50),
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async function removePairing(secret) {
|
|
427
|
+
await persistCliConfig((config) => ({
|
|
428
|
+
...config,
|
|
429
|
+
pairings: config.pairings.filter((entry) => entry.secret !== secret),
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
315
432
|
// ============================================================================
|
|
316
433
|
// File System Handlers
|
|
317
434
|
// ============================================================================
|
|
@@ -2205,6 +2322,14 @@ async function processMessage(message) {
|
|
|
2205
2322
|
case "ping":
|
|
2206
2323
|
result = handleSystemPing();
|
|
2207
2324
|
break;
|
|
2325
|
+
case "pairDevice": {
|
|
2326
|
+
const phoneId = typeof payload.phoneId === "string" ? payload.phoneId.trim() : "";
|
|
2327
|
+
if (!phoneId) {
|
|
2328
|
+
throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
|
|
2329
|
+
}
|
|
2330
|
+
result = { ...(await registerPersistentPairing(phoneId)) };
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2208
2333
|
default:
|
|
2209
2334
|
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
2210
2335
|
}
|
|
@@ -2526,10 +2651,11 @@ function normalizeGatewayUrl(input) {
|
|
|
2526
2651
|
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
|
|
2527
2652
|
return `${parsed.protocol}//${parsed.host}${path}`;
|
|
2528
2653
|
}
|
|
2529
|
-
async function createSessionFromManager() {
|
|
2654
|
+
async function createSessionFromManager(opts = {}) {
|
|
2530
2655
|
const response = await fetch(`${MANAGER_URL}/v1/session`, {
|
|
2531
2656
|
method: "POST",
|
|
2532
2657
|
headers: { "Content-Type": "application/json" },
|
|
2658
|
+
body: JSON.stringify(opts.requestedCode ? { requestedCode: opts.requestedCode } : {}),
|
|
2533
2659
|
});
|
|
2534
2660
|
if (!response.ok) {
|
|
2535
2661
|
throw new Error(`Failed to create session from manager: ${response.status}`);
|
|
@@ -2580,7 +2706,7 @@ async function resolveSessionByResumeToken(resumeToken) {
|
|
|
2580
2706
|
}
|
|
2581
2707
|
return {
|
|
2582
2708
|
sessionId: snapshot.sessionId || "",
|
|
2583
|
-
code: snapshot.code,
|
|
2709
|
+
code: snapshot.code || "",
|
|
2584
2710
|
password: snapshot.resumeToken,
|
|
2585
2711
|
primary: snapshot.primary,
|
|
2586
2712
|
backup: snapshot.backup ?? null,
|
|
@@ -2588,6 +2714,52 @@ async function resolveSessionByResumeToken(resumeToken) {
|
|
|
2588
2714
|
expiresAt: snapshot.expiresAt,
|
|
2589
2715
|
};
|
|
2590
2716
|
}
|
|
2717
|
+
async function registerPersistentPairing(phoneId) {
|
|
2718
|
+
if (!currentSessionPassword) {
|
|
2719
|
+
throw new Error("No active session secret available for pairing");
|
|
2720
|
+
}
|
|
2721
|
+
const config = await getCliConfig();
|
|
2722
|
+
const response = await fetch(`${MANAGER_URL}/v1/pairings/register`, {
|
|
2723
|
+
method: "POST",
|
|
2724
|
+
headers: { "Content-Type": "application/json" },
|
|
2725
|
+
body: JSON.stringify({
|
|
2726
|
+
activeSecret: currentSessionPassword,
|
|
2727
|
+
phoneId,
|
|
2728
|
+
pcId: config.deviceId,
|
|
2729
|
+
root: ROOT_DIR,
|
|
2730
|
+
hostname: os.hostname(),
|
|
2731
|
+
}),
|
|
2732
|
+
});
|
|
2733
|
+
if (!response.ok) {
|
|
2734
|
+
throw new Error(`Failed to register persistent pairing (${response.status})`);
|
|
2735
|
+
}
|
|
2736
|
+
const payload = await response.json();
|
|
2737
|
+
if (typeof payload.secret !== "string" ||
|
|
2738
|
+
typeof payload.hostname !== "string" ||
|
|
2739
|
+
typeof payload.root !== "string" ||
|
|
2740
|
+
typeof payload.phoneId !== "string" ||
|
|
2741
|
+
typeof payload.pairedAt !== "number" ||
|
|
2742
|
+
typeof payload.lastUsedAt !== "number") {
|
|
2743
|
+
throw new Error("Manager returned invalid pairing response");
|
|
2744
|
+
}
|
|
2745
|
+
const normalized = {
|
|
2746
|
+
secret: payload.secret,
|
|
2747
|
+
hostname: payload.hostname,
|
|
2748
|
+
root: normalizePairingRoot(payload.root),
|
|
2749
|
+
phoneId: payload.phoneId,
|
|
2750
|
+
pairedAt: payload.pairedAt,
|
|
2751
|
+
lastUsedAt: payload.lastUsedAt,
|
|
2752
|
+
};
|
|
2753
|
+
await savePairing({
|
|
2754
|
+
secret: normalized.secret,
|
|
2755
|
+
root: normalized.root,
|
|
2756
|
+
hostname: normalized.hostname,
|
|
2757
|
+
phoneId: normalized.phoneId,
|
|
2758
|
+
pairedAt: normalized.pairedAt,
|
|
2759
|
+
lastUsedAt: normalized.lastUsedAt,
|
|
2760
|
+
});
|
|
2761
|
+
return normalized;
|
|
2762
|
+
}
|
|
2591
2763
|
function displayQR(primaryGateway, backupGateway, code) {
|
|
2592
2764
|
console.log("\n");
|
|
2593
2765
|
qrcode.generate(code, { small: true }, (qr) => {
|
|
@@ -2901,7 +3073,11 @@ async function main() {
|
|
|
2901
3073
|
if (cloudJobConfig?.session_code) {
|
|
2902
3074
|
console.log(`[cloud] Running in VM mode (sandbox: ${cloudJobConfig.sandbox_id})`);
|
|
2903
3075
|
}
|
|
3076
|
+
if (RUN_ABCD_MODE) {
|
|
3077
|
+
console.log("[review] Running in pinned review session mode (code: abcd)");
|
|
3078
|
+
}
|
|
2904
3079
|
try {
|
|
3080
|
+
await getCliConfig();
|
|
2905
3081
|
console.log("Checking PTY runtime...");
|
|
2906
3082
|
await ensurePtyBinaryReady();
|
|
2907
3083
|
console.log("PTY runtime ready.\n");
|
|
@@ -2921,15 +3097,41 @@ async function main() {
|
|
|
2921
3097
|
}
|
|
2922
3098
|
});
|
|
2923
3099
|
let session;
|
|
2924
|
-
if (
|
|
3100
|
+
if (RUN_ABCD_MODE) {
|
|
3101
|
+
session = await createSessionFromManager({ requestedCode: "abcd" });
|
|
3102
|
+
console.log(`[review] Attached to session ${session.code} via ${session.primary}`);
|
|
3103
|
+
}
|
|
3104
|
+
else if (cloudJobConfig?.session_code) {
|
|
2925
3105
|
// Cloud mode: connect to an existing session assigned by the manager
|
|
2926
3106
|
session = await connectToCloudSession(cloudJobConfig.session_code);
|
|
2927
3107
|
console.log(`[cloud] Connected to session ${session.code} via ${session.primary}`);
|
|
2928
3108
|
}
|
|
2929
3109
|
else {
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3110
|
+
const savedPairing = await getSavedPairingForRoot(ROOT_DIR);
|
|
3111
|
+
if (savedPairing) {
|
|
3112
|
+
try {
|
|
3113
|
+
const resolved = await resolveSessionByResumeToken(savedPairing.secret);
|
|
3114
|
+
if (resolved) {
|
|
3115
|
+
session = resolved;
|
|
3116
|
+
console.log(`[pairing] Waiting on saved connection for ${savedPairing.hostname}`);
|
|
3117
|
+
}
|
|
3118
|
+
else {
|
|
3119
|
+
await removePairing(savedPairing.secret);
|
|
3120
|
+
session = await createSessionFromManager();
|
|
3121
|
+
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
catch {
|
|
3125
|
+
await removePairing(savedPairing.secret);
|
|
3126
|
+
session = await createSessionFromManager();
|
|
3127
|
+
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
else {
|
|
3131
|
+
// Public mode: create a new session and show QR code
|
|
3132
|
+
session = await createSessionFromManager();
|
|
3133
|
+
displayQR(normalizeGatewayUrl(session.primary), session.backup ? normalizeGatewayUrl(session.backup) : null, session.code);
|
|
3134
|
+
}
|
|
2933
3135
|
}
|
|
2934
3136
|
currentPrimaryGateway = normalizeGatewayUrl(session.primary);
|
|
2935
3137
|
currentBackupGateway = session.backup ? normalizeGatewayUrl(session.backup) : null;
|