@cli-remote/local 0.3.4 → 0.4.0
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.cjs +499 -23
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2263,7 +2263,7 @@ var require_websocket = __commonJS({
|
|
|
2263
2263
|
var EventEmitter = require("events");
|
|
2264
2264
|
var https = require("https");
|
|
2265
2265
|
var http = require("http");
|
|
2266
|
-
var
|
|
2266
|
+
var net2 = require("net");
|
|
2267
2267
|
var tls = require("tls");
|
|
2268
2268
|
var { randomBytes: randomBytes3, createHash: createHash2 } = require("crypto");
|
|
2269
2269
|
var { Duplex, Readable } = require("stream");
|
|
@@ -3007,12 +3007,12 @@ var require_websocket = __commonJS({
|
|
|
3007
3007
|
}
|
|
3008
3008
|
function netConnect(options) {
|
|
3009
3009
|
options.path = options.socketPath;
|
|
3010
|
-
return
|
|
3010
|
+
return net2.connect(options);
|
|
3011
3011
|
}
|
|
3012
3012
|
function tlsConnect(options) {
|
|
3013
3013
|
options.path = void 0;
|
|
3014
3014
|
if (!options.servername && options.servername !== "") {
|
|
3015
|
-
options.servername =
|
|
3015
|
+
options.servername = net2.isIP(options.host) ? "" : options.host;
|
|
3016
3016
|
}
|
|
3017
3017
|
return tls.connect(options);
|
|
3018
3018
|
}
|
|
@@ -9940,7 +9940,7 @@ var require_lib = __commonJS({
|
|
|
9940
9940
|
|
|
9941
9941
|
// src/index.ts
|
|
9942
9942
|
var import_node_fs5 = require("node:fs");
|
|
9943
|
-
var
|
|
9943
|
+
var import_node_child_process5 = require("node:child_process");
|
|
9944
9944
|
var import_node_os8 = require("node:os");
|
|
9945
9945
|
var import_node_path8 = require("node:path");
|
|
9946
9946
|
var import_node_url = require("node:url");
|
|
@@ -9957,7 +9957,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
9957
9957
|
var wrapper_default = import_websocket.default;
|
|
9958
9958
|
|
|
9959
9959
|
// src/client.ts
|
|
9960
|
-
var
|
|
9960
|
+
var import_node_child_process4 = require("node:child_process");
|
|
9961
9961
|
var import_node_readline = require("node:readline");
|
|
9962
9962
|
var import_node_fs4 = require("node:fs");
|
|
9963
9963
|
var import_node_os7 = require("node:os");
|
|
@@ -10031,6 +10031,8 @@ function resolveCommand(cmd) {
|
|
|
10031
10031
|
return { file: "kimi", args: [] };
|
|
10032
10032
|
case "pi":
|
|
10033
10033
|
return { file: "pi", args: [] };
|
|
10034
|
+
case "desktop":
|
|
10035
|
+
throw new Error("desktop sessions are not PTY sessions");
|
|
10034
10036
|
default:
|
|
10035
10037
|
return { file: cmd, args: [] };
|
|
10036
10038
|
}
|
|
@@ -10214,6 +10216,12 @@ var SessionLogger = class {
|
|
|
10214
10216
|
if (this.closed || this.mode === "off") return;
|
|
10215
10217
|
this.write({ ts: Date.now(), event: "forced", cmd });
|
|
10216
10218
|
}
|
|
10219
|
+
/** Desktop display/workspace switch metadata (v0.8). Content is never
|
|
10220
|
+
* logged — this records only WHICH screen/desktop was switched to. */
|
|
10221
|
+
logSwitch(kind, value) {
|
|
10222
|
+
if (this.closed || this.mode === "off") return;
|
|
10223
|
+
this.write({ ts: Date.now(), event: "switch", kind, value });
|
|
10224
|
+
}
|
|
10217
10225
|
logExit(code) {
|
|
10218
10226
|
if (this.closed || this.mode === "off") return;
|
|
10219
10227
|
this.write({ ts: Date.now(), event: "exit", code });
|
|
@@ -10421,6 +10429,347 @@ async function exists(p) {
|
|
|
10421
10429
|
}
|
|
10422
10430
|
}
|
|
10423
10431
|
|
|
10432
|
+
// src/desktop-manager.ts
|
|
10433
|
+
var import_node_child_process = require("node:child_process");
|
|
10434
|
+
var import_node_util = require("node:util");
|
|
10435
|
+
var import_node_net = __toESM(require("node:net"), 1);
|
|
10436
|
+
var execFileP = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
10437
|
+
var X11VNC_START_TIMEOUT_MS = 8e3;
|
|
10438
|
+
var PAUSE_POLL_MS = 25;
|
|
10439
|
+
var EXEC_TIMEOUT_MS = 5e3;
|
|
10440
|
+
var DesktopManager = class {
|
|
10441
|
+
constructor(hooks, config, platform2 = process.platform, execImpl, spawnImpl) {
|
|
10442
|
+
this.hooks = hooks;
|
|
10443
|
+
this.config = config;
|
|
10444
|
+
this.platform = platform2;
|
|
10445
|
+
this.exec = execImpl ?? ((file, args, timeoutMs = EXEC_TIMEOUT_MS) => execFileP(file, args, { timeout: timeoutMs }).then((r) => String(r.stdout ?? "")));
|
|
10446
|
+
this.spawnChildFn = spawnImpl ?? ((file, args) => {
|
|
10447
|
+
const child = (0, import_node_child_process.spawn)(file, args, { stdio: "ignore" });
|
|
10448
|
+
child.unref();
|
|
10449
|
+
return child;
|
|
10450
|
+
});
|
|
10451
|
+
}
|
|
10452
|
+
sessions = /* @__PURE__ */ new Map();
|
|
10453
|
+
exec;
|
|
10454
|
+
spawnChildFn;
|
|
10455
|
+
/** Human-readable reason when the machine cannot serve desktop sessions. */
|
|
10456
|
+
unsupportedReason() {
|
|
10457
|
+
if (!this.config.enabled) {
|
|
10458
|
+
return 'desktop disabled on this device (config.json: {"desktop": {"enabled": true}})';
|
|
10459
|
+
}
|
|
10460
|
+
if (this.platform === "win32") {
|
|
10461
|
+
return "desktop sessions are not supported on Windows yet";
|
|
10462
|
+
}
|
|
10463
|
+
return null;
|
|
10464
|
+
}
|
|
10465
|
+
// -------------------------------------------------------------------------
|
|
10466
|
+
// Enumeration (desktop-query)
|
|
10467
|
+
// -------------------------------------------------------------------------
|
|
10468
|
+
/** Probes displays/workspaces. Degrades gracefully — missing tools yield
|
|
10469
|
+
* empty/unknown values, never throws (web always gets an answer). */
|
|
10470
|
+
async queryInfo() {
|
|
10471
|
+
if (this.platform !== "linux") {
|
|
10472
|
+
return { displays: [], workspaces: null, currentWorkspace: null, canPickDisplay: false };
|
|
10473
|
+
}
|
|
10474
|
+
const [heads, ws] = await Promise.all([this.listHeads().catch(() => []), this.workspaceState().catch(() => null)]);
|
|
10475
|
+
return {
|
|
10476
|
+
displays: heads.map((h) => ({ name: h.name, width: h.w, height: h.h })),
|
|
10477
|
+
workspaces: ws?.count ?? null,
|
|
10478
|
+
currentWorkspace: ws?.current ?? null,
|
|
10479
|
+
canPickDisplay: heads.length > 0
|
|
10480
|
+
};
|
|
10481
|
+
}
|
|
10482
|
+
/** `xrandr --query` connected heads (name + geometry). */
|
|
10483
|
+
async listHeads() {
|
|
10484
|
+
const out = await this.exec("xrandr", ["--query"]);
|
|
10485
|
+
const heads = [];
|
|
10486
|
+
for (const line of out.split("\n")) {
|
|
10487
|
+
const m = /^(\S+) connected (?:primary )?(\d+)x(\d+)\+(\d+)\+(\d+)\s/.exec(line);
|
|
10488
|
+
if (m) heads.push({ name: m[1], w: Number(m[2]), h: Number(m[3]), x: Number(m[4]), y: Number(m[5]) });
|
|
10489
|
+
}
|
|
10490
|
+
return heads;
|
|
10491
|
+
}
|
|
10492
|
+
/** `wmctrl -d` → { count, current }. The current line carries ` * `. */
|
|
10493
|
+
async workspaceState() {
|
|
10494
|
+
const out = await this.exec("wmctrl", ["-d"]);
|
|
10495
|
+
const lines = out.split("\n").filter((l) => l.trim());
|
|
10496
|
+
if (lines.length === 0) return null;
|
|
10497
|
+
const current = lines.findIndex((l) => /(^|\s)\*(\s|$)/.test(l));
|
|
10498
|
+
return { count: lines.length, current: current >= 0 ? current : 0 };
|
|
10499
|
+
}
|
|
10500
|
+
// -------------------------------------------------------------------------
|
|
10501
|
+
// Session lifecycle
|
|
10502
|
+
// -------------------------------------------------------------------------
|
|
10503
|
+
async spawn(sessionId, opts = {}) {
|
|
10504
|
+
if (this.sessions.has(sessionId)) {
|
|
10505
|
+
throw new Error(`session ${sessionId} already exists`);
|
|
10506
|
+
}
|
|
10507
|
+
const { vncHost: host, vncPort: port } = this.config;
|
|
10508
|
+
if (opts.workspace !== void 0 && this.platform !== "linux") {
|
|
10509
|
+
throw new Error("workspace selection is not supported on this platform (session-internal prev/next still works on macOS)");
|
|
10510
|
+
}
|
|
10511
|
+
if (opts.display !== void 0 && this.platform !== "linux") {
|
|
10512
|
+
throw new Error("display selection is not supported on this platform");
|
|
10513
|
+
}
|
|
10514
|
+
if (opts.workspace !== void 0) {
|
|
10515
|
+
await this.switchWorkspaceAbsolute(opts.workspace);
|
|
10516
|
+
this.hooks.log(`desktop ${sessionId}: workspace \u2192 ${opts.workspace}`);
|
|
10517
|
+
}
|
|
10518
|
+
let tcp = await probeConnect(host, port, 750);
|
|
10519
|
+
let ownChild = null;
|
|
10520
|
+
let clip;
|
|
10521
|
+
if (opts.display !== void 0) {
|
|
10522
|
+
const heads = await this.listHeads().catch(() => []);
|
|
10523
|
+
clip = heads[opts.display];
|
|
10524
|
+
if (!clip) {
|
|
10525
|
+
throw new Error(`display ${opts.display} not found (${heads.length} connected)`);
|
|
10526
|
+
}
|
|
10527
|
+
}
|
|
10528
|
+
if (!tcp && this.platform === "linux") {
|
|
10529
|
+
const x11vnc = await this.startX11vnc(port, clip);
|
|
10530
|
+
ownChild = x11vnc;
|
|
10531
|
+
tcp = await probeConnect(host, port, X11VNC_START_TIMEOUT_MS);
|
|
10532
|
+
if (!tcp) {
|
|
10533
|
+
try {
|
|
10534
|
+
x11vnc?.kill("SIGTERM");
|
|
10535
|
+
} catch {
|
|
10536
|
+
}
|
|
10537
|
+
throw new Error(
|
|
10538
|
+
`no VNC server on ${host}:${port} and x11vnc did not come up (is a desktop/X11 session running?)`
|
|
10539
|
+
);
|
|
10540
|
+
}
|
|
10541
|
+
if (x11vnc) {
|
|
10542
|
+
x11vnc.once("exit", () => {
|
|
10543
|
+
const s = this.sessions.get(sessionId);
|
|
10544
|
+
if (s && s.x11vnc === x11vnc) this.end(sessionId, 1, "x11vnc exited");
|
|
10545
|
+
});
|
|
10546
|
+
}
|
|
10547
|
+
this.hooks.log(
|
|
10548
|
+
`desktop ${sessionId}: started x11vnc on ${host}:${port}` + (clip ? ` clipped to ${clip.w}x${clip.h}+${clip.x}+${clip.y}` : "") + ` (display ${this.config.display}, loopback only)`
|
|
10549
|
+
);
|
|
10550
|
+
}
|
|
10551
|
+
if (!tcp) {
|
|
10552
|
+
throw new Error(
|
|
10553
|
+
this.platform === "darwin" ? `no VNC server on ${host}:${port} \u2014 enable \u300C\u5C4F\u5E55\u5171\u4EAB\u300Din macOS System Settings, or set desktop.vncHost/vncPort` : `no VNC server on ${host}:${port} (auto-start is only supported on Linux X11)`
|
|
10554
|
+
);
|
|
10555
|
+
}
|
|
10556
|
+
if (clip && !ownChild) {
|
|
10557
|
+
throw new Error("external VNC server on the port cannot serve a chosen display (stop it, or spawn with the default display)");
|
|
10558
|
+
}
|
|
10559
|
+
const session = {
|
|
10560
|
+
id: sessionId,
|
|
10561
|
+
tcp,
|
|
10562
|
+
startedAt: Date.now(),
|
|
10563
|
+
paused: false,
|
|
10564
|
+
externalServer: !ownChild,
|
|
10565
|
+
...ownChild ? { x11vnc: ownChild } : {},
|
|
10566
|
+
...clip ? { clip: { w: clip.w, h: clip.h, x: clip.x, y: clip.y } } : {}
|
|
10567
|
+
};
|
|
10568
|
+
this.sessions.set(sessionId, session);
|
|
10569
|
+
this.wireSocket(session, tcp);
|
|
10570
|
+
}
|
|
10571
|
+
async startX11vnc(port, clip) {
|
|
10572
|
+
if (this.platform !== "linux") return null;
|
|
10573
|
+
try {
|
|
10574
|
+
const args = ["-display", this.config.display, "-localhost", "-nopw", "-quiet", "-rfbport", String(port)];
|
|
10575
|
+
if (clip) args.push("-clip", `${clip.w}x${clip.h}+${clip.x}+${clip.y}`);
|
|
10576
|
+
return this.spawnChildFn("x11vnc", args);
|
|
10577
|
+
} catch {
|
|
10578
|
+
return null;
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
/** Attach data/backpressure/close handlers to a session's TCP socket. */
|
|
10582
|
+
wireSocket(session, tcp) {
|
|
10583
|
+
session.tcp = tcp;
|
|
10584
|
+
tcp.setNoDelay(true);
|
|
10585
|
+
tcp.on("data", (buf) => {
|
|
10586
|
+
this.hooks.send(session.id, new Uint8Array(buf));
|
|
10587
|
+
if (!session.paused && this.hooks.congested()) {
|
|
10588
|
+
session.paused = true;
|
|
10589
|
+
tcp.pause();
|
|
10590
|
+
session.pauseTimer = setInterval(() => {
|
|
10591
|
+
if (!this.hooks.congested()) {
|
|
10592
|
+
session.paused = false;
|
|
10593
|
+
if (session.pauseTimer) clearInterval(session.pauseTimer);
|
|
10594
|
+
session.pauseTimer = void 0;
|
|
10595
|
+
try {
|
|
10596
|
+
tcp.resume();
|
|
10597
|
+
} catch {
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
}, PAUSE_POLL_MS);
|
|
10601
|
+
}
|
|
10602
|
+
});
|
|
10603
|
+
tcp.on("error", (err) => {
|
|
10604
|
+
this.hooks.log(`desktop ${session.id}: tcp error ${err.message}`);
|
|
10605
|
+
this.end(session.id, 1, "tcp error");
|
|
10606
|
+
});
|
|
10607
|
+
tcp.on("close", () => {
|
|
10608
|
+
const s = this.sessions.get(session.id);
|
|
10609
|
+
if (s) this.end(session.id, 0, "vnc closed");
|
|
10610
|
+
});
|
|
10611
|
+
}
|
|
10612
|
+
/** VNC client bytes (RFB) from the web side → the local VNC server. */
|
|
10613
|
+
write(sessionId, data) {
|
|
10614
|
+
const s = this.sessions.get(sessionId);
|
|
10615
|
+
if (!s) throw new Error(`desktop session ${sessionId} not found`);
|
|
10616
|
+
s.tcp.write(data);
|
|
10617
|
+
}
|
|
10618
|
+
has(sessionId) {
|
|
10619
|
+
return this.sessions.has(sessionId);
|
|
10620
|
+
}
|
|
10621
|
+
list() {
|
|
10622
|
+
return [...this.sessions.values()].map((s) => ({ sessionId: s.id, startedAt: s.startedAt }));
|
|
10623
|
+
}
|
|
10624
|
+
kill(sessionId) {
|
|
10625
|
+
const s = this.sessions.get(sessionId);
|
|
10626
|
+
if (s) this.end(sessionId, 0, "killed");
|
|
10627
|
+
}
|
|
10628
|
+
/** Called whenever the upstream transport dies or the process exits —
|
|
10629
|
+
* a desktop session must NEVER outlive the tunnel (design §2 #8). */
|
|
10630
|
+
killAll() {
|
|
10631
|
+
for (const id of [...this.sessions.keys()]) {
|
|
10632
|
+
this.kill(id);
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
end(sessionId, code, why) {
|
|
10636
|
+
const s = this.sessions.get(sessionId);
|
|
10637
|
+
if (!s) return;
|
|
10638
|
+
this.sessions.delete(sessionId);
|
|
10639
|
+
if (s.pauseTimer) clearInterval(s.pauseTimer);
|
|
10640
|
+
try {
|
|
10641
|
+
s.tcp.destroy();
|
|
10642
|
+
} catch {
|
|
10643
|
+
}
|
|
10644
|
+
if (s.x11vnc) {
|
|
10645
|
+
try {
|
|
10646
|
+
s.x11vnc.kill("SIGTERM");
|
|
10647
|
+
} catch {
|
|
10648
|
+
}
|
|
10649
|
+
}
|
|
10650
|
+
this.hooks.log(`desktop ${sessionId} ended (${why})`);
|
|
10651
|
+
this.hooks.onExit(sessionId, code);
|
|
10652
|
+
}
|
|
10653
|
+
// -------------------------------------------------------------------------
|
|
10654
|
+
// Live switching (desktop-switch)
|
|
10655
|
+
// -------------------------------------------------------------------------
|
|
10656
|
+
/** Returns an error reason on failure, undefined on success. */
|
|
10657
|
+
async switch(sessionId, opts) {
|
|
10658
|
+
const s = this.sessions.get(sessionId);
|
|
10659
|
+
if (!s) return `desktop session ${sessionId} not found`;
|
|
10660
|
+
if (opts.display !== void 0) {
|
|
10661
|
+
const err = await this.switchDisplay(s, opts.display);
|
|
10662
|
+
if (err) return err;
|
|
10663
|
+
}
|
|
10664
|
+
if (opts.workspaceDelta !== void 0) {
|
|
10665
|
+
const err = await this.switchWorkspaceDelta(opts.workspaceDelta);
|
|
10666
|
+
if (err) return err;
|
|
10667
|
+
} else if (opts.workspace !== void 0) {
|
|
10668
|
+
const err = await this.switchWorkspaceAbsolute(opts.workspace);
|
|
10669
|
+
if (err) return err;
|
|
10670
|
+
}
|
|
10671
|
+
return void 0;
|
|
10672
|
+
}
|
|
10673
|
+
/** Linux: respawn OUR x11vnc with a new -clip and swap the TCP socket.
|
|
10674
|
+
* The RFB handshake restarts on the web side (viewer re-attaches). */
|
|
10675
|
+
async switchDisplay(s, display) {
|
|
10676
|
+
if (this.platform !== "linux") return "display switching is not supported on this platform";
|
|
10677
|
+
if (s.externalServer || !s.x11vnc) {
|
|
10678
|
+
return "cannot switch displays on an external VNC server (only locally started x11vnc)";
|
|
10679
|
+
}
|
|
10680
|
+
const heads = await this.listHeads().catch(() => []);
|
|
10681
|
+
const head = heads[display];
|
|
10682
|
+
if (!head) return `display ${display} not found (${heads.length} connected)`;
|
|
10683
|
+
const { vncHost: host, vncPort: port } = this.config;
|
|
10684
|
+
try {
|
|
10685
|
+
s.tcp.removeAllListeners();
|
|
10686
|
+
s.tcp.destroy();
|
|
10687
|
+
} catch {
|
|
10688
|
+
}
|
|
10689
|
+
try {
|
|
10690
|
+
s.x11vnc.kill("SIGTERM");
|
|
10691
|
+
} catch {
|
|
10692
|
+
}
|
|
10693
|
+
const child = await this.startX11vnc(port, head);
|
|
10694
|
+
const tcp = await probeConnect(host, port, X11VNC_START_TIMEOUT_MS);
|
|
10695
|
+
if (!tcp) {
|
|
10696
|
+
this.end(s.id, 1, "x11vnc restart failed during display switch");
|
|
10697
|
+
return "x11vnc did not come back up";
|
|
10698
|
+
}
|
|
10699
|
+
if (child) {
|
|
10700
|
+
child.once("exit", () => {
|
|
10701
|
+
const cur = this.sessions.get(s.id);
|
|
10702
|
+
if (cur && cur.x11vnc === child) this.end(s.id, 1, "x11vnc exited");
|
|
10703
|
+
});
|
|
10704
|
+
}
|
|
10705
|
+
s.x11vnc = child ?? void 0;
|
|
10706
|
+
s.clip = { w: head.w, h: head.h, x: head.x, y: head.y };
|
|
10707
|
+
this.wireSocket(s, tcp);
|
|
10708
|
+
this.hooks.log(`desktop ${s.id}: display \u2192 ${display} (${head.name} ${head.w}x${head.h}+${head.x}+${head.y})`);
|
|
10709
|
+
return void 0;
|
|
10710
|
+
}
|
|
10711
|
+
async switchWorkspaceAbsolute(n) {
|
|
10712
|
+
if (this.platform !== "linux") return "absolute workspace switching is not supported on this platform";
|
|
10713
|
+
try {
|
|
10714
|
+
await this.exec("wmctrl", ["-s", String(n)]);
|
|
10715
|
+
return void 0;
|
|
10716
|
+
} catch {
|
|
10717
|
+
return `wmctrl -s ${n} failed (is wmctrl installed? does workspace ${n} exist?)`;
|
|
10718
|
+
}
|
|
10719
|
+
}
|
|
10720
|
+
async switchWorkspaceDelta(delta) {
|
|
10721
|
+
if (this.platform === "linux") {
|
|
10722
|
+
const ws = await this.workspaceState().catch(() => null);
|
|
10723
|
+
if (!ws) return "cannot read current workspace (wmctrl missing?)";
|
|
10724
|
+
const target = ws.current + delta;
|
|
10725
|
+
if (target < 0 || target >= ws.count) return `no workspace ${target < 0 ? "before" : "after"} the current one`;
|
|
10726
|
+
return this.switchWorkspaceAbsolute(target);
|
|
10727
|
+
}
|
|
10728
|
+
if (this.platform === "darwin") {
|
|
10729
|
+
const keyCode = delta === 1 ? 124 : 123;
|
|
10730
|
+
try {
|
|
10731
|
+
await this.exec(
|
|
10732
|
+
"osascript",
|
|
10733
|
+
["-e", `tell application "System Events" to key code ${keyCode} using control down`],
|
|
10734
|
+
8e3
|
|
10735
|
+
);
|
|
10736
|
+
return void 0;
|
|
10737
|
+
} catch {
|
|
10738
|
+
return "System Events keystroke failed (grant Accessibility permission to the app running cli-local, or switch Spaces manually)";
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
return "workspace switching is not supported on this platform";
|
|
10742
|
+
}
|
|
10743
|
+
};
|
|
10744
|
+
function probeConnect(host, port, timeoutMs) {
|
|
10745
|
+
return new Promise((resolve2) => {
|
|
10746
|
+
const sock = import_node_net.default.connect({ host, port });
|
|
10747
|
+
let settled = false;
|
|
10748
|
+
const done = (result) => {
|
|
10749
|
+
if (settled) return;
|
|
10750
|
+
settled = true;
|
|
10751
|
+
clearTimeout(timer);
|
|
10752
|
+
if (result === null) {
|
|
10753
|
+
sock.destroy();
|
|
10754
|
+
}
|
|
10755
|
+
resolve2(result);
|
|
10756
|
+
};
|
|
10757
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
10758
|
+
sock.once("connect", () => done(sock));
|
|
10759
|
+
sock.once("error", () => done(null));
|
|
10760
|
+
});
|
|
10761
|
+
}
|
|
10762
|
+
|
|
10763
|
+
// src/config.ts
|
|
10764
|
+
function resolveDesktopConfig(cfg) {
|
|
10765
|
+
return {
|
|
10766
|
+
enabled: cfg?.enabled === true,
|
|
10767
|
+
display: cfg?.display ?? ":0",
|
|
10768
|
+
vncHost: cfg?.vncHost ?? "127.0.0.1",
|
|
10769
|
+
vncPort: cfg?.vncPort ?? 5900
|
|
10770
|
+
};
|
|
10771
|
+
}
|
|
10772
|
+
|
|
10424
10773
|
// src/gateway-api.ts
|
|
10425
10774
|
var import_node_http = require("node:http");
|
|
10426
10775
|
var import_node_fs3 = require("node:fs");
|
|
@@ -10606,7 +10955,7 @@ function startSetupCallback(timeoutMs = 3 * 6e4) {
|
|
|
10606
10955
|
}
|
|
10607
10956
|
|
|
10608
10957
|
// src/auth.ts
|
|
10609
|
-
var
|
|
10958
|
+
var import_node_child_process3 = require("node:child_process");
|
|
10610
10959
|
var import_node_crypto3 = require("node:crypto");
|
|
10611
10960
|
var import_promises3 = require("node:fs/promises");
|
|
10612
10961
|
var import_node_os6 = require("node:os");
|
|
@@ -10614,13 +10963,13 @@ var import_node_path6 = require("node:path");
|
|
|
10614
10963
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
10615
10964
|
|
|
10616
10965
|
// src/machine-id.ts
|
|
10617
|
-
var
|
|
10966
|
+
var import_node_child_process2 = require("node:child_process");
|
|
10618
10967
|
var import_node_crypto2 = require("node:crypto");
|
|
10619
10968
|
var import_promises2 = require("node:fs/promises");
|
|
10620
10969
|
var import_node_os5 = require("node:os");
|
|
10621
10970
|
var import_node_path5 = require("node:path");
|
|
10622
|
-
var
|
|
10623
|
-
var execFileAsync = (0,
|
|
10971
|
+
var import_node_util2 = require("node:util");
|
|
10972
|
+
var execFileAsync = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
10624
10973
|
var MACHINE_ID_DOMAIN = "cli-remote.machineid.v1";
|
|
10625
10974
|
function normalizeHardwareId(raw) {
|
|
10626
10975
|
return raw.trim().toLowerCase().replace(/\s+/g, " ");
|
|
@@ -10785,7 +11134,7 @@ function openBrowser(url) {
|
|
|
10785
11134
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
10786
11135
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
10787
11136
|
try {
|
|
10788
|
-
const child = (0,
|
|
11137
|
+
const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: "ignore", detached: true });
|
|
10789
11138
|
child.unref();
|
|
10790
11139
|
} catch {
|
|
10791
11140
|
}
|
|
@@ -10797,7 +11146,14 @@ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
|
|
|
10797
11146
|
throw new AuthError(`\u767B\u5F55\u5931\u8D25: ${reason ?? login.status}`);
|
|
10798
11147
|
}
|
|
10799
11148
|
const token = login.data.token;
|
|
10800
|
-
if (!token)
|
|
11149
|
+
if (!token) {
|
|
11150
|
+
if (login.data?.totpRequired) {
|
|
11151
|
+
throw new AuthError(
|
|
11152
|
+
"\u8BE5\u8D26\u53F7\u5DF2\u5F00\u542F\u4E24\u6B65\u9A8C\u8BC1\uFF0C\u8D26\u53F7\u5BC6\u7801\u65B9\u5F0F\u4E0D\u53EF\u7528\u3002\u8BF7\u6539\u7528\uFF1A\u2460\u201C\u901A\u8FC7\u7F51\u9875\u767B\u5F55\u83B7\u53D6\u201D\uFF08\u63A8\u8350\uFF09\u6216 \u2461\u624B\u52A8\u7C98\u8D34 registrationKey"
|
|
11153
|
+
);
|
|
11154
|
+
}
|
|
11155
|
+
throw new AuthError("\u670D\u52A1\u7AEF\u672A\u8FD4\u56DE\u4F1A\u8BDD token");
|
|
11156
|
+
}
|
|
10801
11157
|
const me = await fetch(`${baseUrl}/api/me`, {
|
|
10802
11158
|
headers: { authorization: `Bearer ${token}` }
|
|
10803
11159
|
});
|
|
@@ -10975,7 +11331,7 @@ function applyConfigDeprecations(cfg) {
|
|
|
10975
11331
|
function isTTY() {
|
|
10976
11332
|
return process.stdin.isTTY === true;
|
|
10977
11333
|
}
|
|
10978
|
-
function detectCommands() {
|
|
11334
|
+
function detectCommands(desktopEnabled) {
|
|
10979
11335
|
const bins = [
|
|
10980
11336
|
["claude", "claude"],
|
|
10981
11337
|
["codex", "codex"],
|
|
@@ -10986,15 +11342,16 @@ function detectCommands() {
|
|
|
10986
11342
|
for (const [cmd, bin] of bins) {
|
|
10987
11343
|
let ok = false;
|
|
10988
11344
|
if (process.platform === "win32") {
|
|
10989
|
-
ok = (0,
|
|
11345
|
+
ok = (0, import_node_child_process4.spawnSync)("where", [bin], { timeout: 5e3 }).status === 0;
|
|
10990
11346
|
} else {
|
|
10991
11347
|
const shell = process.env.SHELL ?? "/bin/bash";
|
|
10992
|
-
ok = (0,
|
|
11348
|
+
ok = (0, import_node_child_process4.spawnSync)(shell, ["-l", "-c", `command -v ${bin}`], {
|
|
10993
11349
|
timeout: 5e3
|
|
10994
11350
|
}).status === 0;
|
|
10995
11351
|
}
|
|
10996
11352
|
if (ok) found.push(cmd);
|
|
10997
11353
|
}
|
|
11354
|
+
if (desktopEnabled) found.push("desktop");
|
|
10998
11355
|
return found;
|
|
10999
11356
|
}
|
|
11000
11357
|
async function readInput(prompt) {
|
|
@@ -11120,6 +11477,20 @@ async function startLocal(opts = {}) {
|
|
|
11120
11477
|
(data) => safeSend(data),
|
|
11121
11478
|
(msg) => console.log(`[local] ${msg}`)
|
|
11122
11479
|
);
|
|
11480
|
+
const desktops = new DesktopManager(
|
|
11481
|
+
{
|
|
11482
|
+
send: (sessionId, data) => safeSend(encodeFrame({ t: "desktop-data", sessionId, data })),
|
|
11483
|
+
congested: () => ws ? ws.bufferedAmount > 4 * 1024 * 1024 : false,
|
|
11484
|
+
onExit: (sessionId, code) => {
|
|
11485
|
+
const logger = loggers.get(sessionId);
|
|
11486
|
+
logger?.logExit(code);
|
|
11487
|
+
loggers.delete(sessionId);
|
|
11488
|
+
safeSend(encodeFrame({ t: "exit", sessionId, code }));
|
|
11489
|
+
},
|
|
11490
|
+
log: (msg) => console.log(`[local] ${msg}`)
|
|
11491
|
+
},
|
|
11492
|
+
resolveDesktopConfig(config.desktop)
|
|
11493
|
+
);
|
|
11123
11494
|
const other = acquireSingleInstanceLock();
|
|
11124
11495
|
if (other !== null) {
|
|
11125
11496
|
console.error(`[local] \u53E6\u4E00\u4E2A cli-local \u6B63\u5728\u8FD0\u884C (pid ${other})\uFF0C\u62D2\u7EDD\u53CC\u5F00\u3002`);
|
|
@@ -11153,7 +11524,7 @@ async function startLocal(opts = {}) {
|
|
|
11153
11524
|
} else {
|
|
11154
11525
|
credentials = await ensureCredentials(baseUrl, registrationKey);
|
|
11155
11526
|
}
|
|
11156
|
-
const commands = detectCommands();
|
|
11527
|
+
const commands = detectCommands(desktops.unsupportedReason() === null);
|
|
11157
11528
|
console.log(`[local] connecting to ${url}`);
|
|
11158
11529
|
console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
|
|
11159
11530
|
console.log(`[local] bash: passthrough (audit: ${auditMode}, retention: ${retentionDays === 0 ? "forever" : retentionDays + "d"}${dangerConfirm ? ", dangerConfirm: on" : ""})`);
|
|
@@ -11176,11 +11547,18 @@ async function startLocal(opts = {}) {
|
|
|
11176
11547
|
uptimeMs: Date.now() - startedAt,
|
|
11177
11548
|
reconnectAttempts,
|
|
11178
11549
|
accessTokenExpiresAt: credentials.accessTokenExpiresAt || null,
|
|
11179
|
-
sessions:
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11550
|
+
sessions: [
|
|
11551
|
+
...pty.list().map((s) => ({
|
|
11552
|
+
sessionId: s.id,
|
|
11553
|
+
cmd: s.cmd,
|
|
11554
|
+
startedAt: s.startedAt
|
|
11555
|
+
})),
|
|
11556
|
+
...desktops.list().map((d) => ({
|
|
11557
|
+
sessionId: d.sessionId,
|
|
11558
|
+
cmd: "desktop",
|
|
11559
|
+
startedAt: d.startedAt
|
|
11560
|
+
}))
|
|
11561
|
+
]
|
|
11184
11562
|
});
|
|
11185
11563
|
function setPhase(p, reason = "") {
|
|
11186
11564
|
phase = p;
|
|
@@ -11231,6 +11609,10 @@ async function startLocal(opts = {}) {
|
|
|
11231
11609
|
exitProcess(0);
|
|
11232
11610
|
},
|
|
11233
11611
|
killSession: (id) => {
|
|
11612
|
+
if (desktops.has(id)) {
|
|
11613
|
+
desktops.kill(id);
|
|
11614
|
+
return true;
|
|
11615
|
+
}
|
|
11234
11616
|
try {
|
|
11235
11617
|
pty.kill(id);
|
|
11236
11618
|
return true;
|
|
@@ -11283,6 +11665,7 @@ async function startLocal(opts = {}) {
|
|
|
11283
11665
|
process.exit(userExitCode);
|
|
11284
11666
|
}
|
|
11285
11667
|
console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
|
|
11668
|
+
desktops.killAll();
|
|
11286
11669
|
if (phase !== "needs-pairing") setPhase("reconnecting");
|
|
11287
11670
|
scheduleReconnect();
|
|
11288
11671
|
});
|
|
@@ -11321,6 +11704,7 @@ async function startLocal(opts = {}) {
|
|
|
11321
11704
|
controlApi?.close();
|
|
11322
11705
|
if (opts.daemon) removeGatewayPid();
|
|
11323
11706
|
transfers.abortAll();
|
|
11707
|
+
desktops.killAll();
|
|
11324
11708
|
pty.killAll();
|
|
11325
11709
|
for (const [, logger] of loggers) logger.logExit(-1);
|
|
11326
11710
|
loggers.clear();
|
|
@@ -11359,9 +11743,22 @@ async function startLocal(opts = {}) {
|
|
|
11359
11743
|
case "spawn":
|
|
11360
11744
|
handleSpawn(frame);
|
|
11361
11745
|
break;
|
|
11746
|
+
case "desktop-query":
|
|
11747
|
+
handleDesktopQuery(frame);
|
|
11748
|
+
break;
|
|
11749
|
+
case "desktop-switch":
|
|
11750
|
+
void handleDesktopSwitch(frame);
|
|
11751
|
+
break;
|
|
11362
11752
|
case "input":
|
|
11363
11753
|
handleInput(frame);
|
|
11364
11754
|
break;
|
|
11755
|
+
case "desktop-data":
|
|
11756
|
+
try {
|
|
11757
|
+
desktops.write(frame.sessionId, frame.data);
|
|
11758
|
+
} catch (e) {
|
|
11759
|
+
console.error("[local] desktop write error:", e.message);
|
|
11760
|
+
}
|
|
11761
|
+
break;
|
|
11365
11762
|
case "resize":
|
|
11366
11763
|
pty.resize(frame.sessionId, frame.cols, frame.rows);
|
|
11367
11764
|
break;
|
|
@@ -11414,6 +11811,13 @@ async function startLocal(opts = {}) {
|
|
|
11414
11811
|
}
|
|
11415
11812
|
function handleSpawn(frame) {
|
|
11416
11813
|
const sessionId = frame.sessionId;
|
|
11814
|
+
if (frame.cmd === "desktop") {
|
|
11815
|
+
void handleDesktopSpawn(sessionId, {
|
|
11816
|
+
...frame.desktopDisplay !== void 0 ? { display: frame.desktopDisplay } : {},
|
|
11817
|
+
...frame.desktopWorkspace !== void 0 ? { workspace: frame.desktopWorkspace } : {}
|
|
11818
|
+
});
|
|
11819
|
+
return;
|
|
11820
|
+
}
|
|
11417
11821
|
try {
|
|
11418
11822
|
const logger = new SessionLogger(sessionId, frame.cmd, auditMode);
|
|
11419
11823
|
pty.spawn(
|
|
@@ -11461,6 +11865,63 @@ async function startLocal(opts = {}) {
|
|
|
11461
11865
|
console.error(`[local] spawn failed for ${sessionId}:`, reason);
|
|
11462
11866
|
}
|
|
11463
11867
|
}
|
|
11868
|
+
async function handleDesktopSpawn(sessionId, opts2 = {}) {
|
|
11869
|
+
const unsupported = desktops.unsupportedReason();
|
|
11870
|
+
if (unsupported) {
|
|
11871
|
+
safeSend(encodeFrame({ t: "spawn-err", sessionId, reason: unsupported }));
|
|
11872
|
+
return;
|
|
11873
|
+
}
|
|
11874
|
+
const logger = new SessionLogger(sessionId, "desktop", auditMode);
|
|
11875
|
+
try {
|
|
11876
|
+
await desktops.spawn(sessionId, opts2);
|
|
11877
|
+
loggers.set(sessionId, logger);
|
|
11878
|
+
safeSend(encodeFrame({ t: "spawn-ok", sessionId }));
|
|
11879
|
+
console.log(`[local] desktop session ${sessionId} bridged to VNC`);
|
|
11880
|
+
} catch (e) {
|
|
11881
|
+
logger.logExit(1);
|
|
11882
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
11883
|
+
safeSend(encodeFrame({ t: "spawn-err", sessionId, reason }));
|
|
11884
|
+
console.error(`[local] desktop spawn failed for ${sessionId}:`, reason);
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
function handleDesktopQuery(frame) {
|
|
11888
|
+
desktops.queryInfo().then((info) => {
|
|
11889
|
+
safeSend(encodeFrame({ t: "desktop-info", sessionId: frame.sessionId, ...info }));
|
|
11890
|
+
}).catch(() => {
|
|
11891
|
+
safeSend(
|
|
11892
|
+
encodeFrame({
|
|
11893
|
+
t: "desktop-info",
|
|
11894
|
+
sessionId: frame.sessionId,
|
|
11895
|
+
displays: [],
|
|
11896
|
+
workspaces: null,
|
|
11897
|
+
currentWorkspace: null,
|
|
11898
|
+
canPickDisplay: false
|
|
11899
|
+
})
|
|
11900
|
+
);
|
|
11901
|
+
});
|
|
11902
|
+
}
|
|
11903
|
+
async function handleDesktopSwitch(frame) {
|
|
11904
|
+
const logger = loggers.get(frame.sessionId);
|
|
11905
|
+
const reason = await desktops.switch(frame.sessionId, {
|
|
11906
|
+
...frame.display !== void 0 ? { display: frame.display } : {},
|
|
11907
|
+
...frame.workspace !== void 0 ? { workspace: frame.workspace } : {},
|
|
11908
|
+
...frame.workspaceDelta !== void 0 ? { workspaceDelta: frame.workspaceDelta } : {}
|
|
11909
|
+
}).catch((e) => e instanceof Error ? e.message : String(e));
|
|
11910
|
+
if (reason === void 0) {
|
|
11911
|
+
if (frame.display !== void 0) logger?.logSwitch("display", String(frame.display));
|
|
11912
|
+
if (frame.workspace !== void 0) logger?.logSwitch("workspace", String(frame.workspace));
|
|
11913
|
+
if (frame.workspaceDelta !== void 0)
|
|
11914
|
+
logger?.logSwitch("workspace", frame.workspaceDelta === 1 ? "next" : "prev");
|
|
11915
|
+
}
|
|
11916
|
+
safeSend(
|
|
11917
|
+
encodeFrame({
|
|
11918
|
+
t: "desktop-switch-res",
|
|
11919
|
+
sessionId: frame.sessionId,
|
|
11920
|
+
ok: reason === void 0,
|
|
11921
|
+
...reason !== void 0 ? { reason } : {}
|
|
11922
|
+
})
|
|
11923
|
+
);
|
|
11924
|
+
}
|
|
11464
11925
|
function handleInput(frame) {
|
|
11465
11926
|
const sessionId = frame.sessionId;
|
|
11466
11927
|
const logger = loggers.get(sessionId);
|
|
@@ -11535,10 +11996,25 @@ async function startLocal(opts = {}) {
|
|
|
11535
11996
|
}
|
|
11536
11997
|
}
|
|
11537
11998
|
function handleKill(sessionId) {
|
|
11999
|
+
if (desktops.has(sessionId)) {
|
|
12000
|
+
desktops.kill(sessionId);
|
|
12001
|
+
return;
|
|
12002
|
+
}
|
|
11538
12003
|
pty.kill(sessionId);
|
|
11539
12004
|
lineBuffers.delete(sessionId);
|
|
11540
12005
|
}
|
|
11541
12006
|
function handleReplay(frame) {
|
|
12007
|
+
if (desktops.has(frame.sessionId)) {
|
|
12008
|
+
safeSend(
|
|
12009
|
+
encodeFrame({
|
|
12010
|
+
t: "replay-res",
|
|
12011
|
+
sessionId: frame.sessionId,
|
|
12012
|
+
frames: [],
|
|
12013
|
+
exists: true
|
|
12014
|
+
})
|
|
12015
|
+
);
|
|
12016
|
+
return;
|
|
12017
|
+
}
|
|
11542
12018
|
const frames = pty.replay(frame.sessionId, frame.fromSeq);
|
|
11543
12019
|
safeSend(
|
|
11544
12020
|
encodeFrame({
|
|
@@ -11698,7 +12174,7 @@ function resolveHere() {
|
|
|
11698
12174
|
var argUrl = process.argv[2];
|
|
11699
12175
|
var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
|
|
11700
12176
|
function readVersion() {
|
|
11701
|
-
if (true) return "0.
|
|
12177
|
+
if (true) return "0.4.0";
|
|
11702
12178
|
try {
|
|
11703
12179
|
const pkg = JSON.parse(
|
|
11704
12180
|
(0, import_node_fs5.readFileSync)(new URL("../package.json", import_meta.url), "utf-8")
|
|
@@ -11771,7 +12247,7 @@ function entryScript() {
|
|
|
11771
12247
|
}
|
|
11772
12248
|
function run(cmd, args, okToFail = false) {
|
|
11773
12249
|
try {
|
|
11774
|
-
return (0,
|
|
12250
|
+
return (0, import_node_child_process5.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11775
12251
|
} catch (e) {
|
|
11776
12252
|
if (okToFail) return null;
|
|
11777
12253
|
throw e;
|
|
@@ -11972,7 +12448,7 @@ if (argUrl === "gateway") {
|
|
|
11972
12448
|
for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
|
|
11973
12449
|
const args = [entry, "gateway", "run"];
|
|
11974
12450
|
if (hubArg) args.push("--hub", hubArg);
|
|
11975
|
-
const child = (0,
|
|
12451
|
+
const child = (0, import_node_child_process5.spawn)(process.execPath, args, {
|
|
11976
12452
|
detached: true,
|
|
11977
12453
|
stdio: ["ignore", out, out],
|
|
11978
12454
|
env
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cli-remote/local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Local PTY agent for cli-remote — exposes your local CLI tools (claude/codex/kimi-cli/shell) to the mobile web client via a self-hosted hub.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|