@cli-remote/local 0.0.4 → 0.1.1

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 +753 -96
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2265,7 +2265,7 @@ var require_websocket = __commonJS({
2265
2265
  var http = require("http");
2266
2266
  var net = require("net");
2267
2267
  var tls = require("tls");
2268
- var { randomBytes, createHash } = require("crypto");
2268
+ var { randomBytes: randomBytes2, createHash } = require("crypto");
2269
2269
  var { Duplex, Readable } = require("stream");
2270
2270
  var { URL: URL2 } = require("url");
2271
2271
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2803,7 +2803,7 @@ var require_websocket = __commonJS({
2803
2803
  }
2804
2804
  }
2805
2805
  const defaultPort = isSecure ? 443 : 80;
2806
- const key = randomBytes(16).toString("base64");
2806
+ const key = randomBytes2(16).toString("base64");
2807
2807
  const request = isSecure ? https.request : http.request;
2808
2808
  const protocolSet = /* @__PURE__ */ new Set();
2809
2809
  let perMessageDeflate;
@@ -9939,10 +9939,10 @@ var require_lib = __commonJS({
9939
9939
  });
9940
9940
 
9941
9941
  // src/index.ts
9942
- var import_node_fs3 = require("node:fs");
9943
- var import_node_child_process2 = require("node:child_process");
9944
- var import_node_os6 = require("node:os");
9945
- var import_node_path6 = require("node:path");
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");
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,10 +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
9961
  var import_node_readline = require("node:readline");
9961
- var import_promises3 = require("node:fs/promises");
9962
- var import_node_os5 = require("node:os");
9963
- var import_node_path5 = require("node:path");
9962
+ var import_node_fs4 = require("node:fs");
9963
+ var import_node_os6 = require("node:os");
9964
+ var import_node_path6 = require("node:path");
9964
9965
 
9965
9966
  // ../shared/src/codec.ts
9966
9967
  var import_msgpack = __toESM(require_dist(), 1);
@@ -10077,6 +10078,8 @@ function resolveCommand(cmd) {
10077
10078
  return { file: "codex", args: [] };
10078
10079
  case "kimi-cli":
10079
10080
  return { file: "kimi", args: [] };
10081
+ case "pi":
10082
+ return { file: "pi", args: [] };
10080
10083
  default:
10081
10084
  return { file: cmd, args: [] };
10082
10085
  }
@@ -10507,15 +10510,100 @@ async function exists(p) {
10507
10510
  }
10508
10511
  }
10509
10512
 
10513
+ // src/gateway-api.ts
10514
+ var import_node_http = require("node:http");
10515
+ var import_node_fs3 = require("node:fs");
10516
+ var import_node_os4 = require("node:os");
10517
+ var import_node_path4 = require("node:path");
10518
+ var DEFAULT_API_PORT = 18790;
10519
+ function apiPortFromEnv() {
10520
+ return Number(process.env.CLI_LOCAL_API_PORT) || DEFAULT_API_PORT;
10521
+ }
10522
+ function readConfiguredPort() {
10523
+ if (process.env.CLI_LOCAL_API_PORT) return apiPortFromEnv();
10524
+ try {
10525
+ const cfg = JSON.parse(
10526
+ (0, import_node_fs3.readFileSync)((0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, "config.json"), "utf-8")
10527
+ );
10528
+ return Number(cfg.gateway?.apiPort) || DEFAULT_API_PORT;
10529
+ } catch {
10530
+ return DEFAULT_API_PORT;
10531
+ }
10532
+ }
10533
+ function startControlApi(opts) {
10534
+ const server = (0, import_node_http.createServer)((req, res) => {
10535
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
10536
+ const route = `${req.method} ${url.pathname}`;
10537
+ const json = (code, body) => {
10538
+ res.writeHead(code, { "content-type": "application/json" });
10539
+ res.end(JSON.stringify(body));
10540
+ };
10541
+ if (route === "GET /healthz") return json(200, { ok: true });
10542
+ if (route === "GET /status") return json(200, opts.getStatus());
10543
+ if (route === "GET /sessions") {
10544
+ return json(200, { sessions: opts.getStatus().sessions });
10545
+ }
10546
+ if (route === "GET /logs") {
10547
+ const n = Math.min(500, Math.max(1, Number(url.searchParams.get("n")) || 200));
10548
+ const lines = opts.getLogs().slice(-n);
10549
+ return json(200, { lines });
10550
+ }
10551
+ if (route === "POST /stop") {
10552
+ json(200, { ok: true });
10553
+ opts.onStop();
10554
+ return;
10555
+ }
10556
+ const killMatch = /^POST \/sessions\/([\w-]+)\/kill$/.exec(`${req.method} ${url.pathname}`);
10557
+ if (killMatch) {
10558
+ const ok = opts.killSession(killMatch[1]);
10559
+ return json(ok ? 200 : 404, ok ? { ok: true } : { ok: false, error: "no such session" });
10560
+ }
10561
+ json(404, { error: "not found" });
10562
+ });
10563
+ server.listen(opts.port, "127.0.0.1");
10564
+ return {
10565
+ port: opts.port,
10566
+ close: () => server.close()
10567
+ };
10568
+ }
10569
+ async function apiGet(port, path, timeoutMs = 800) {
10570
+ try {
10571
+ const ctrl = new AbortController();
10572
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
10573
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
10574
+ signal: ctrl.signal
10575
+ });
10576
+ clearTimeout(timer);
10577
+ if (!res.ok) return null;
10578
+ return await res.json();
10579
+ } catch {
10580
+ return null;
10581
+ }
10582
+ }
10583
+ async function apiPost(port, path, timeoutMs = 800) {
10584
+ try {
10585
+ const ctrl = new AbortController();
10586
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
10587
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
10588
+ method: "POST",
10589
+ signal: ctrl.signal
10590
+ });
10591
+ clearTimeout(timer);
10592
+ return res.ok;
10593
+ } catch {
10594
+ return false;
10595
+ }
10596
+ }
10597
+
10510
10598
  // src/auth.ts
10511
10599
  var import_node_child_process = require("node:child_process");
10512
10600
  var import_node_crypto = require("node:crypto");
10513
10601
  var import_promises2 = require("node:fs/promises");
10514
- var import_node_os4 = require("node:os");
10515
- var import_node_path4 = require("node:path");
10602
+ var import_node_os5 = require("node:os");
10603
+ var import_node_path5 = require("node:path");
10516
10604
  var import_qrcode = __toESM(require_lib(), 1);
