@cli-remote/local 0.3.3 → 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.
Files changed (2) hide show
  1. package/dist/index.cjs +686 -75
  2. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -2263,9 +2263,9 @@ var require_websocket = __commonJS({
2263
2263
  var EventEmitter = require("events");
2264
2264
  var https = require("https");
2265
2265
  var http = require("http");
2266
- var net = require("net");
2266
+ var net2 = require("net");
2267
2267
  var tls = require("tls");
2268
- var { randomBytes: randomBytes3, createHash } = require("crypto");
2268
+ var { randomBytes: randomBytes3, createHash: createHash2 } = require("crypto");
2269
2269
  var { Duplex, Readable } = require("stream");
2270
2270
  var { URL: URL2 } = require("url");
2271
2271
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2933,7 +2933,7 @@ var require_websocket = __commonJS({
2933
2933
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2934
2934
  return;
2935
2935
  }
2936
- const digest = createHash("sha1").update(key + GUID).digest("base64");
2936
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
2937
2937
  if (res.headers["sec-websocket-accept"] !== digest) {
2938
2938
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2939
2939
  return;
@@ -3007,12 +3007,12 @@ var require_websocket = __commonJS({
3007
3007
  }
3008
3008
  function netConnect(options) {
3009
3009
  options.path = options.socketPath;
3010
- return net.connect(options);
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 = net.isIP(options.host) ? "" : options.host;
3015
+ options.servername = net2.isIP(options.host) ? "" : options.host;
3016
3016
  }
3017
3017
  return tls.connect(options);
3018
3018
  }
@@ -3302,7 +3302,7 @@ var require_websocket_server = __commonJS({
3302
3302
  var EventEmitter = require("events");
3303
3303
  var http = require("http");
3304
3304
  var { Duplex } = require("stream");
3305
- var { createHash } = require("crypto");
3305
+ var { createHash: createHash2 } = require("crypto");
3306
3306
  var extension2 = require_extension();
3307
3307
  var PerMessageDeflate2 = require_permessage_deflate();
3308
3308
  var subprotocol2 = require_subprotocol();
@@ -3609,7 +3609,7 @@ var require_websocket_server = __commonJS({
3609
3609
  );
3610
3610
  }
3611
3611
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3612
- const digest = createHash("sha1").update(key + GUID).digest("base64");
3612
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
3613
3613
  const headers = [
3614
3614
  "HTTP/1.1 101 Switching Protocols",
3615
3615
  "Upgrade: websocket",
@@ -9940,9 +9940,9 @@ var require_lib = __commonJS({
9940
9940
 
9941
9941
  // src/index.ts
9942
9942
  var import_node_fs5 = require("node:fs");
9943
- var import_node_child_process3 = require("node:child_process");
9944
- var import_node_os7 = require("node:os");
9945
- var import_node_path7 = require("node:path");
9943
+ var import_node_child_process5 = require("node:child_process");
9944
+ var import_node_os8 = require("node:os");
9945
+ var import_node_path8 = require("node:path");
9946
9946
  var import_node_url = require("node:url");
9947
9947
 
9948
9948
  // ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
@@ -9957,11 +9957,11 @@ 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 import_node_child_process2 = require("node:child_process");
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
- var import_node_os6 = require("node:os");
9964
- var import_node_path6 = require("node:path");
9963
+ var import_node_os7 = require("node:os");
9964
+ var import_node_path7 = require("node:path");
9965
9965
 
9966
9966
  // ../shared/src/codec.ts
9967
9967
  var import_msgpack = __toESM(require_dist(), 1);
@@ -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,18 +10955,148 @@ function startSetupCallback(timeoutMs = 3 * 6e4) {
10606
10955
  }
10607
10956
 
10608
10957
  // src/auth.ts
10609
- var import_node_child_process = require("node:child_process");
10958
+ var import_node_child_process3 = require("node:child_process");
10959
+ var import_node_crypto3 = require("node:crypto");
10960
+ var import_promises3 = require("node:fs/promises");
10961
+ var import_node_os6 = require("node:os");
10962
+ var import_node_path6 = require("node:path");
10963
+ var import_qrcode = __toESM(require_lib(), 1);
10964
+
10965
+ // src/machine-id.ts
10966
+ var import_node_child_process2 = require("node:child_process");
10610
10967
  var import_node_crypto2 = require("node:crypto");
10611
10968
  var import_promises2 = require("node:fs/promises");
10612
10969
  var import_node_os5 = require("node:os");
10613
10970
  var import_node_path5 = require("node:path");
10614
- var import_qrcode = __toESM(require_lib(), 1);
10971
+ var import_node_util2 = require("node:util");
10972
+ var execFileAsync = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
10973
+ var MACHINE_ID_DOMAIN = "cli-remote.machineid.v1";
10974
+ function normalizeHardwareId(raw) {
10975
+ return raw.trim().toLowerCase().replace(/\s+/g, " ");
10976
+ }
10977
+ var GARBAGE_VALUES = /* @__PURE__ */ new Set([
10978
+ "",
10979
+ "none",
10980
+ "null",
10981
+ "default string",
10982
+ "system serial number",
10983
+ "to be filled by o.e.m.",
10984
+ "to be filled by oem",
10985
+ "not specified",
10986
+ "not available",
10987
+ "unknown",
10988
+ "chassis manufacturer",
10989
+ "no enclosure",
10990
+ "empty"
10991
+ ]);
10992
+ function isValidHardwareId(raw) {
10993
+ const v = normalizeHardwareId(raw);
10994
+ if (v.length < 4) return false;
10995
+ if (GARBAGE_VALUES.has(v)) return false;
10996
+ const digits = v.replace(/[^0-9a-f]/g, "");
10997
+ if (digits.length >= 8 && /^([0-9a-f])\1*$/.test(digits)) return false;
10998
+ return true;
10999
+ }
11000
+ function deriveMachineId(tag, raw) {
11001
+ const h = (0, import_node_crypto2.createHash)("sha256").update(`${MACHINE_ID_DOMAIN}|${tag}|${normalizeHardwareId(raw)}`).digest("hex");
11002
+ return "m_" + h.slice(0, 32);
11003
+ }
11004
+ async function ioregValue(exec, key) {
11005
+ try {
11006
+ const { stdout } = await exec("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
11007
+ const m = stdout.match(new RegExp(`"${key}"\\s*=\\s*"([^"]*)"`));
11008
+ return m?.[1] ? normalizeHardwareId(m[1]) : null;
11009
+ } catch {
11010
+ return null;
11011
+ }
11012
+ }
11013
+ async function firstPhysicalMacMac(deps) {
11014
+ try {
11015
+ const names = (await deps.readDir("/sys/class/net")).sort();
11016
+ for (const name of names) {
11017
+ const base = (0, import_node_path5.join)("/sys/class/net", name);
11018
+ if (!await deps.statOrNull((0, import_node_path5.join)(base, "device"))) continue;
11019
+ const mac = await deps.readFile((0, import_node_path5.join)(base, "address"));
11020
+ return normalizeHardwareId(mac);
11021
+ }
11022
+ } catch {
11023
+ }
11024
+ return null;
11025
+ }
11026
+ async function winMachineGuid(exec) {
11027
+ try {
11028
+ const { stdout } = await exec("reg", [
11029
+ "query",
11030
+ "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
11031
+ "/v",
11032
+ "MachineGuid"
11033
+ ]);
11034
+ const m = stdout.match(/MachineGuid\s+REG_SZ\s+(\S+)/i);
11035
+ return m?.[1] ? normalizeHardwareId(m[1]) : null;
11036
+ } catch {
11037
+ return null;
11038
+ }
11039
+ }
11040
+ async function resolveHardwareMachineId(deps = {}) {
11041
+ const plat = deps.platform ?? (0, import_node_os5.platform)();
11042
+ const readFileDeps = deps.readFile ?? ((p) => (0, import_promises2.readFile)(p, "utf-8"));
11043
+ const execDeps = deps.exec ?? (async (cmd, args) => {
11044
+ const { stdout } = await execFileAsync(cmd, args, { timeout: 3e3 });
11045
+ return { stdout };
11046
+ });
11047
+ const candidates = [];
11048
+ if (plat === "darwin") {
11049
+ candidates.push(
11050
+ { tag: "mac:serial", raw: await ioregValue(execDeps, "IOPlatformSerialNumber") },
11051
+ { tag: "mac:platform-uuid", raw: await ioregValue(execDeps, "IOPlatformUUID") }
11052
+ );
11053
+ } else if (plat === "linux") {
11054
+ const readFileOrNull = async (p) => {
11055
+ try {
11056
+ return await readFileDeps(p);
11057
+ } catch {
11058
+ return null;
11059
+ }
11060
+ };
11061
+ candidates.push(
11062
+ { tag: "linux:product-uuid", raw: await readFileOrNull("/sys/class/dmi/id/product_uuid") },
11063
+ { tag: "linux:board-serial", raw: await readFileOrNull("/sys/class/dmi/id/board_serial") },
11064
+ { tag: "linux:machine-id", raw: await readFileOrNull("/etc/machine-id") },
11065
+ { tag: "linux:machine-id", raw: await readFileOrNull("/var/lib/dbus/machine-id") },
11066
+ {
11067
+ tag: "linux:nic-mac",
11068
+ raw: await firstPhysicalMacMac({
11069
+ readFile: readFileDeps,
11070
+ readDir: deps.readDir ?? ((p) => (0, import_promises2.readdir)(p)),
11071
+ statOrNull: deps.statOrNull ?? (async (p) => {
11072
+ try {
11073
+ await (0, import_promises2.stat)(p);
11074
+ return true;
11075
+ } catch {
11076
+ return false;
11077
+ }
11078
+ })
11079
+ })
11080
+ }
11081
+ );
11082
+ } else if (plat === "win32") {
11083
+ candidates.push({ tag: "win:machine-guid", raw: await winMachineGuid(execDeps) });
11084
+ }
11085
+ for (const c of candidates) {
11086
+ if (c.raw !== null && isValidHardwareId(c.raw)) {
11087
+ return { id: deriveMachineId(c.tag, c.raw), source: c.tag };
11088
+ }
11089
+ }
11090
+ return null;
11091
+ }
11092
+
11093
+ // src/auth.ts
10615
11094
  function credentialsPath() {
10616
- return (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11095
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10617
11096
  }
10618
11097
  async function loadCredentials() {
10619
11098
  try {
10620
- const raw = JSON.parse(await (0, import_promises2.readFile)(credentialsPath(), "utf-8"));
11099
+ const raw = JSON.parse(await (0, import_promises3.readFile)(credentialsPath(), "utf-8"));
10621
11100
  if (!raw.deviceId || !raw.accessToken || !raw.refreshToken) return null;
10622
11101
  return raw;
10623
11102
  } catch {
@@ -10626,10 +11105,10 @@ async function loadCredentials() {
10626
11105
  }
10627
11106
  async function saveCredentials(creds) {
10628
11107
  const path = credentialsPath();
10629
- await (0, import_promises2.mkdir)((0, import_node_path5.join)(path, ".."), { recursive: true });
10630
- await (0, import_promises2.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
11108
+ await (0, import_promises3.mkdir)((0, import_node_path6.join)(path, ".."), { recursive: true });
11109
+ await (0, import_promises3.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10631
11110
  try {
10632
- await (0, import_promises2.chmod)(path, 384);
11111
+ await (0, import_promises3.chmod)(path, 384);
10633
11112
  } catch {
10634
11113
  }
10635
11114
  }
@@ -10655,7 +11134,7 @@ function openBrowser(url) {
10655
11134
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
10656
11135
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
10657
11136
  try {
10658
- const child = (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
11137
+ const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: "ignore", detached: true });
10659
11138
  child.unref();
10660
11139
  } catch {
10661
11140
  }
@@ -10667,7 +11146,14 @@ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
10667
11146
  throw new AuthError(`\u767B\u5F55\u5931\u8D25: ${reason ?? login.status}`);
10668
11147
  }
10669
11148
  const token = login.data.token;
10670
- if (!token) throw new AuthError("\u670D\u52A1\u7AEF\u672A\u8FD4\u56DE\u4F1A\u8BDD 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
+ }
10671
11157
  const me = await fetch(`${baseUrl}/api/me`, {
10672
11158
  headers: { authorization: `Bearer ${token}` }
10673
11159
  });
@@ -10677,15 +11163,15 @@ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
10677
11163
  return { registrationKey: data.registrationKey, user: data.user?.email ?? email };
10678
11164
  }
10679
11165
  async function mergeLocalConfig(patch) {
10680
- const dir = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR);
10681
- const path = (0, import_node_path5.join)(dir, CONFIG_FILE);
11166
+ const dir = (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR);
11167
+ const path = (0, import_node_path6.join)(dir, CONFIG_FILE);
10682
11168
  let existing = {};
10683
11169
  try {
10684
- existing = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
11170
+ existing = JSON.parse(await (0, import_promises3.readFile)(path, "utf-8"));
10685
11171
  } catch {
10686
11172
  }
10687
- await (0, import_promises2.mkdir)(dir, { recursive: true });
10688
- await (0, import_promises2.writeFile)(path, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n");
11173
+ await (0, import_promises3.mkdir)(dir, { recursive: true });
11174
+ await (0, import_promises3.writeFile)(path, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n");
10689
11175
  }
10690
11176
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
10691
11177
  var AuthError = class extends Error {
@@ -10699,7 +11185,7 @@ async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
10699
11185
  const res = await postJson(`${baseUrl}/oauth/device_authorization`, {
10700
11186
  registrationKey,
10701
11187
  device_id: deviceId,
10702
- device_name: (0, import_node_os5.hostname)(),
11188
+ device_name: (0, import_node_os6.hostname)(),
10703
11189
  platform: process.platform
10704
11190
  });
10705
11191
  if (res.status !== 200) {
@@ -10789,14 +11275,19 @@ async function ensureFreshAccessToken(baseUrl, creds) {
10789
11275
  return refreshInFlight;
10790
11276
  }
10791
11277
  async function ensureMachineId() {
10792
- const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
11278
+ const path = (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10793
11279
  try {
10794
- const cfg = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
11280
+ const cfg = JSON.parse(await (0, import_promises3.readFile)(path, "utf-8"));
10795
11281
  if (typeof cfg.machineId === "string" && cfg.machineId) return cfg.machineId;
10796
11282
  } catch {
10797
11283
  }
10798
- const machineId = "m_" + (0, import_node_crypto2.randomBytes)(16).toString("hex");
10799
- await mergeLocalConfig({ machineId });
11284
+ const hw = await resolveHardwareMachineId();
11285
+ if (hw) {
11286
+ await mergeLocalConfig({ machineId: hw.id, machineIdSource: hw.source });
11287
+ return hw.id;
11288
+ }
11289
+ const machineId = "m_" + (0, import_node_crypto3.randomBytes)(16).toString("hex");
11290
+ await mergeLocalConfig({ machineId, machineIdSource: "random" });
10800
11291
  return machineId;
10801
11292
  }
10802
11293
  async function stableDeviceId() {
@@ -10819,7 +11310,7 @@ function isPermanentAuthFailure(e) {
10819
11310
  return e instanceof AuthError && e.message.includes("invalid_grant");
10820
11311
  }
10821
11312
  async function loadConfig() {
10822
- const path = (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
11313
+ const path = (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10823
11314
  try {
10824
11315
  return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf-8"));
10825
11316
  } catch {
@@ -10840,7 +11331,7 @@ function applyConfigDeprecations(cfg) {
10840
11331
  function isTTY() {
10841
11332
  return process.stdin.isTTY === true;
10842
11333
  }
10843
- function detectCommands() {
11334
+ function detectCommands(desktopEnabled) {
10844
11335
  const bins = [
10845
11336
  ["claude", "claude"],
10846
11337
  ["codex", "codex"],
@@ -10851,15 +11342,16 @@ function detectCommands() {
10851
11342
  for (const [cmd, bin] of bins) {
10852
11343
  let ok = false;
10853
11344
  if (process.platform === "win32") {
10854
- ok = (0, import_node_child_process2.spawnSync)("where", [bin], { timeout: 5e3 }).status === 0;
11345
+ ok = (0, import_node_child_process4.spawnSync)("where", [bin], { timeout: 5e3 }).status === 0;
10855
11346
  } else {
10856
11347
  const shell = process.env.SHELL ?? "/bin/bash";
10857
- ok = (0, import_node_child_process2.spawnSync)(shell, ["-l", "-c", `command -v ${bin}`], {
11348
+ ok = (0, import_node_child_process4.spawnSync)(shell, ["-l", "-c", `command -v ${bin}`], {
10858
11349
  timeout: 5e3
10859
11350
  }).status === 0;
10860
11351
  }
10861
11352
  if (ok) found.push(cmd);
10862
11353
  }
11354
+ if (desktopEnabled) found.push("desktop");
10863
11355
  return found;
10864
11356
  }
10865
11357
  async function readInput(prompt) {
@@ -10985,6 +11477,20 @@ async function startLocal(opts = {}) {
10985
11477
  (data) => safeSend(data),
10986
11478
  (msg) => console.log(`[local] ${msg}`)
10987
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
+ );
10988
11494
  const other = acquireSingleInstanceLock();
10989
11495
  if (other !== null) {
10990
11496
  console.error(`[local] \u53E6\u4E00\u4E2A cli-local \u6B63\u5728\u8FD0\u884C (pid ${other})\uFF0C\u62D2\u7EDD\u53CC\u5F00\u3002`);
@@ -11018,7 +11524,7 @@ async function startLocal(opts = {}) {
11018
11524
  } else {
11019
11525
  credentials = await ensureCredentials(baseUrl, registrationKey);
11020
11526
  }
11021
- const commands = detectCommands();
11527
+ const commands = detectCommands(desktops.unsupportedReason() === null);
11022
11528
  console.log(`[local] connecting to ${url}`);
11023
11529
  console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
11024
11530
  console.log(`[local] bash: passthrough (audit: ${auditMode}, retention: ${retentionDays === 0 ? "forever" : retentionDays + "d"}${dangerConfirm ? ", dangerConfirm: on" : ""})`);
@@ -11041,11 +11547,18 @@ async function startLocal(opts = {}) {
11041
11547
  uptimeMs: Date.now() - startedAt,
11042
11548
  reconnectAttempts,
11043
11549
  accessTokenExpiresAt: credentials.accessTokenExpiresAt || null,
11044
- sessions: pty.list().map((s) => ({
11045
- sessionId: s.id,
11046
- cmd: s.cmd,
11047
- startedAt: s.startedAt
11048
- }))
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
+ ]
11049
11562
  });
11050
11563
  function setPhase(p, reason = "") {
11051
11564
  phase = p;
@@ -11096,6 +11609,10 @@ async function startLocal(opts = {}) {
11096
11609
  exitProcess(0);
11097
11610
  },
11098
11611
  killSession: (id) => {
11612
+ if (desktops.has(id)) {
11613
+ desktops.kill(id);
11614
+ return true;
11615
+ }
11099
11616
  try {
11100
11617
  pty.kill(id);
11101
11618
  return true;
@@ -11148,6 +11665,7 @@ async function startLocal(opts = {}) {
11148
11665
  process.exit(userExitCode);
11149
11666
  }
11150
11667
  console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
11668
+ desktops.killAll();
11151
11669
  if (phase !== "needs-pairing") setPhase("reconnecting");
11152
11670
  scheduleReconnect();
11153
11671
  });
@@ -11186,6 +11704,7 @@ async function startLocal(opts = {}) {
11186
11704
  controlApi?.close();
11187
11705
  if (opts.daemon) removeGatewayPid();
11188
11706
  transfers.abortAll();
11707
+ desktops.killAll();
11189
11708
  pty.killAll();
11190
11709
  for (const [, logger] of loggers) logger.logExit(-1);
11191
11710
  loggers.clear();
@@ -11224,9 +11743,22 @@ async function startLocal(opts = {}) {
11224
11743
  case "spawn":
11225
11744
  handleSpawn(frame);
11226
11745
  break;
11746
+ case "desktop-query":
11747
+ handleDesktopQuery(frame);
11748
+ break;
11749
+ case "desktop-switch":
11750
+ void handleDesktopSwitch(frame);
11751
+ break;
11227
11752
  case "input":
11228
11753
  handleInput(frame);
11229
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;
11230
11762
  case "resize":
11231
11763
  pty.resize(frame.sessionId, frame.cols, frame.rows);
11232
11764
  break;
@@ -11279,6 +11811,13 @@ async function startLocal(opts = {}) {
11279
11811
  }
11280
11812
  function handleSpawn(frame) {
11281
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
+ }
11282
11821
  try {
11283
11822
  const logger = new SessionLogger(sessionId, frame.cmd, auditMode);
11284
11823
  pty.spawn(
@@ -11326,6 +11865,63 @@ async function startLocal(opts = {}) {
11326
11865
  console.error(`[local] spawn failed for ${sessionId}:`, reason);
11327
11866
  }
11328
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
+ }
11329
11925
  function handleInput(frame) {
11330
11926
  const sessionId = frame.sessionId;
11331
11927
  const logger = loggers.get(sessionId);
@@ -11400,10 +11996,25 @@ async function startLocal(opts = {}) {
11400
11996
  }
11401
11997
  }
11402
11998
  function handleKill(sessionId) {
11999
+ if (desktops.has(sessionId)) {
12000
+ desktops.kill(sessionId);
12001
+ return;
12002
+ }
11403
12003
  pty.kill(sessionId);
11404
12004
  lineBuffers.delete(sessionId);
11405
12005
  }
11406
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
+ }
11407
12018
  const frames = pty.replay(frame.sessionId, frame.fromSeq);
11408
12019
  safeSend(
11409
12020
  encodeFrame({
@@ -11429,10 +12040,10 @@ async function startLocal(opts = {}) {
11429
12040
  }
11430
12041
  }
11431
12042
  function credentialsPathOf() {
11432
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "credentials.json");
12043
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "credentials.json");
11433
12044
  }
11434
12045
  function instanceLockPath() {
11435
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "instance.lock");
12046
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "instance.lock");
11436
12047
  }
11437
12048
  function processAlive(pid) {
11438
12049
  try {
@@ -11452,7 +12063,7 @@ function acquireSingleInstanceLock() {
11452
12063
  } catch {
11453
12064
  }
11454
12065
  try {
11455
- (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(path, ".."), { recursive: true });
12066
+ (0, import_node_fs4.mkdirSync)((0, import_node_path7.join)(path, ".."), { recursive: true });
11456
12067
  (0, import_node_fs4.writeFileSync)(path, String(process.pid));
11457
12068
  } catch {
11458
12069
  }
@@ -11482,10 +12093,10 @@ function toUint8(raw) {
11482
12093
  throw new Error("unexpected raw type");
11483
12094
  }
11484
12095
  function gatewayPidFile() {
11485
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
12096
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11486
12097
  }
11487
12098
  function writeGatewayPid() {
11488
- (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR), { recursive: true });
12099
+ (0, import_node_fs4.mkdirSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR), { recursive: true });
11489
12100
  (0, import_node_fs4.writeFileSync)(gatewayPidFile(), String(process.pid));
11490
12101
  }
11491
12102
  function removeGatewayPid() {
@@ -11530,11 +12141,11 @@ function resolveHere() {
11530
12141
  if (DEBUG) console.error("[debug] cannot resolve script path");
11531
12142
  return;
11532
12143
  }
11533
- const hereDir = (0, import_node_path7.dirname)(here);
12144
+ const hereDir = (0, import_node_path8.dirname)(here);
11534
12145
  const candidates = [
11535
- (0, import_node_path7.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11536
- (0, import_node_path7.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11537
- (0, import_node_path7.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
12146
+ (0, import_node_path8.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
12147
+ (0, import_node_path8.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
12148
+ (0, import_node_path8.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
11538
12149
  ];
11539
12150
  let prebuildsDir = null;
11540
12151
  for (const c of candidates) {
@@ -11549,7 +12160,7 @@ function resolveHere() {
11549
12160
  }
11550
12161
  let n = 0;
11551
12162
  for (const sub of (0, import_node_fs5.readdirSync)(prebuildsDir)) {
11552
- const helper = (0, import_node_path7.join)(prebuildsDir, sub, "spawn-helper");
12163
+ const helper = (0, import_node_path8.join)(prebuildsDir, sub, "spawn-helper");
11553
12164
  if ((0, import_node_fs5.existsSync)(helper)) {
11554
12165
  try {
11555
12166
  (0, import_node_fs5.chmodSync)(helper, 493);
@@ -11563,7 +12174,7 @@ function resolveHere() {
11563
12174
  var argUrl = process.argv[2];
11564
12175
  var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
11565
12176
  function readVersion() {
11566
- if (true) return "0.3.3";
12177
+ if (true) return "0.4.0";
11567
12178
  try {
11568
12179
  const pkg = JSON.parse(
11569
12180
  (0, import_node_fs5.readFileSync)(new URL("../package.json", import_meta.url), "utf-8")
@@ -11623,7 +12234,7 @@ example:
11623
12234
  }
11624
12235
  var LAUNCHD_LABEL = "net.cli-remote.local";
11625
12236
  function launchdPlistPath() {
11626
- return (0, import_node_path7.join)((0, import_node_os7.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
12237
+ return (0, import_node_path8.join)((0, import_node_os8.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11627
12238
  }
11628
12239
  function entryScript() {
11629
12240
  const a1 = process.argv[1];
@@ -11636,7 +12247,7 @@ function entryScript() {
11636
12247
  }
11637
12248
  function run(cmd, args, okToFail = false) {
11638
12249
  try {
11639
- return (0, import_node_child_process3.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
12250
+ return (0, import_node_child_process5.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
11640
12251
  } catch (e) {
11641
12252
  if (okToFail) return null;
11642
12253
  throw e;
@@ -11647,7 +12258,7 @@ function launchdInstall(urlArg) {
11647
12258
  console.error(`[local] launchd \u5E38\u9A7B\u4EC5\u652F\u6301 macOS\uFF08\u5F53\u524D ${process.platform}\uFF09\uFF1BLinux \u8BF7\u7528 systemd --user \u81EA\u884C\u6258\u7BA1`);
11648
12259
  process.exit(1);
11649
12260
  }
11650
- const home = (0, import_node_os7.homedir)();
12261
+ const home = (0, import_node_os8.homedir)();
11651
12262
  const entry = entryScript();
11652
12263
  if (!entry) {
11653
12264
  console.error("[local] \u5F53\u524D\u4EE5\u6E90\u7801\uFF08tsx\uFF09\u6A21\u5F0F\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
@@ -11655,9 +12266,9 @@ function launchdInstall(urlArg) {
11655
12266
  process.exit(1);
11656
12267
  }
11657
12268
  if (urlArg) {
11658
- const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
12269
+ const cfgDir = (0, import_node_path8.join)(home, DEFAULT_CONFIG_DIR);
11659
12270
  (0, import_node_fs5.mkdirSync)(cfgDir, { recursive: true });
11660
- const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
12271
+ const cfgPath = (0, import_node_path8.join)(cfgDir, "config.json");
11661
12272
  let cfg = {};
11662
12273
  try {
11663
12274
  cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
@@ -11667,15 +12278,15 @@ function launchdInstall(urlArg) {
11667
12278
  (0, import_node_fs5.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11668
12279
  console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11669
12280
  }
11670
- const credPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
12281
+ const credPath = (0, import_node_path8.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11671
12282
  if (!(0, import_node_fs5.existsSync)(credPath)) {
11672
12283
  console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11673
12284
  console.error("[local] \u8BF7\u5148\u4EA4\u4E92\u5F0F\u8FD0\u884C\u4E00\u6B21 cli-local \u5B8C\u6210\u914D\u7F6E\u4E0E\u6388\u6743\uFF0C\u518D\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
11674
12285
  process.exit(1);
11675
12286
  }
11676
12287
  const plist = launchdPlistPath();
11677
- const logDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "logs");
11678
- (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(plist), { recursive: true });
12288
+ const logDir = (0, import_node_path8.join)(home, DEFAULT_CONFIG_DIR, "logs");
12289
+ (0, import_node_fs5.mkdirSync)((0, import_node_path8.dirname)(plist), { recursive: true });
11679
12290
  (0, import_node_fs5.mkdirSync)(logDir, { recursive: true });
11680
12291
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
11681
12292
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -11690,8 +12301,8 @@ function launchdInstall(urlArg) {
11690
12301
  <key>RunAtLoad</key><true/>
11691
12302
  <key>KeepAlive</key><true/>
11692
12303
  <key>ThrottleInterval</key><integer>30</integer>
11693
- <key>StandardOutPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.out.log")}</string>
11694
- <key>StandardErrorPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.err.log")}</string>
12304
+ <key>StandardOutPath</key><string>${(0, import_node_path8.join)(logDir, "daemon.out.log")}</string>
12305
+ <key>StandardErrorPath</key><string>${(0, import_node_path8.join)(logDir, "daemon.err.log")}</string>
11695
12306
  </dict>
11696
12307
  </plist>
11697
12308
  `;
@@ -11734,8 +12345,8 @@ if (argUrl && !argMatch && argUrl === "uninstall") {
11734
12345
  }
11735
12346
  if (argUrl === "pair") {
11736
12347
  void (async () => {
11737
- const home = (0, import_node_os7.homedir)();
11738
- const cfgPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "config.json");
12348
+ const home = (0, import_node_os8.homedir)();
12349
+ const cfgPath = (0, import_node_path8.join)(home, DEFAULT_CONFIG_DIR, "config.json");
11739
12350
  let cfg = {};
11740
12351
  try {
11741
12352
  cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
@@ -11773,7 +12384,7 @@ if (argUrl === "pair") {
11773
12384
  }
11774
12385
  if (argUrl === "gateway") {
11775
12386
  let gatewayLogFile = function() {
11776
- return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "logs", "gateway.log");
12387
+ return (0, import_node_path8.join)((0, import_node_os8.homedir)(), DEFAULT_CONFIG_DIR, "logs", "gateway.log");
11777
12388
  }, gatewayLogs = function() {
11778
12389
  const follow = rest.includes("-f") || rest.includes("--follow");
11779
12390
  const li = rest.findIndex((a) => a === "-n" || a === "--lines");
@@ -11817,10 +12428,10 @@ if (argUrl === "gateway") {
11817
12428
  console.log(`[gateway] already running (pid ${gatewayPid()})`);
11818
12429
  process.exit(0);
11819
12430
  }
11820
- const home = (0, import_node_os7.homedir)();
12431
+ const home = (0, import_node_os8.homedir)();
11821
12432
  const entry = entryScript();
11822
- const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
11823
- const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
12433
+ const cfgDir = (0, import_node_path8.join)(home, DEFAULT_CONFIG_DIR);
12434
+ const cfgPath = (0, import_node_path8.join)(cfgDir, "config.json");
11824
12435
  const hasCfg = (0, import_node_fs5.existsSync)(cfgPath);
11825
12436
  if (!entry) {
11826
12437
  console.error("[gateway] \u6E90\u7801\uFF08tsx\uFF09\u6A21\u5F0F\u4E0D\u652F\u6301\u540E\u53F0\u8FD0\u884C\uFF1B\u8BF7\u7528 npm \u5B89\u88C5\u7248\uFF1Anpm i -g @cli-remote/local");
@@ -11831,13 +12442,13 @@ if (argUrl === "gateway") {
11831
12442
  console.error("[gateway] \uFF08\u65E0\u51ED\u8BC1\u4E5F\u53EF\u4EE5\u542F\u52A8\uFF1Agateway \u4F1A\u8FDB\u5165 needs-pairing\uFF0C\u7B49 cli-local pair\uFF09");
11832
12443
  process.exit(1);
11833
12444
  }
11834
- (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(gatewayLogFile()), { recursive: true });
12445
+ (0, import_node_fs5.mkdirSync)((0, import_node_path8.dirname)(gatewayLogFile()), { recursive: true });
11835
12446
  const out = (0, import_node_fs5.openSync)(gatewayLogFile(), "a");
11836
12447
  const env = {};
11837
12448
  for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
11838
12449
  const args = [entry, "gateway", "run"];
11839
12450
  if (hubArg) args.push("--hub", hubArg);
11840
- const child = (0, import_node_child_process3.spawn)(process.execPath, args, {
12451
+ const child = (0, import_node_child_process5.spawn)(process.execPath, args, {
11841
12452
  detached: true,
11842
12453
  stdio: ["ignore", out, out],
11843
12454
  env
@@ -11863,12 +12474,12 @@ if (argUrl === "gateway") {
11863
12474
  while (Date.now() - t02 < 5e3 && gatewayAlive()) {
11864
12475
  await new Promise((res) => setTimeout(res, 150));
11865
12476
  }
11866
- (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
12477
+ (0, import_node_fs5.rmSync)((0, import_node_path8.join)((0, import_node_os8.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11867
12478
  return;
11868
12479
  }
11869
12480
  const pid = gatewayPid();
11870
12481
  if (pid === null || !gatewayAlive()) {
11871
- (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
12482
+ (0, import_node_fs5.rmSync)((0, import_node_path8.join)((0, import_node_os8.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11872
12483
  return;
11873
12484
  }
11874
12485
  try {
@@ -11879,7 +12490,7 @@ if (argUrl === "gateway") {
11879
12490
  while (Date.now() - t0 < 5e3 && gatewayAlive()) {
11880
12491
  await new Promise((res) => setTimeout(res, 150));
11881
12492
  }
11882
- (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
12493
+ (0, import_node_fs5.rmSync)((0, import_node_path8.join)((0, import_node_os8.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11883
12494
  }
11884
12495
  async function gatewayStop() {
11885
12496
  const pid = gatewayPid();
@@ -11920,7 +12531,7 @@ if (argUrl === "gateway") {
11920
12531
  const pid = gatewayPid();
11921
12532
  let hub = "(\u672A\u914D\u7F6E)";
11922
12533
  try {
11923
- const cfg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "config.json"), "utf-8"));
12534
+ const cfg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path8.join)((0, import_node_os8.homedir)(), DEFAULT_CONFIG_DIR, "config.json"), "utf-8"));
11924
12535
  hub = cfg.relayUrl ?? hub;
11925
12536
  } catch {
11926
12537
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli-remote/local",
3
- "version": "0.3.3",
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": {
@@ -30,6 +30,7 @@
30
30
  "build": "node scripts/build.mjs",
31
31
  "postinstall": "node scripts/fix-spawn-helper.mjs",
32
32
  "typecheck": "tsc --noEmit",
33
+ "test": "vitest run",
33
34
  "prepublishOnly": "pnpm build"
34
35
  },
35
36
  "dependencies": {