10517
10605
  function credentialsPath() {
10518
- return (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10606
+ return (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10519
10607
  }
10520
10608
  async function loadCredentials() {
10521
10609
  try {
@@ -10528,19 +10616,13 @@ async function loadCredentials() {
10528
10616
  }
10529
10617
  async function saveCredentials(creds) {
10530
10618
  const path = credentialsPath();
10531
- await (0, import_promises2.mkdir)((0, import_node_path4.join)(path, ".."), { recursive: true });
10619
+ await (0, import_promises2.mkdir)((0, import_node_path5.join)(path, ".."), { recursive: true });
10532
10620
  await (0, import_promises2.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10533
10621
  try {
10534
10622
  await (0, import_promises2.chmod)(path, 384);
10535
10623
  } catch {
10536
10624
  }
10537
10625
  }
10538
- async function clearCredentials() {
10539
- try {
10540
- await (0, import_promises2.writeFile)(credentialsPath(), "");
10541
- } catch {
10542
- }
10543
- }
10544
10626
  function baseUrlFromWsUrl(wsUrl) {
10545
10627
  const http = wsUrl.replace(/^ws/, "http");
10546
10628
  try {
@@ -10568,9 +10650,25 @@ function openBrowser(url) {
10568
10650
  } catch {
10569
10651
  }
10570
10652
  }
10653
+ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
10654
+ const login = await postJson(`${baseUrl}/api/login`, { email, password });
10655
+ if (login.status !== 200) {
10656
+ const reason = login.data?.reason;
10657
+ throw new AuthError(`\u767B\u5F55\u5931\u8D25: ${reason ?? login.status}`);
10658
+ }
10659
+ const token = login.data.token;
10660
+ if (!token) throw new AuthError("\u670D\u52A1\u7AEF\u672A\u8FD4\u56DE\u4F1A\u8BDD token");
10661
+ const me = await fetch(`${baseUrl}/api/me`, {
10662
+ headers: { authorization: `Bearer ${token}` }
10663
+ });
10664
+ if (!me.ok) throw new AuthError(`\u83B7\u53D6\u8D26\u53F7\u4FE1\u606F\u5931\u8D25 (${me.status})`);
10665
+ const data = await me.json();
10666
+ if (!data.registrationKey) throw new AuthError("\u8D26\u53F7\u4FE1\u606F\u91CC\u6CA1\u6709 registrationKey");
10667
+ return { registrationKey: data.registrationKey, user: data.user?.email ?? email };
10668
+ }
10571
10669
  async function mergeLocalConfig(patch) {
10572
- const dir = (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR);
10573
- const path = (0, import_node_path4.join)(dir, CONFIG_FILE);
10670
+ const dir = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR);
10671
+ const path = (0, import_node_path5.join)(dir, CONFIG_FILE);
10574
10672
  let existing = {};
10575
10673
  try {
10576
10674
  existing = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
@@ -10591,7 +10689,7 @@ async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
10591
10689
  const res = await postJson(`${baseUrl}/oauth/device_authorization`, {
10592
10690
  registrationKey,
10593
10691
  device_id: deviceId,
10594
- device_name: (0, import_node_os4.hostname)(),
10692
+ device_name: (0, import_node_os5.hostname)(),
10595
10693
  platform: process.platform
10596
10694
  });
10597
10695
  if (res.status !== 200) {
@@ -10680,6 +10778,20 @@ async function ensureFreshAccessToken(baseUrl, creds) {
10680
10778
  }
10681
10779
  return refreshInFlight;
10682
10780
  }
10781
+ async function ensureMachineId() {
10782
+ const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10783
+ try {
10784
+ const cfg = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
10785
+ if (typeof cfg.machineId === "string" && cfg.machineId) return cfg.machineId;
10786
+ } catch {
10787
+ }
10788
+ const machineId = "m_" + (0, import_node_crypto.randomBytes)(16).toString("hex");
10789
+ await mergeLocalConfig({ machineId });
10790
+ return machineId;
10791
+ }
10792
+ async function stableDeviceId() {
10793
+ return "dev_" + await ensureMachineId();
10794
+ }
10683
10795
  async function ensureCredentials(baseUrl, registrationKey) {
10684
10796
  const existing = await loadCredentials();
10685
10797
  if (existing) {
@@ -10689,14 +10801,17 @@ async function ensureCredentials(baseUrl, registrationKey) {
10689
10801
  console.error(`[local] ${e.message}\uFF0C\u91CD\u65B0\u8D70\u6388\u6743\u6D41\u7A0B`);
10690
10802
  }
10691
10803
  }
10692
- return deviceFlowAuthorize(baseUrl, registrationKey, "dev_" + (0, import_node_crypto.randomUUID)());
10804
+ return deviceFlowAuthorize(baseUrl, registrationKey, await stableDeviceId());
10693
10805
  }
10694
10806
 
10695
10807
  // src/client.ts
10808
+ function isPermanentAuthFailure(e) {
10809
+ return e instanceof AuthError && e.message.includes("invalid_grant");
10810
+ }
10696
10811
  async function loadConfig() {
10697
- const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10812
+ const path = (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10698
10813
  try {
10699
- return JSON.parse(await (0, import_promises3.readFile)(path, "utf-8"));
10814
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf-8"));
10700
10815
  } catch {
10701
10816
  return {};
10702
10817
  }
@@ -10710,6 +10825,28 @@ function buildWhitelist(cfg) {
10710
10825
  function isTTY() {
10711
10826
  return process.stdin.isTTY === true;
10712
10827
  }
10828
+ function detectCommands() {
10829
+ const bins = [
10830
+ ["claude", "claude"],
10831
+ ["codex", "codex"],
10832
+ ["kimi-cli", "kimi"],
10833
+ ["pi", "pi"]
10834
+ ];
10835
+ const found = ["shell"];
10836
+ for (const [cmd, bin] of bins) {
10837
+ let ok = false;
10838
+ if (process.platform === "win32") {
10839
+ ok = (0, import_node_child_process2.spawnSync)("where", [bin], { timeout: 5e3 }).status === 0;
10840
+ } else {
10841
+ const shell = process.env.SHELL ?? "/bin/bash";
10842
+ ok = (0, import_node_child_process2.spawnSync)(shell, ["-l", "-c", `command -v ${bin}`], {
10843
+ timeout: 5e3
10844
+ }).status === 0;
10845
+ }
10846
+ if (ok) found.push(cmd);
10847
+ }
10848
+ return found;
10849
+ }
10713
10850
  async function readInput(prompt) {
10714
10851
  const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
10715
10852
  try {
@@ -10748,18 +10885,42 @@ async function setupWizard(config, opts) {
10748
10885
  const base = baseUrlFromWsUrl(url);
10749
10886
  console.log("");
10750
10887
  console.log("[local] \u7F3A\u5C11 registrationKey\u3002");
10751
- console.log(`[local] \u5DF2\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u767B\u5F55 ${base} \u5E76\u5728\u300C\u6211\u7684\u8BBE\u5907\u300D\u9875\u590D\u5236 registrationKey`);
10752
- openBrowser(base);
10753
10888
  if (!isTTY()) {
10889
+ openBrowser(base);
10754
10890
  console.log("[local] \u5F53\u524D\u975E\u4EA4\u4E92\u7EC8\u7AEF\uFF1A\u62FF\u5230 key \u540E\u5199\u5165 ~/.cli-remote/config.json \u518D\u8FD0\u884C\u3002");
10755
10891
  return null;
10756
10892
  }
10757
- const ans = (await readInput("\u7C98\u8D34 registrationKey: ")).trim();
10758
- if (!ans) {
10759
- console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10760
- return null;
10893
+ console.log(" 1) \u767B\u5F55\u8D26\u53F7\u81EA\u52A8\u83B7\u53D6\uFF08\u63A8\u8350\uFF09");
10894
+ console.log(" 2) \u4ECE\u7F51\u9875\u300C\u63A7\u5236\u9762\u677F\u300D\u590D\u5236\u7C98\u8D34");
10895
+ const mode = (await readInput("\u9009\u62E9 [1]: ")).trim();
10896
+ if (mode === "2") {
10897
+ openBrowser(base);
10898
+ console.log(`[local] \u5DF2\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u767B\u5F55 ${base} \u5E76\u5728\u300C\u63A7\u5236\u9762\u677F\u300D\u9875\u590D\u5236 registrationKey`);
10899
+ const ans = (await readInput("\u7C98\u8D34 registrationKey: ")).trim();
10900
+ if (!ans) {
10901
+ console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10902
+ return null;
10903
+ }
10904
+ registrationKey = ans;
10905
+ } else {
10906
+ for (; ; ) {
10907
+ const email = (await readInput("\u90AE\u7BB1: ")).trim();
10908
+ const password = (await readInput("\u5BC6\u7801: ")).trim();
10909
+ if (!email || !password) {
10910
+ console.log("[local] \u90AE\u7BB1\u6216\u5BC6\u7801\u4E3A\u7A7A\uFF0C\u91CD\u8BD5\uFF08\u6216 Ctrl+C \u9000\u51FA\uFF09\u3002");
10911
+ continue;
10912
+ }
10913
+ try {
10914
+ const res = await loginAndFetchRegistrationKey(base, email, password);
10915
+ registrationKey = res.registrationKey;
10916
+ console.log(`[local] \u5DF2\u767B\u5F55 ${res.user}\uFF0C\u83B7\u53D6 registrationKey \u6210\u529F`);
10917
+ break;
10918
+ } catch (e) {
10919
+ console.error(`[local] ${e.message}`);
10920
+ console.log("[local] \u91CD\u8BD5\uFF08\u6216 Ctrl+C \u9000\u51FA\uFF09\u3002");
10921
+ }
10922
+ }
10761
10923
  }
10762
- registrationKey = ans;
10763
10924
  changed = true;
10764
10925
  }
10765
10926
  if (changed) {
@@ -10787,14 +10948,123 @@ async function startLocal(opts = {}) {
10787
10948
  (data) => safeSend(data),
10788
10949
  (msg) => console.log(`[local] ${msg}`)
10789
10950
  );
10790
- let credentials = await ensureCredentials(baseUrl, registrationKey);
10951
+ const other = acquireSingleInstanceLock();
10952
+ if (other !== null) {
10953
+ console.error(`[local] \u53E6\u4E00\u4E2A cli-local \u6B63\u5728\u8FD0\u884C (pid ${other})\uFF0C\u62D2\u7EDD\u53CC\u5F00\u3002`);
10954
+ console.error("[local] \u53CC\u5F00\u4F1A\u4E92\u76F8\u5237\u65B0 token \u5BFC\u81F4\u8BBE\u5907\u88AB\u670D\u52A1\u7AEF\u540A\u9500\u3002");
10955
+ console.error("[local] \u540E\u53F0\u5E38\u9A7B\u8BF7\u7528: cli-local gateway start / restart / status");
10956
+ process.exit(1);
10957
+ }
10958
+ const startedAt = Date.now();
10959
+ const logRing = [];
10960
+ if (opts.daemon) {
10961
+ writeGatewayPid();
10962
+ patchConsoleIntoRing(logRing);
10963
+ }
10964
+ let credentials;
10965
+ if (opts.daemon) {
10966
+ const existing = await loadCredentials();
10967
+ if (!existing) {
10968
+ credentials = {
10969
+ deviceId: "",
10970
+ accessToken: "",
10971
+ refreshToken: "",
10972
+ accessTokenExpiresAt: 0
10973
+ };
10974
+ } else {
10975
+ credentials = existing;
10976
+ try {
10977
+ credentials = await ensureFreshAccessToken(baseUrl, existing);
10978
+ } catch {
10979
+ }
10980
+ }
10981
+ } else {
10982
+ credentials = await ensureCredentials(baseUrl, registrationKey);
10983
+ }
10984
+ const commands = detectCommands();
10791
10985
  console.log(`[local] connecting to ${url}`);
10792
- console.log(`[local] deviceId=${credentials.deviceId}`);
10986
+ console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
10793
10987
  console.log(`[local] whitelist: ${whitelist.size} commands`);
10988
+ console.log(`[local] available CLIs: ${commands.join(", ")}`);
10989
+ let phase = "starting";
10990
+ let phaseReason = "";
10991
+ let reconnectAttempts = 0;
10992
+ const getStatus = () => ({
10993
+ phase,
10994
+ ...phaseReason ? { reason: phaseReason } : {},
10995
+ deviceId: credentials.deviceId || null,
10996
+ hubUrl: url,
10997
+ uptimeMs: Date.now() - startedAt,
10998
+ reconnectAttempts,
10999
+ accessTokenExpiresAt: credentials.accessTokenExpiresAt || null,
11000
+ sessions: pty.list().map((s) => ({
11001
+ sessionId: s.id,
11002
+ cmd: s.cmd,
11003
+ startedAt: s.startedAt
11004
+ }))
11005
+ });
11006
+ function setPhase(p, reason = "") {
11007
+ phase = p;
11008
+ phaseReason = reason;
11009
+ }
11010
+ let pairingTimer = null;
11011
+ let pairingCredMtime = 0;
11012
+ function enterNeedsPairing(reason) {
11013
+ setPhase("needs-pairing", reason);
11014
+ console.error(`[local] \u9700\u8981\u91CD\u65B0\u6388\u6743: ${reason}`);
11015
+ console.error("[local] \u5728\u4EFB\u610F\u7EC8\u7AEF\u8FD0\u884C: cli-local pair");
11016
+ console.error("[local] \uFF08\u672C\u8FDB\u7A0B\u4FDD\u6301\u8FD0\u884C\uFF0CPTY \u4F1A\u8BDD\u4FDD\u6D3B\uFF1B\u914D\u5BF9\u5B8C\u6210\u540E\u81EA\u52A8\u91CD\u8FDE\uFF09");
11017
+ if (pairingTimer) return;
11018
+ try {
11019
+ pairingCredMtime = (0, import_node_fs4.statSync)(credentialsPathOf()).mtimeMs;
11020
+ } catch {
11021
+ pairingCredMtime = 0;
11022
+ }
11023
+ pairingTimer = setInterval(async () => {
11024
+ let mtime;
11025
+ try {
11026
+ mtime = (0, import_node_fs4.statSync)(credentialsPathOf()).mtimeMs;
11027
+ } catch {
11028
+ return;
11029
+ }
11030
+ if (mtime <= pairingCredMtime) return;
11031
+ const fresh = await loadCredentials();
11032
+ if (!fresh?.deviceId || !fresh?.refreshToken) return;
11033
+ pairingCredMtime = mtime;
11034
+ credentials = fresh;
11035
+ console.log("[local] \u68C0\u6D4B\u5230\u65B0\u51ED\u8BC1\uFF0C\u91CD\u8FDE\u4E2D\u2026");
11036
+ if (pairingTimer) clearInterval(pairingTimer);
11037
+ pairingTimer = null;
11038
+ reconnectAttempts = 0;
11039
+ setPhase("connecting");
11040
+ connect();
11041
+ }, 2e3);
11042
+ }
11043
+ let controlApi = null;
11044
+ if (opts.daemon) {
11045
+ const port = readConfiguredPort();
11046
+ controlApi = startControlApi({
11047
+ port,
11048
+ getStatus,
11049
+ getLogs: () => [...logRing],
11050
+ onStop: () => {
11051
+ console.log("[local] \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF08\u63A7\u5236 API\uFF09");
11052
+ exitProcess(0);
11053
+ },
11054
+ killSession: (id) => {
11055
+ try {
11056
+ pty.kill(id);
11057
+ return true;
11058
+ } catch {
11059
+ return false;
11060
+ }
11061
+ }
11062
+ });
11063
+ console.log(`[local] control api: http://127.0.0.1:${port} (\u4EC5\u672C\u673A)`);
11064
+ }
10794
11065
  let ws = null;
10795
11066
  let heartbeatTimer = null;
10796
11067
  let reconnectTimer = null;
10797
- let reconnectAttempts = 0;
10798
11068
  let userExitCode = null;
10799
11069
  function stopHeartbeat() {
10800
11070
  if (heartbeatTimer) clearInterval(heartbeatTimer);
@@ -10806,7 +11076,8 @@ async function startLocal(opts = {}) {
10806
11076
  t: "auth",
10807
11077
  role: "bridge",
10808
11078
  deviceId: credentials.deviceId,
10809
- token: credentials.accessToken
11079
+ token: credentials.accessToken,
11080
+ commands
10810
11081
  })
10811
11082
  );
10812
11083
  }
@@ -10816,6 +11087,7 @@ async function startLocal(opts = {}) {
10816
11087
  ws = sock;
10817
11088
  sock.on("open", () => {
10818
11089
  console.log("[local] connected");
11090
+ if (phase !== "needs-pairing") setPhase("connecting");
10819
11091
  sendAuth();
10820
11092
  stopHeartbeat();
10821
11093
  heartbeatTimer = setInterval(() => {
@@ -10832,6 +11104,7 @@ async function startLocal(opts = {}) {
10832
11104
  process.exit(userExitCode);
10833
11105
  }
10834
11106
  console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
11107
+ if (phase !== "needs-pairing") setPhase("reconnecting");
10835
11108
  scheduleReconnect();
10836
11109
  });
10837
11110
  sock.on("error", (err) => {
@@ -10840,6 +11113,7 @@ async function startLocal(opts = {}) {
10840
11113
  }
10841
11114
  async function scheduleReconnect() {
10842
11115
  if (reconnectTimer || userExitCode !== null) return;
11116
+ if (phase === "needs-pairing") return;
10843
11117
  const idx = Math.min(reconnectAttempts, RECONNECT_DELAYS_MS.length - 1);
10844
11118
  const delay = RECONNECT_DELAYS_MS[idx];
10845
11119
  reconnectAttempts++;
@@ -10849,11 +11123,13 @@ async function startLocal(opts = {}) {
10849
11123
  ensureFreshAccessToken(baseUrl, credentials).then((creds) => {
10850
11124
  credentials = creds;
10851
11125
  connect();
10852
- }).catch(async (e) => {
10853
- await clearCredentials();
10854
- console.error(`[local] ${e.message}`);
10855
- console.error("[local] \u51ED\u8BC1\u5DF2\u5931\u6548\u5E76\u6E05\u9664\uFF1B\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10856
- exitProcess(1);
11126
+ }).catch((e) => {
11127
+ if (isPermanentAuthFailure(e)) {
11128
+ enterNeedsPairing(e.message);
11129
+ return;
11130
+ }
11131
+ console.error(`[local] token \u5237\u65B0\u6682\u65F6\u5931\u8D25: ${e.message}`);
11132
+ scheduleReconnect();
10857
11133
  });
10858
11134
  }, delay);
10859
11135
  }
@@ -10861,6 +11137,10 @@ async function startLocal(opts = {}) {
10861
11137
  userExitCode = code;
10862
11138
  if (reconnectTimer) clearTimeout(reconnectTimer);
10863
11139
  reconnectTimer = null;
11140
+ if (pairingTimer) clearInterval(pairingTimer);
11141
+ pairingTimer = null;
11142
+ controlApi?.close();
11143
+ if (opts.daemon) removeGatewayPid();
10864
11144
  transfers.abortAll();
10865
11145
  pty.killAll();
10866
11146
  for (const [, logger] of loggers) logger.logExit(-1);
@@ -10889,6 +11169,7 @@ async function startLocal(opts = {}) {
10889
11169
  console.log(`[local] reconnected after ${reconnectAttempts} attempt(s); ${pty.list().length} PTY session(s) alive`);
10890
11170
  }
10891
11171
  reconnectAttempts = 0;
11172
+ setPhase("connected");
10892
11173
  console.log(`[local] auth ok, deviceId=${frame.channelId}`);
10893
11174
  break;
10894
11175
  case "auth-err":
@@ -10931,20 +11212,26 @@ async function startLocal(opts = {}) {
10931
11212
  }
10932
11213
  }
10933
11214
  async function handleAuthErr(reason) {
10934
- if (reason === "access token expired") {
11215
+ if (reason === "access token expired" || reason === "invalid access token") {
10935
11216
  try {
10936
11217
  credentials = await ensureFreshAccessToken(baseUrl, credentials);
10937
11218
  console.log("[local] token refreshed, retrying auth");
10938
11219
  sendAuth();
10939
11220
  return;
10940
11221
  } catch (e) {
10941
- console.error(`[local] ${e.message}`);
11222
+ if (isPermanentAuthFailure(e)) {
11223
+ enterNeedsPairing(e.message);
11224
+ return;
11225
+ }
11226
+ console.error(`[local] token \u5237\u65B0\u6682\u65F6\u5931\u8D25\uFF08${e.message}\uFF09\uFF0C\u8F6C\u5165\u91CD\u8FDE`);
11227
+ try {
11228
+ ws?.close();
11229
+ } catch {
11230
+ }
11231
+ return;
10942
11232
  }
10943
11233
  }
10944
- await clearCredentials();
10945
- console.error("[local] auth failed:", reason);
10946
- console.error("[local] \u51ED\u8BC1\u5DF2\u6E05\u9664\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10947
- exitProcess(1);
11234
+ enterNeedsPairing(reason);
10948
11235
  }
10949
11236
  function handleSpawn(frame) {
10950
11237
  const sessionId = frame.sessionId;
@@ -11078,7 +11365,8 @@ async function startLocal(opts = {}) {
11078
11365
  encodeFrame({
11079
11366
  t: "replay-res",
11080
11367
  sessionId: frame.sessionId,
11081
- frames
11368
+ frames,
11369
+ exists: pty.has(frame.sessionId)
11082
11370
  })
11083
11371
  );
11084
11372
  }
@@ -11089,7 +11377,57 @@ async function startLocal(opts = {}) {
11089
11377
  }
11090
11378
  process.on("SIGINT", () => cleanup(0));
11091
11379
  process.on("SIGTERM", () => cleanup(0));
11092
- connect();
11380
+ if (credentials.deviceId) {
11381
+ setPhase("connecting");
11382
+ connect();
11383
+ } else {
11384
+ enterNeedsPairing("\u65E0\u51ED\u8BC1\uFF08credentials.json \u7F3A\u5931\u6216\u4E3A\u7A7A\uFF09");
11385
+ }
11386
+ }
11387
+ function credentialsPathOf() {
11388
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "credentials.json");
11389
+ }
11390
+ function instanceLockPath() {
11391
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "instance.lock");
11392
+ }
11393
+ function processAlive(pid) {
11394
+ try {
11395
+ process.kill(pid, 0);
11396
+ return true;
11397
+ } catch {
11398
+ return false;
11399
+ }
11400
+ }
11401
+ function acquireSingleInstanceLock() {
11402
+ const path = instanceLockPath();
11403
+ try {
11404
+ const pid = Number((0, import_node_fs4.readFileSync)(path, "utf-8").trim());
11405
+ if (Number.isFinite(pid) && pid !== process.pid && processAlive(pid)) {
11406
+ return pid;
11407
+ }
11408
+ } catch {
11409
+ }
11410
+ try {
11411
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(path, ".."), { recursive: true });
11412
+ (0, import_node_fs4.writeFileSync)(path, String(process.pid));
11413
+ } catch {
11414
+ }
11415
+ return null;
11416
+ }
11417
+ function patchConsoleIntoRing(ring) {
11418
+ const CAP = 500;
11419
+ const push = (line) => {
11420
+ ring.push(line);
11421
+ if (ring.length > CAP) ring.splice(0, ring.length - CAP);
11422
+ };
11423
+ const wrap = (orig) => {
11424
+ return (...args) => {
11425
+ push(args.map((a) => typeof a === "string" ? a : String(a)).join(" "));
11426
+ orig(...args);
11427
+ };
11428
+ };
11429
+ console.log = wrap(console.log.bind(console));
11430
+ console.error = wrap(console.error.bind(console));
11093
11431
  }
11094
11432
  function toUint8(raw) {
11095
11433
  if (raw instanceof Buffer) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
@@ -11099,6 +11437,37 @@ function toUint8(raw) {
11099
11437
  }
11100
11438
  throw new Error("unexpected raw type");
11101
11439
  }
11440
+ function gatewayPidFile() {
11441
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11442
+ }
11443
+ function writeGatewayPid() {
11444
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR), { recursive: true });
11445
+ (0, import_node_fs4.writeFileSync)(gatewayPidFile(), String(process.pid));
11446
+ }
11447
+ function removeGatewayPid() {
11448
+ try {
11449
+ (0, import_node_fs4.rmSync)(gatewayPidFile());
11450
+ } catch {
11451
+ }
11452
+ }
11453
+ function gatewayPid() {
11454
+ try {
11455
+ const pid = Number((0, import_node_fs4.readFileSync)(gatewayPidFile(), "utf-8").trim());
11456
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
11457
+ } catch {
11458
+ return null;
11459
+ }
11460
+ }
11461
+ function gatewayAlive() {
11462
+ const pid = gatewayPid();
11463
+ if (pid === null) return false;
11464
+ try {
11465
+ process.kill(pid, 0);
11466
+ return true;
11467
+ } catch {
11468
+ return false;
11469
+ }
11470
+ }
11102
11471
 
11103
11472
  // src/index.ts
11104
11473
  var import_meta = {};
@@ -11117,15 +11486,15 @@ function resolveHere() {
11117
11486
  if (DEBUG) console.error("[debug] cannot resolve script path");
11118
11487
  return;
11119
11488
  }
11120
- const hereDir = (0, import_node_path6.dirname)(here);
11489
+ const hereDir = (0, import_node_path7.dirname)(here);
11121
11490
  const candidates = [
11122
- (0, import_node_path6.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11123
- (0, import_node_path6.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11124
- (0, import_node_path6.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
11491
+ (0, import_node_path7.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11492
+ (0, import_node_path7.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11493
+ (0, import_node_path7.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
11125
11494
  ];
11126
11495
  let prebuildsDir = null;
11127
11496
  for (const c of candidates) {
11128
- if ((0, import_node_fs3.existsSync)(c)) {
11497
+ if ((0, import_node_fs5.existsSync)(c)) {
11129
11498
  prebuildsDir = c;
11130
11499
  break;
11131
11500
  }
@@ -11135,11 +11504,11 @@ function resolveHere() {
11135
11504
  return;
11136
11505
  }
11137
11506
  let n = 0;
11138
- for (const sub of (0, import_node_fs3.readdirSync)(prebuildsDir)) {
11139
- const helper = (0, import_node_path6.join)(prebuildsDir, sub, "spawn-helper");
11140
- if ((0, import_node_fs3.existsSync)(helper)) {
11507
+ for (const sub of (0, import_node_fs5.readdirSync)(prebuildsDir)) {
11508
+ const helper = (0, import_node_path7.join)(prebuildsDir, sub, "spawn-helper");
11509
+ if ((0, import_node_fs5.existsSync)(helper)) {
11141
11510
  try {
11142
- (0, import_node_fs3.chmodSync)(helper, 493);
11511
+ (0, import_node_fs5.chmodSync)(helper, 493);
11143
11512
  n++;
11144
11513
  } catch {
11145
11514
  }
@@ -11150,56 +11519,63 @@ function resolveHere() {
11150
11519
  var argUrl = process.argv[2];
11151
11520
  var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
11152
11521
  if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl === "help")) {
11153
- console.log(`cli-local \u2014 local PTY agent for cli-remote
11522
+ console.log(`cli-local \u2014 local PTY agent for cli-remote (gateway \u5E38\u9A7B\u670D\u52A1)
11154
11523
 
11155
11524
  usage:
11156
- cli-local [HUB_URL]
11157
- cli-local install [HUB_URL] \u5B89\u88C5 macOS launchd \u5E38\u9A7B\u670D\u52A1\uFF08\u5F00\u673A\u81EA\u542F + \u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\uFF09
11158
- cli-local uninstall \u5378\u8F7D launchd \u5E38\u9A7B\u670D\u52A1
11525
+ cli-local \u65E0 gateway \u4E14\u5DF2\u914D\u7F6E\u65F6\u524D\u53F0\u8FD0\u884C\uFF1B\u5426\u5219\u663E\u793A\u72B6\u6001
11526
+ cli-local pair \u91CD\u65B0\u6388\u6743\uFF08\u540A\u9500/\u8FC7\u671F\u540E\u91CD\u914D\u5BF9\uFF1Bgateway \u70ED\u52A0\u8F7D\uFF09
11527
+ cli-local gateway start \u540E\u53F0\u542F\u52A8\u5E38\u9A7B gateway\uFF08\u63A8\u8350\uFF09
11528
+ cli-local gateway stop|restart|status|logs [-f] [-n N]
11529
+ cli-local install [HUB_URL] \u5B89\u88C5 macOS launchd \u5E38\u9A7B\u670D\u52A1
11530
+ cli-local uninstall \u5378\u8F7D launchd \u5E38\u9A7B\u670D\u52A1
11531
+
11532
+ \u9996\u6B21\u8FD0\u884C\uFF08\u65E0\u914D\u7F6E\uFF09\u4F1A\u8FDB\u5165\u5F15\u5BFC\uFF1Ahub \u5730\u5740 \u2192 registrationKey \u2192 Device Flow \u6388\u6743\u3002
11533
+ \u6388\u6743\u540E\u5EFA\u8BAE\u7528 cli-local gateway start \u5E38\u9A7B\uFF08\u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\u3001PTY \u4FDD\u6D3B\uFF09\u3002
11159
11534
 
11160
- \u9996\u6B21\u8FD0\u884C\uFF08\u65E0\u914D\u7F6E\uFF09\u4F1A\u8FDB\u5165\u5F15\u5BFC\uFF1A
11161
- 1. \u63D0\u793A hub \u5730\u5740\uFF08\u56DE\u8F66\u7528\u9ED8\u8BA4\uFF09
11162
- 2. \u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668 \u2192 \u767B\u5F55 hub\uFF0C\u4ECE\u300C\u6211\u7684\u8BBE\u5907\u300D\u9875\u590D\u5236 registrationKey
11163
- 3. \u56DE\u5230\u7EC8\u7AEF\u7C98\u8D34 key \u2192 \u81EA\u52A8\u5199\u5165 ~/.cli-remote/config.json
11164
- 4. \u968F\u540E\u8D70 OAuth Device Flow\uFF08RFC 8628\uFF09\uFF1A\u81EA\u52A8\u6253\u5F00\u6388\u6743\u9875 \u2192 \u6279\u51C6\u672C\u8BBE\u5907
11165
- 5. \u51ED\u8BC1\u5B58\u5165 ~/.cli-remote/credentials.json\uFF080600\uFF09\uFF0C\u4E4B\u540E\u81EA\u52A8\u7EED\u671F
11535
+ \u88AB\u540A\u9500/\u51ED\u8BC1\u8FC7\u671F\u65F6 gateway \u4E0D\u4F1A\u9000\u51FA\uFF0C\u800C\u662F\u8FDB\u5165 needs-pairing \u7B49\u5F85\uFF1B
11536
+ \u53E6\u5F00\u7EC8\u7AEF\u8DD1 cli-local pair \u5199\u5165\u65B0\u51ED\u8BC1\u540E\u81EA\u52A8\u91CD\u8FDE\uFF08\u65E0\u9700\u91CD\u542F gateway\uFF09\u3002
11166
11537
 
11167
- \u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\uFF1Ahub \u91CD\u542F/\u7F51\u7EDC\u95EA\u65AD\u540E\u8FDB\u7A0B\u4E0D\u9000\u51FA\uFF0CPTY \u4F1A\u8BDD\u4FDD\u6D3B\u5E76\u81EA\u52A8\u91CD\u8FDE\u3002
11538
+ \u672C\u5730\u63A7\u5236 API\uFF08\u4EC5 127.0.0.1\uFF0Cgateway \u8FD0\u884C\u65F6\uFF09\uFF1A
11539
+ GET /status /healthz /logs /sessions POST /stop
11540
+ \u7AEF\u53E3\u9ED8\u8BA4 18790\uFF0Cconfig.json gateway.apiPort \u6216 CLI_LOCAL_API_PORT \u53EF\u6539
11168
11541
 
11169
11542
  environment:
11170
11543
  RELAY_URL hub ws url (overrides argv and config)
11171
11544
  REGISTRATION_KEY registration key (fallback of config.json)
11172
11545
  CLI_LOCAL_NO_BROWSER=1 \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF08\u53EA\u6253\u5370\u94FE\u63A5\uFF09
11546
+ CLI_LOCAL_API_PORT gateway \u63A7\u5236API\u7AEF\u53E3
11173
11547
  HOME where ~/.cli-remote/ lives
11174
11548
 
11175
11549
  config (~/.cli-remote/config.json):
11176
11550
  {
11177
11551
  "relayUrl": "wss://your-hub/ws",
11178
11552
  "registrationKey": "from-the-web-devices-page",
11553
+ "gateway": { "apiPort": 18790 },
11179
11554
  "whitelist": { "add": [], "remove": [] }
11180
11555
  }
11181
11556
 
11182
11557
  example:
11183
- cli-local wss://cli-remote.mkjs.net/ws
11184
- cli-local install # \u914D\u7F6E\u5E76\u6388\u6743\u540E\uFF0C\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1`);
11558
+ cli-local pair # \u9996\u6B21/\u91CD\u65B0\u6388\u6743
11559
+ cli-local gateway start # \u540E\u53F0\u5E38\u9A7B
11560
+ cli-local gateway status # \u67E5\u770B\u8FDE\u63A5\u72B6\u6001`);
11185
11561
  process.exit(0);
11186
11562
  }
11187
11563
  var LAUNCHD_LABEL = "net.cli-remote.local";
11188
11564
  function launchdPlistPath() {
11189
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11565
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11190
11566
  }
11191
11567
  function entryScript() {
11192
11568
  const a1 = process.argv[1];
11193
11569
  if (!a1 || a1.endsWith(".ts")) return null;
11194
11570
  try {
11195
- return (0, import_node_fs3.realpathSync)(a1);
11571
+ return (0, import_node_fs5.realpathSync)(a1);
11196
11572
  } catch {
11197
11573
  return a1;
11198
11574
  }
11199
11575
  }
11200
11576
  function run(cmd, args, okToFail = false) {
11201
11577
  try {
11202
- return (0, import_node_child_process2.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
11578
+ return (0, import_node_child_process3.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
11203
11579
  } catch (e) {
11204
11580
  if (okToFail) return null;
11205
11581
  throw e;
@@ -11210,7 +11586,7 @@ function launchdInstall(urlArg) {
11210
11586
  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`);
11211
11587
  process.exit(1);
11212
11588
  }
11213
- const home = (0, import_node_os6.homedir)();
11589
+ const home = (0, import_node_os7.homedir)();
11214
11590
  const entry = entryScript();
11215
11591
  if (!entry) {
11216
11592
  console.error("[local] \u5F53\u524D\u4EE5\u6E90\u7801\uFF08tsx\uFF09\u6A21\u5F0F\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
@@ -11218,28 +11594,28 @@ function launchdInstall(urlArg) {
11218
11594
  process.exit(1);
11219
11595
  }
11220
11596
  if (urlArg) {
11221
- const cfgDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR);
11222
- (0, import_node_fs3.mkdirSync)(cfgDir, { recursive: true });
11223
- const cfgPath = (0, import_node_path6.join)(cfgDir, "config.json");
11597
+ const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
11598
+ (0, import_node_fs5.mkdirSync)(cfgDir, { recursive: true });
11599
+ const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
11224
11600
  let cfg = {};
11225
11601
  try {
11226
- cfg = JSON.parse((0, import_node_fs3.readFileSync)(cfgPath, "utf-8"));
11602
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11227
11603
  } catch {
11228
11604
  }
11229
11605
  cfg.relayUrl = urlArg;
11230
- (0, import_node_fs3.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11606
+ (0, import_node_fs5.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11231
11607
  console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11232
11608
  }
11233
- const credPath = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11234
- if (!(0, import_node_fs3.existsSync)(credPath)) {
11609
+ const credPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11610
+ if (!(0, import_node_fs5.existsSync)(credPath)) {
11235
11611
  console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11236
11612
  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");
11237
11613
  process.exit(1);
11238
11614
  }
11239
11615
  const plist = launchdPlistPath();
11240
- const logDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, "logs");
11241
- (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(plist), { recursive: true });
11242
- (0, import_node_fs3.mkdirSync)(logDir, { recursive: true });
11616
+ const logDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "logs");
11617
+ (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(plist), { recursive: true });
11618
+ (0, import_node_fs5.mkdirSync)(logDir, { recursive: true });
11243
11619
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
11244
11620
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11245
11621
  <plist version="1.0">
@@ -11253,12 +11629,12 @@ function launchdInstall(urlArg) {
11253
11629
  <key>RunAtLoad</key><true/>
11254
11630
  <key>KeepAlive</key><true/>
11255
11631
  <key>ThrottleInterval</key><integer>30</integer>
11256
- <key>StandardOutPath</key><string>${(0, import_node_path6.join)(logDir, "daemon.out.log")}</string>
11257
- <key>StandardErrorPath</key><string>${(0, import_node_path6.join)(logDir, "daemon.err.log")}</string>
11632
+ <key>StandardOutPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.out.log")}</string>
11633
+ <key>StandardErrorPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.err.log")}</string>
11258
11634
  </dict>
11259
11635
  </plist>
11260
11636
  `;
11261
- (0, import_node_fs3.writeFileSync)(plist, xml);
11637
+ (0, import_node_fs5.writeFileSync)(plist, xml);
11262
11638
  const uid = process.getuid?.();
11263
11639
  if (uid === void 0) {
11264
11640
  console.error("[local] \u65E0\u6CD5\u786E\u5B9A uid\uFF0Claunchd \u5B89\u88C5\u5931\u8D25");
@@ -11276,7 +11652,7 @@ function launchdInstall(urlArg) {
11276
11652
  }
11277
11653
  function launchdUninstall() {
11278
11654
  const plist = launchdPlistPath();
11279
- if (!(0, import_node_fs3.existsSync)(plist)) {
11655
+ if (!(0, import_node_fs5.existsSync)(plist)) {
11280
11656
  console.log("[local] \u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u5E38\u9A7B\u670D\u52A1\uFF08nothing to uninstall\uFF09");
11281
11657
  process.exit(0);
11282
11658
  }
@@ -11284,7 +11660,7 @@ function launchdUninstall() {
11284
11660
  const guiDomain = `gui/${uid ?? 0}`;
11285
11661
  run("launchctl", ["bootout", guiDomain, plist], true);
11286
11662
  run("launchctl", ["unload", "-w", plist], true);
11287
- (0, import_node_fs3.rmSync)(plist);
11663
+ (0, import_node_fs5.rmSync)(plist);
11288
11664
  console.log(`[local] launchd \u670D\u52A1\u5DF2\u505C\u6B62\u5E76\u5378\u8F7D\uFF1A${LAUNCHD_LABEL}`);
11289
11665
  }
11290
11666
  if (argUrl && !argMatch && argUrl === "install") {
@@ -11295,13 +11671,294 @@ if (argUrl && !argMatch && argUrl === "uninstall") {
11295
11671
  launchdUninstall();
11296
11672
  process.exit(0);
11297
11673
  }
11298
- startLocal(argMatch ? { url: argUrl } : {}).catch((err) => {
11299
- if (err instanceof AuthError) {
11300
- console.error(`[local] ${err.message}`);
11674
+ if (argUrl === "pair") {
11675
+ void (async () => {
11676
+ const home = (0, import_node_os7.homedir)();
11677
+ const cfgPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "config.json");
11678
+ let cfg = {};
11679
+ try {
11680
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11681
+ } catch {
11682
+ }
11683
+ const url = process.argv[4] ?? process.env.RELAY_URL ?? cfg.relayUrl;
11684
+ if (!url) {
11685
+ console.error("[pair] \u7F3A\u5C11 hub \u5730\u5740\uFF1A\u5148\u8FD0\u884C\u4E00\u6B21 cli-local \u5B8C\u6210\u914D\u7F6E\uFF0C\u6216 cli-local pair wss://your-hub/ws");
11686
+ process.exit(1);
11687
+ }
11688
+ if (!cfg.registrationKey && !process.env.REGISTRATION_KEY) {
11689
+ console.error("[pair] \u7F3A\u5C11 registrationKey\uFF1A\u5199\u5165 ~/.cli-remote/config.json \u6216\u8BBE\u7F6E REGISTRATION_KEY");
11690
+ process.exit(1);
11691
+ }
11692
+ const prev = await loadCredentials();
11693
+ const deviceId = prev?.deviceId || await stableDeviceId();
11694
+ console.log(`[pair] hub: ${url}\uFF08\u8BBE\u5907 ${deviceId}${prev ? "\uFF0C\u590D\u7528" : "\uFF0C\u7531\u673A\u5668\u7801\u6D3E\u751F"}\uFF09`);
11695
+ const creds = await deviceFlowAuthorize(
11696
+ baseUrlFromWsUrl(url),
11697
+ cfg.registrationKey ?? process.env.REGISTRATION_KEY,
11698
+ deviceId
11699
+ );
11700
+ console.log(`[pair] \u6388\u6743\u5B8C\u6210 deviceId=${creds.deviceId}`);
11701
+ const port = readConfiguredPort();
11702
+ const health = await apiGet(port, "/healthz");
11703
+ if (health?.ok) {
11704
+ console.log("[pair] gateway \u5728\u8FD0\u884C\uFF0C\u65B0\u51ED\u8BC1\u5DF2\u5199\u5165 \u2014 \u6570\u79D2\u5185\u81EA\u52A8\u91CD\u8FDE\uFF08\u65E0\u9700\u91CD\u542F\uFF09");
11705
+ } else {
11706
+ console.log("[pair] gateway \u672A\u8FD0\u884C\uFF1Acli-local gateway start \u542F\u52A8\u5E38\u9A7B\u670D\u52A1");
11707
+ }
11708
+ })().catch((e) => {
11709
+ console.error(`[pair] ${e.message}`);
11710
+ process.exit(1);
11711
+ });
11712
+ }
11713
+ if (argUrl === "gateway") {
11714
+ let gatewayLogFile = function() {
11715
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "logs", "gateway.log");
11716
+ }, gatewayLogs = function() {
11717
+ const follow = rest.includes("-f") || rest.includes("--follow");
11718
+ const li = rest.findIndex((a) => a === "-n" || a === "--lines");
11719
+ const lines = li >= 0 ? Number(rest[li + 1]) || 50 : 50;
11720
+ const file = gatewayLogFile();
11721
+ if (!(0, import_node_fs5.existsSync)(file)) {
11722
+ console.log(`[gateway] \u6682\u65E0\u65E5\u5FD7\uFF08${file} \u4E0D\u5B58\u5728\uFF09`);
11723
+ process.exit(0);
11724
+ }
11725
+ const tail = (0, import_node_fs5.readFileSync)(file, "utf-8").trimEnd().split("\n").slice(-lines);
11726
+ console.log(tail.join("\n"));
11727
+ if (!follow) process.exit(0);
11728
+ let size = (0, import_node_fs5.statSync)(file).size;
11729
+ console.log("--- following (Ctrl+C \u9000\u51FA) ---");
11730
+ setInterval(() => {
11731
+ try {
11732
+ const st = (0, import_node_fs5.statSync)(file);
11733
+ if (st.size > size) {
11734
+ const fd = (0, import_node_fs5.openSync)(file, "r");
11735
+ const buf = Buffer.alloc(st.size - size);
11736
+ (0, import_node_fs5.readSync)(fd, buf, 0, buf.length, size);
11737
+ (0, import_node_fs5.closeSync)(fd);
11738
+ size = st.size;
11739
+ process.stdout.write(buf.toString("utf-8"));
11740
+ } else if (st.size < size) {
11741
+ size = 0;
11742
+ }
11743
+ } catch {
11744
+ }
11745
+ }, 1e3);
11746
+ };
11747
+ gatewayLogFile2 = gatewayLogFile, gatewayLogs2 = gatewayLogs;
11748
+ const action = process.argv[3] ?? "";
11749
+ const rest = process.argv.slice(4);
11750
+ const hubArg = (() => {
11751
+ const i = rest.indexOf("--hub");
11752
+ return i >= 0 ? rest[i + 1] : void 0;
11753
+ })();
11754
+ async function gatewayStart() {
11755
+ if (gatewayAlive()) {
11756
+ console.log(`[gateway] already running (pid ${gatewayPid()})`);
11757
+ process.exit(0);
11758
+ }
11759
+ const home = (0, import_node_os7.homedir)();
11760
+ const entry = entryScript();
11761
+ const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
11762
+ const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
11763
+ const hasCfg = (0, import_node_fs5.existsSync)(cfgPath);
11764
+ if (!entry) {
11765
+ 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");
11766
+ process.exit(1);
11767
+ }
11768
+ if (!hasCfg) {
11769
+ console.error("[gateway] \u5C1A\u672A\u914D\u7F6E hub \u5730\u5740\u3002\u8BF7\u5148\u4EA4\u4E92\u5F0F\u8FD0\u884C\u4E00\u6B21 cli-local \u5B8C\u6210\u521D\u59CB\u914D\u7F6E\u3002");
11770
+ console.error("[gateway] \uFF08\u65E0\u51ED\u8BC1\u4E5F\u53EF\u4EE5\u542F\u52A8\uFF1Agateway \u4F1A\u8FDB\u5165 needs-pairing\uFF0C\u7B49 cli-local pair\uFF09");
11771
+ process.exit(1);
11772
+ }
11773
+ (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(gatewayLogFile()), { recursive: true });
11774
+ const out = (0, import_node_fs5.openSync)(gatewayLogFile(), "a");
11775
+ const env = {};
11776
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
11777
+ const args = [entry, "gateway", "run"];
11778
+ if (hubArg) args.push("--hub", hubArg);
11779
+ const child = (0, import_node_child_process3.spawn)(process.execPath, args, {
11780
+ detached: true,
11781
+ stdio: ["ignore", out, out],
11782
+ env
11783
+ });
11784
+ child.unref();
11785
+ console.log(`[gateway] started (pid ${child.pid})`);
11786
+ const port = readConfiguredPort();
11787
+ for (let i = 0; i < 20; i++) {
11788
+ await new Promise((res) => setTimeout(res, 250));
11789
+ const h = await apiGet(port, "/healthz", 400);
11790
+ if (h?.ok) {
11791
+ console.log(`[gateway] \u63A7\u5236API\u5C31\u7EEA: http://127.0.0.1:${port}\uFF08\u4EC5\u672C\u673A\uFF09`);
11792
+ break;
11793
+ }
11794
+ }
11795
+ console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11796
+ console.log("[gateway] \u72B6\u6001: cli-local gateway status");
11797
+ }
11798
+ async function stopWorker() {
11799
+ const port = readConfiguredPort();
11800
+ if (await apiPost(port, "/stop", 1500)) {
11801
+ const t02 = Date.now();
11802
+ while (Date.now() - t02 < 5e3 && gatewayAlive()) {
11803
+ await new Promise((res) => setTimeout(res, 150));
11804
+ }
11805
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11806
+ return;
11807
+ }
11808
+ const pid = gatewayPid();
11809
+ if (pid === null || !gatewayAlive()) {
11810
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11811
+ return;
11812
+ }
11813
+ try {
11814
+ process.kill(pid, "SIGTERM");
11815
+ } catch {
11816
+ }
11817
+ const t0 = Date.now();
11818
+ while (Date.now() - t0 < 5e3 && gatewayAlive()) {
11819
+ await new Promise((res) => setTimeout(res, 150));
11820
+ }
11821
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11822
+ }
11823
+ async function gatewayStop() {
11824
+ const pid = gatewayPid();
11825
+ const wasRunning = gatewayAlive();
11826
+ await stopWorker();
11827
+ console.log(wasRunning ? `[gateway] stopped (pid ${pid ?? "?"})` : "[gateway] not running");
11828
+ process.exit(0);
11829
+ }
11830
+ async function gatewayStatus() {
11831
+ const port = readConfiguredPort();
11832
+ const st = await apiGet(port, "/status", 600);
11833
+ if (st) {
11834
+ const phaseLabel = {
11835
+ starting: "\u542F\u52A8\u4E2D",
11836
+ connecting: "\u8FDE\u63A5\u4E2D",
11837
+ connected: "\u5DF2\u8FDE\u63A5",
11838
+ reconnecting: "\u91CD\u8FDE\u4E2D",
11839
+ "needs-pairing": "\u9700\u8981\u91CD\u65B0\u6388\u6743"
11840
+ };
11841
+ const up = Math.floor(st.uptimeMs / 1e3);
11842
+ const upStr = up > 3600 ? `${Math.floor(up / 3600)}h${Math.floor(up % 3600 / 60)}m` : up > 60 ? `${Math.floor(up / 60)}m${up % 60}s` : `${up}s`;
11843
+ const exp = st.accessTokenExpiresAt ? new Date(st.accessTokenExpiresAt).toLocaleTimeString() : "\u2014";
11844
+ console.log(`[gateway] running (pid ${gatewayPid() ?? "?"}, api :${port})`);
11845
+ console.log(`[gateway] \u72B6\u6001: ${phaseLabel[st.phase] ?? st.phase}${st.reason ? ` (${st.reason})` : ""}`);
11846
+ console.log(`[gateway] hub: ${st.hubUrl}`);
11847
+ console.log(`[gateway] deviceId: ${st.deviceId ?? "\u2014"}`);
11848
+ console.log(`[gateway] uptime: ${upStr}, token \u5230\u671F: ${exp}`);
11849
+ console.log(`[gateway] PTY \u4F1A\u8BDD: ${st.sessions.length} \u4E2A${st.sessions.length ? ` (${st.sessions.map((s) => s.cmd).join(", ")})` : ""}`);
11850
+ if (st.phase === "needs-pairing") {
11851
+ console.log("[gateway] \u26A0 \u8FD0\u884C cli-local pair \u91CD\u65B0\u6388\u6743\uFF08\u5B8C\u6210\u540E\u81EA\u52A8\u91CD\u8FDE\uFF09");
11852
+ }
11853
+ process.exit(0);
11854
+ }
11855
+ if (!gatewayAlive()) {
11856
+ console.log("[gateway] stopped");
11857
+ process.exit(1);
11858
+ }
11859
+ const pid = gatewayPid();
11860
+ let hub = "(\u672A\u914D\u7F6E)";
11861
+ try {
11862
+ 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"));
11863
+ hub = cfg.relayUrl ?? hub;
11864
+ } catch {
11865
+ }
11866
+ console.log(`[gateway] running (pid ${pid}) \u2014 \u63A7\u5236API\u672A\u54CD\u5E94\uFF08\u65E7\u7248\u672C\uFF1F\uFF09`);
11867
+ console.log(`[gateway] hub: ${hub}`);
11868
+ console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11869
+ process.exit(0);
11870
+ }
11871
+ void (async () => {
11872
+ switch (action) {
11873
+ case "start":
11874
+ gatewayStart().then(() => process.exit(0)).catch((e) => {
11875
+ console.error(`[gateway] ${e.message}`);
11876
+ process.exit(1);
11877
+ });
11878
+ break;
11879
+ case "stop":
11880
+ gatewayStop();
11881
+ break;
11882
+ case "restart":
11883
+ await stopWorker();
11884
+ gatewayStart().then(() => process.exit(0)).catch((e) => {
11885
+ console.error(`[gateway] ${e.message}`);
11886
+ process.exit(1);
11887
+ });
11888
+ break;
11889
+ case "status":
11890
+ void gatewayStatus();
11891
+ break;
11892
+ case "logs":
11893
+ gatewayLogs();
11894
+ break;
11895
+ case "run":
11896
+ startLocal({ url: hubArg, daemon: true }).catch((err) => {
11897
+ if (err instanceof AuthError) console.error(`[local] ${err.message}`);
11898
+ else console.error("[local] fatal:", err);
11899
+ process.exit(1);
11900
+ });
11901
+ break;
11902
+ default:
11903
+ console.log(`cli-local gateway \u2014 \u540E\u53F0\u8FD0\u884C\u7BA1\u7406
11904
+
11905
+ usage:
11906
+ cli-local gateway start [--hub URL] \u540E\u53F0\u542F\u52A8\uFF08\u81EA\u52A8\u91CD\u8FDE\uFF09
11907
+ cli-local gateway stop \u505C\u6B62
11908
+ cli-local gateway restart \u91CD\u542F
11909
+ cli-local gateway status \u8FD0\u884C\u72B6\u6001
11910
+ cli-local gateway logs [-f] [-n N] \u67E5\u770B\u65E5\u5FD7\uFF08-f \u8DDF\u968F\uFF09
11911
+ cli-local gateway run [--hub URL] \u524D\u53F0\u8FD0\u884C\uFF08\u4F9B start \u62C9\u8D77\uFF09
11912
+
11913
+ \u8BF4\u660E\uFF1A\u540E\u53F0\u8FDB\u7A0B\u7531 pid \u6587\u4EF6\u7BA1\u7406\uFF08~/.cli-remote/gateway.pid\uFF09\uFF0C\u672C\u5730\u63A7\u5236API
11914
+ \u4EC5 127.0.0.1\uFF08\u9ED8\u8BA4 18790\uFF0Cconfig.json gateway.apiPort \u53EF\u6539\uFF09\uFF0C\u65E5\u5FD7\u5728
11915
+ ~/.cli-remote/logs/gateway.log\u3002\u65E0\u51ED\u8BC1\u4E5F\u80FD\u542F\u52A8\uFF08\u8FDB\u5165 needs-pairing \u7B49
11916
+ cli-local pair\uFF09\u3002macOS \u60F3\u5F00\u673A\u81EA\u542F\u7528 launchd\uFF1Acli-local install\u3002`);
11917
+ process.exit(action ? 1 : 0);
11918
+ }
11919
+ })().catch((e) => {
11920
+ console.error(`[gateway] ${e.message}`);
11921
+ process.exit(1);
11922
+ });
11923
+ }
11924
+ var gatewayLogFile2;
11925
+ var gatewayLogs2;
11926
+ if (!argUrl || argMatch) {
11927
+ if (!argUrl) {
11928
+ void (async () => {
11929
+ const port = readConfiguredPort();
11930
+ const health = await apiGet(port, "/healthz", 400);
11931
+ if (health?.ok) {
11932
+ console.log("[local] gateway \u5E38\u9A7B\u670D\u52A1\u8FD0\u884C\u4E2D\u3002\u5B9E\u65F6\u72B6\u6001:");
11933
+ const entry = process.argv[1];
11934
+ if (entry && !entry.endsWith(".ts")) {
11935
+ const { execFileSync: ex } = await import("node:child_process");
11936
+ try {
11937
+ ex(process.execPath, [entry, "gateway", "status"], { stdio: "inherit" });
11938
+ } catch {
11939
+ console.log(`[local] \u63A7\u5236API: http://127.0.0.1:${port}/status`);
11940
+ }
11941
+ } else {
11942
+ console.log(`[local] \u63A7\u5236API: http://127.0.0.1:${port}/status`);
11943
+ }
11944
+ process.exit(0);
11945
+ }
11946
+ await startLocal({});
11947
+ })().catch((err) => {
11948
+ if (err instanceof AuthError) console.error(`[local] ${err.message}`);
11949
+ else console.error("[local] fatal:", err);
11950
+ process.exit(1);
11951
+ });
11301
11952
  } else {
11302
- console.error("[local] fatal:", err);
11953
+ startLocal({ url: argUrl }).catch((err) => {
11954
+ if (err instanceof AuthError) {
11955
+ console.error(`[local] ${err.message}`);
11956
+ } else {
11957
+ console.error("[local] fatal:", err);
11958
+ }
11959
+ process.exit(1);
11960
+ });
11303
11961
  }
11304
- process.exit(1);
11305
- });
11962
+ }
11306
11963
  process.on("SIGINT", () => process.exit(0));
11307
11964
  process.on("SIGTERM", () => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli-remote/local",
3
- "version": "0.0.4",
3
+ "version": "0.1.1",
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": {