@cli-remote/local 0.0.6 → 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 +571 -183
  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_fs4 = require("node:fs");
9942
+ var import_node_fs5 = require("node:fs");
9943
9943
  var import_node_child_process3 = require("node:child_process");
9944
- var import_node_os6 = require("node:os");
9945
- var import_node_path6 = require("node:path");
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
@@ -9959,9 +9959,9 @@ var wrapper_default = import_websocket.default;
9959
9959
  // src/client.ts
9960
9960
  var import_node_child_process2 = require("node:child_process");
9961
9961
  var import_node_readline = require("node:readline");
9962
- var import_node_fs3 = require("node:fs");
9963
- var import_node_os5 = require("node:os");
9964
- 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");
9965
9965
 
9966
9966
  // ../shared/src/codec.ts
9967
9967
  var import_msgpack = __toESM(require_dist(), 1);
@@ -10510,15 +10510,100 @@ async function exists(p) {
10510
10510
  }
10511
10511
  }
10512
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
+
10513
10598
  // src/auth.ts
10514
10599
  var import_node_child_process = require("node:child_process");
10515
10600
  var import_node_crypto = require("node:crypto");
10516
10601
  var import_promises2 = require("node:fs/promises");
10517
- var import_node_os4 = require("node:os");
10518
- var import_node_path4 = require("node:path");
10602
+ var import_node_os5 = require("node:os");
10603
+ var import_node_path5 = require("node:path");
10519
10604
  var import_qrcode = __toESM(require_lib(), 1);
10520
10605
  function credentialsPath() {
10521
- 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);
10522
10607
  }
10523
10608
  async function loadCredentials() {
10524
10609
  try {
@@ -10531,19 +10616,13 @@ async function loadCredentials() {
10531
10616
  }
10532
10617
  async function saveCredentials(creds) {
10533
10618
  const path = credentialsPath();
10534
- 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 });
10535
10620
  await (0, import_promises2.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10536
10621
  try {
10537
10622
  await (0, import_promises2.chmod)(path, 384);
10538
10623
  } catch {
10539
10624
  }
10540
10625
  }
10541
- async function clearCredentials() {
10542
- try {
10543
- await (0, import_promises2.writeFile)(credentialsPath(), "");
10544
- } catch {
10545
- }
10546
- }
10547
10626
  function baseUrlFromWsUrl(wsUrl) {
10548
10627
  const http = wsUrl.replace(/^ws/, "http");
10549
10628
  try {
@@ -10588,8 +10667,8 @@ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
10588
10667
  return { registrationKey: data.registrationKey, user: data.user?.email ?? email };
10589
10668
  }
10590
10669
  async function mergeLocalConfig(patch) {
10591
- const dir = (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR);
10592
- 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);
10593
10672
  let existing = {};
10594
10673
  try {
10595
10674
  existing = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
@@ -10610,7 +10689,7 @@ async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
10610
10689
  const res = await postJson(`${baseUrl}/oauth/device_authorization`, {
10611
10690
  registrationKey,
10612
10691
  device_id: deviceId,
10613
- device_name: (0, import_node_os4.hostname)(),
10692
+ device_name: (0, import_node_os5.hostname)(),
10614
10693
  platform: process.platform
10615
10694
  });
10616
10695
  if (res.status !== 200) {
@@ -10699,6 +10778,20 @@ async function ensureFreshAccessToken(baseUrl, creds) {
10699
10778
  }
10700
10779
  return refreshInFlight;
10701
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
+ }
10702
10795
  async function ensureCredentials(baseUrl, registrationKey) {
10703
10796
  const existing = await loadCredentials();
10704
10797
  if (existing) {
@@ -10708,14 +10801,17 @@ async function ensureCredentials(baseUrl, registrationKey) {
10708
10801
  console.error(`[local] ${e.message}\uFF0C\u91CD\u65B0\u8D70\u6388\u6743\u6D41\u7A0B`);
10709
10802
  }
10710
10803
  }
10711
- return deviceFlowAuthorize(baseUrl, registrationKey, "dev_" + (0, import_node_crypto.randomUUID)());
10804
+ return deviceFlowAuthorize(baseUrl, registrationKey, await stableDeviceId());
10712
10805
  }
10713
10806
 
10714
10807
  // src/client.ts
10808
+ function isPermanentAuthFailure(e) {
10809
+ return e instanceof AuthError && e.message.includes("invalid_grant");
10810
+ }
10715
10811
  async function loadConfig() {
10716
- 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);
10717
10813
  try {
10718
- return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
10814
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf-8"));
10719
10815
  } catch {
10720
10816
  return {};
10721
10817
  }
@@ -10852,17 +10948,123 @@ async function startLocal(opts = {}) {
10852
10948
  (data) => safeSend(data),
10853
10949
  (msg) => console.log(`[local] ${msg}`)
10854
10950
  );
10855
- 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
+ }
10856
10984
  const commands = detectCommands();
10857
- if (opts.daemon) writeGatewayPid();
10858
10985
  console.log(`[local] connecting to ${url}`);
10859
- console.log(`[local] deviceId=${credentials.deviceId}`);
10986
+ console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
10860
10987
  console.log(`[local] whitelist: ${whitelist.size} commands`);
10861
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
+ }
10862
11065
  let ws = null;
10863
11066
  let heartbeatTimer = null;
10864
11067
  let reconnectTimer = null;
10865
- let reconnectAttempts = 0;
10866
11068
  let userExitCode = null;
10867
11069
  function stopHeartbeat() {
10868
11070
  if (heartbeatTimer) clearInterval(heartbeatTimer);
@@ -10885,6 +11087,7 @@ async function startLocal(opts = {}) {
10885
11087
  ws = sock;
10886
11088
  sock.on("open", () => {
10887
11089
  console.log("[local] connected");
11090
+ if (phase !== "needs-pairing") setPhase("connecting");
10888
11091
  sendAuth();
10889
11092
  stopHeartbeat();
10890
11093
  heartbeatTimer = setInterval(() => {
@@ -10901,6 +11104,7 @@ async function startLocal(opts = {}) {
10901
11104
  process.exit(userExitCode);
10902
11105
  }
10903
11106
  console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
11107
+ if (phase !== "needs-pairing") setPhase("reconnecting");
10904
11108
  scheduleReconnect();
10905
11109
  });
10906
11110
  sock.on("error", (err) => {
@@ -10909,6 +11113,7 @@ async function startLocal(opts = {}) {
10909
11113
  }
10910
11114
  async function scheduleReconnect() {
10911
11115
  if (reconnectTimer || userExitCode !== null) return;
11116
+ if (phase === "needs-pairing") return;
10912
11117
  const idx = Math.min(reconnectAttempts, RECONNECT_DELAYS_MS.length - 1);
10913
11118
  const delay = RECONNECT_DELAYS_MS[idx];
10914
11119
  reconnectAttempts++;
@@ -10918,11 +11123,13 @@ async function startLocal(opts = {}) {
10918
11123
  ensureFreshAccessToken(baseUrl, credentials).then((creds) => {
10919
11124
  credentials = creds;
10920
11125
  connect();
10921
- }).catch(async (e) => {
10922
- await clearCredentials();
10923
- console.error(`[local] ${e.message}`);
10924
- console.error("[local] \u51ED\u8BC1\u5DF2\u5931\u6548\u5E76\u6E05\u9664\uFF1B\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10925
- 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();
10926
11133
  });
10927
11134
  }, delay);
10928
11135
  }
@@ -10930,6 +11137,9 @@ async function startLocal(opts = {}) {
10930
11137
  userExitCode = code;
10931
11138
  if (reconnectTimer) clearTimeout(reconnectTimer);
10932
11139
  reconnectTimer = null;
11140
+ if (pairingTimer) clearInterval(pairingTimer);
11141
+ pairingTimer = null;
11142
+ controlApi?.close();
10933
11143
  if (opts.daemon) removeGatewayPid();
10934
11144
  transfers.abortAll();
10935
11145
  pty.killAll();
@@ -10959,6 +11169,7 @@ async function startLocal(opts = {}) {
10959
11169
  console.log(`[local] reconnected after ${reconnectAttempts} attempt(s); ${pty.list().length} PTY session(s) alive`);
10960
11170
  }
10961
11171
  reconnectAttempts = 0;
11172
+ setPhase("connected");
10962
11173
  console.log(`[local] auth ok, deviceId=${frame.channelId}`);
10963
11174
  break;
10964
11175
  case "auth-err":
@@ -11001,20 +11212,26 @@ async function startLocal(opts = {}) {
11001
11212
  }
11002
11213
  }
11003
11214
  async function handleAuthErr(reason) {
11004
- if (reason === "access token expired") {
11215
+ if (reason === "access token expired" || reason === "invalid access token") {
11005
11216
  try {
11006
11217
  credentials = await ensureFreshAccessToken(baseUrl, credentials);
11007
11218
  console.log("[local] token refreshed, retrying auth");
11008
11219
  sendAuth();
11009
11220
  return;
11010
11221
  } catch (e) {
11011
- 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;
11012
11232
  }
11013
11233
  }
11014
- await clearCredentials();
11015
- console.error("[local] auth failed:", reason);
11016
- console.error("[local] \u51ED\u8BC1\u5DF2\u6E05\u9664\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
11017
- exitProcess(1);
11234
+ enterNeedsPairing(reason);
11018
11235
  }
11019
11236
  function handleSpawn(frame) {
11020
11237
  const sessionId = frame.sessionId;
@@ -11160,7 +11377,57 @@ async function startLocal(opts = {}) {
11160
11377
  }
11161
11378
  process.on("SIGINT", () => cleanup(0));
11162
11379
  process.on("SIGTERM", () => cleanup(0));
11163
- 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));
11164
11431
  }
11165
11432
  function toUint8(raw) {
11166
11433
  if (raw instanceof Buffer) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
@@ -11171,21 +11438,21 @@ function toUint8(raw) {
11171
11438
  throw new Error("unexpected raw type");
11172
11439
  }
11173
11440
  function gatewayPidFile() {
11174
- return (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11441
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11175
11442
  }
11176
11443
  function writeGatewayPid() {
11177
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR), { recursive: true });
11178
- (0, import_node_fs3.writeFileSync)(gatewayPidFile(), String(process.pid));
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));
11179
11446
  }
11180
11447
  function removeGatewayPid() {
11181
11448
  try {
11182
- (0, import_node_fs3.rmSync)(gatewayPidFile());
11449
+ (0, import_node_fs4.rmSync)(gatewayPidFile());
11183
11450
  } catch {
11184
11451
  }
11185
11452
  }
11186
11453
  function gatewayPid() {
11187
11454
  try {
11188
- const pid = Number((0, import_node_fs3.readFileSync)(gatewayPidFile(), "utf-8").trim());
11455
+ const pid = Number((0, import_node_fs4.readFileSync)(gatewayPidFile(), "utf-8").trim());
11189
11456
  return Number.isInteger(pid) && pid > 0 ? pid : null;
11190
11457
  } catch {
11191
11458
  return null;
@@ -11219,15 +11486,15 @@ function resolveHere() {
11219
11486
  if (DEBUG) console.error("[debug] cannot resolve script path");
11220
11487
  return;
11221
11488
  }
11222
- const hereDir = (0, import_node_path6.dirname)(here);
11489
+ const hereDir = (0, import_node_path7.dirname)(here);
11223
11490
  const candidates = [
11224
- (0, import_node_path6.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11225
- (0, import_node_path6.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11226
- (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")
11227
11494
  ];
11228
11495
  let prebuildsDir = null;
11229
11496
  for (const c of candidates) {
11230
- if ((0, import_node_fs4.existsSync)(c)) {
11497
+ if ((0, import_node_fs5.existsSync)(c)) {
11231
11498
  prebuildsDir = c;
11232
11499
  break;
11233
11500
  }
@@ -11237,11 +11504,11 @@ function resolveHere() {
11237
11504
  return;
11238
11505
  }
11239
11506
  let n = 0;
11240
- for (const sub of (0, import_node_fs4.readdirSync)(prebuildsDir)) {
11241
- const helper = (0, import_node_path6.join)(prebuildsDir, sub, "spawn-helper");
11242
- if ((0, import_node_fs4.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)) {
11243
11510
  try {
11244
- (0, import_node_fs4.chmodSync)(helper, 493);
11511
+ (0, import_node_fs5.chmodSync)(helper, 493);
11245
11512
  n++;
11246
11513
  } catch {
11247
11514
  }
@@ -11252,49 +11519,56 @@ function resolveHere() {
11252
11519
  var argUrl = process.argv[2];
11253
11520
  var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
11254
11521
  if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl === "help")) {
11255
- 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)
11256
11523
 
11257
11524
  usage:
11258
- cli-local [HUB_URL]
11259
- 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
11260
- 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
11261
11534
 
11262
- \u9996\u6B21\u8FD0\u884C\uFF08\u65E0\u914D\u7F6E\uFF09\u4F1A\u8FDB\u5165\u5F15\u5BFC\uFF1A
11263
- 1. \u63D0\u793A hub \u5730\u5740\uFF08\u56DE\u8F66\u7528\u9ED8\u8BA4\uFF09
11264
- 2. \u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668 \u2192 \u767B\u5F55 hub\uFF0C\u4ECE\u300C\u6211\u7684\u8BBE\u5907\u300D\u9875\u590D\u5236 registrationKey
11265
- 3. \u56DE\u5230\u7EC8\u7AEF\u7C98\u8D34 key \u2192 \u81EA\u52A8\u5199\u5165 ~/.cli-remote/config.json
11266
- 4. \u968F\u540E\u8D70 OAuth Device Flow\uFF08RFC 8628\uFF09\uFF1A\u81EA\u52A8\u6253\u5F00\u6388\u6743\u9875 \u2192 \u6279\u51C6\u672C\u8BBE\u5907
11267
- 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
11268
11537
 
11269
- \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
11270
11541
 
11271
11542
  environment:
11272
11543
  RELAY_URL hub ws url (overrides argv and config)
11273
11544
  REGISTRATION_KEY registration key (fallback of config.json)
11274
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
11275
11547
  HOME where ~/.cli-remote/ lives
11276
11548
 
11277
11549
  config (~/.cli-remote/config.json):
11278
11550
  {
11279
11551
  "relayUrl": "wss://your-hub/ws",
11280
11552
  "registrationKey": "from-the-web-devices-page",
11553
+ "gateway": { "apiPort": 18790 },
11281
11554
  "whitelist": { "add": [], "remove": [] }
11282
11555
  }
11283
11556
 
11284
11557
  example:
11285
- cli-local wss://cli-remote.mkjs.net/ws
11286
- 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`);
11287
11561
  process.exit(0);
11288
11562
  }
11289
11563
  var LAUNCHD_LABEL = "net.cli-remote.local";
11290
11564
  function launchdPlistPath() {
11291
- 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`);
11292
11566
  }
11293
11567
  function entryScript() {
11294
11568
  const a1 = process.argv[1];
11295
11569
  if (!a1 || a1.endsWith(".ts")) return null;
11296
11570
  try {
11297
- return (0, import_node_fs4.realpathSync)(a1);
11571
+ return (0, import_node_fs5.realpathSync)(a1);
11298
11572
  } catch {
11299
11573
  return a1;
11300
11574
  }
@@ -11312,7 +11586,7 @@ function launchdInstall(urlArg) {
11312
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`);
11313
11587
  process.exit(1);
11314
11588
  }
11315
- const home = (0, import_node_os6.homedir)();
11589
+ const home = (0, import_node_os7.homedir)();
11316
11590
  const entry = entryScript();
11317
11591
  if (!entry) {
11318
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");
@@ -11320,28 +11594,28 @@ function launchdInstall(urlArg) {
11320
11594
  process.exit(1);
11321
11595
  }
11322
11596
  if (urlArg) {
11323
- const cfgDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR);
11324
- (0, import_node_fs4.mkdirSync)(cfgDir, { recursive: true });
11325
- 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");
11326
11600
  let cfg = {};
11327
11601
  try {
11328
- cfg = JSON.parse((0, import_node_fs4.readFileSync)(cfgPath, "utf-8"));
11602
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11329
11603
  } catch {
11330
11604
  }
11331
11605
  cfg.relayUrl = urlArg;
11332
- (0, import_node_fs4.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11606
+ (0, import_node_fs5.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11333
11607
  console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11334
11608
  }
11335
- const credPath = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11336
- if (!(0, import_node_fs4.existsSync)(credPath)) {
11609
+ const credPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11610
+ if (!(0, import_node_fs5.existsSync)(credPath)) {
11337
11611
  console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11338
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");
11339
11613
  process.exit(1);
11340
11614
  }
11341
11615
  const plist = launchdPlistPath();
11342
- const logDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, "logs");
11343
- (0, import_node_fs4.mkdirSync)((0, import_node_path6.dirname)(plist), { recursive: true });
11344
- (0, import_node_fs4.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 });
11345
11619
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
11346
11620
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11347
11621
  <plist version="1.0">
@@ -11355,12 +11629,12 @@ function launchdInstall(urlArg) {
11355
11629
  <key>RunAtLoad</key><true/>
11356
11630
  <key>KeepAlive</key><true/>
11357
11631
  <key>ThrottleInterval</key><integer>30</integer>
11358
- <key>StandardOutPath</key><string>${(0, import_node_path6.join)(logDir, "daemon.out.log")}</string>
11359
- <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>
11360
11634
  </dict>
11361
11635
  </plist>
11362
11636
  `;
11363
- (0, import_node_fs4.writeFileSync)(plist, xml);
11637
+ (0, import_node_fs5.writeFileSync)(plist, xml);
11364
11638
  const uid = process.getuid?.();
11365
11639
  if (uid === void 0) {
11366
11640
  console.error("[local] \u65E0\u6CD5\u786E\u5B9A uid\uFF0Claunchd \u5B89\u88C5\u5931\u8D25");
@@ -11378,7 +11652,7 @@ function launchdInstall(urlArg) {
11378
11652
  }
11379
11653
  function launchdUninstall() {
11380
11654
  const plist = launchdPlistPath();
11381
- if (!(0, import_node_fs4.existsSync)(plist)) {
11655
+ if (!(0, import_node_fs5.existsSync)(plist)) {
11382
11656
  console.log("[local] \u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u5E38\u9A7B\u670D\u52A1\uFF08nothing to uninstall\uFF09");
11383
11657
  process.exit(0);
11384
11658
  }
@@ -11386,7 +11660,7 @@ function launchdUninstall() {
11386
11660
  const guiDomain = `gui/${uid ?? 0}`;
11387
11661
  run("launchctl", ["bootout", guiDomain, plist], true);
11388
11662
  run("launchctl", ["unload", "-w", plist], true);
11389
- (0, import_node_fs4.rmSync)(plist);
11663
+ (0, import_node_fs5.rmSync)(plist);
11390
11664
  console.log(`[local] launchd \u670D\u52A1\u5DF2\u505C\u6B62\u5E76\u5378\u8F7D\uFF1A${LAUNCHD_LABEL}`);
11391
11665
  }
11392
11666
  if (argUrl && !argMatch && argUrl === "install") {
@@ -11397,70 +11671,70 @@ if (argUrl && !argMatch && argUrl === "uninstall") {
11397
11671
  launchdUninstall();
11398
11672
  process.exit(0);
11399
11673
  }
11400
- if (argUrl === "gateway") {
11401
- let gatewayLogFile = function() {
11402
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "logs", "gateway.log");
11403
- }, stopWorker = function() {
11404
- const pid = gatewayPid();
11405
- if (pid === null || !gatewayAlive()) {
11406
- (0, import_node_fs4.rmSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11407
- return;
11408
- }
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 = {};
11409
11679
  try {
11410
- process.kill(pid, "SIGTERM");
11680
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11411
11681
  } catch {
11412
11682
  }
11413
- const t0 = Date.now();
11414
- while (Date.now() - t0 < 5e3 && gatewayAlive()) {
11415
- const end = Date.now() + 150;
11416
- while (Date.now() < end) {
11417
- }
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);
11418
11687
  }
11419
- (0, import_node_fs4.rmSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11420
- }, gatewayStop = function() {
11421
- const pid = gatewayPid();
11422
- const wasRunning = gatewayAlive();
11423
- stopWorker();
11424
- console.log(wasRunning ? `[gateway] stopped (pid ${pid ?? "?"})` : "[gateway] not running");
11425
- process.exit(0);
11426
- }, gatewayStatus = function() {
11427
- if (!gatewayAlive()) {
11428
- console.log("[gateway] stopped");
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");
11429
11690
  process.exit(1);
11430
11691
  }
11431
- const pid = gatewayPid();
11432
- let hub = "(\u672A\u914D\u7F6E)";
11433
- try {
11434
- const cfg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "config.json"), "utf-8"));
11435
- hub = cfg.relayUrl ?? hub;
11436
- } catch {
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");
11437
11707
  }
11438
- console.log(`[gateway] running (pid ${pid})`);
11439
- console.log(`[gateway] hub: ${hub}`);
11440
- console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11441
- process.exit(0);
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");
11442
11716
  }, gatewayLogs = function() {
11443
11717
  const follow = rest.includes("-f") || rest.includes("--follow");
11444
11718
  const li = rest.findIndex((a) => a === "-n" || a === "--lines");
11445
11719
  const lines = li >= 0 ? Number(rest[li + 1]) || 50 : 50;
11446
11720
  const file = gatewayLogFile();
11447
- if (!(0, import_node_fs4.existsSync)(file)) {
11721
+ if (!(0, import_node_fs5.existsSync)(file)) {
11448
11722
  console.log(`[gateway] \u6682\u65E0\u65E5\u5FD7\uFF08${file} \u4E0D\u5B58\u5728\uFF09`);
11449
11723
  process.exit(0);
11450
11724
  }
11451
- const tail = (0, import_node_fs4.readFileSync)(file, "utf-8").trimEnd().split("\n").slice(-lines);
11725
+ const tail = (0, import_node_fs5.readFileSync)(file, "utf-8").trimEnd().split("\n").slice(-lines);
11452
11726
  console.log(tail.join("\n"));
11453
11727
  if (!follow) process.exit(0);
11454
- let size = (0, import_node_fs4.statSync)(file).size;
11728
+ let size = (0, import_node_fs5.statSync)(file).size;
11455
11729
  console.log("--- following (Ctrl+C \u9000\u51FA) ---");
11456
11730
  setInterval(() => {
11457
11731
  try {
11458
- const st = (0, import_node_fs4.statSync)(file);
11732
+ const st = (0, import_node_fs5.statSync)(file);
11459
11733
  if (st.size > size) {
11460
- const fd = (0, import_node_fs4.openSync)(file, "r");
11734
+ const fd = (0, import_node_fs5.openSync)(file, "r");
11461
11735
  const buf = Buffer.alloc(st.size - size);
11462
- (0, import_node_fs4.readSync)(fd, buf, 0, buf.length, size);
11463
- (0, import_node_fs4.closeSync)(fd);
11736
+ (0, import_node_fs5.readSync)(fd, buf, 0, buf.length, size);
11737
+ (0, import_node_fs5.closeSync)(fd);
11464
11738
  size = st.size;
11465
11739
  process.stdout.write(buf.toString("utf-8"));
11466
11740
  } else if (st.size < size) {
@@ -11470,7 +11744,7 @@ if (argUrl === "gateway") {
11470
11744
  }
11471
11745
  }, 1e3);
11472
11746
  };
11473
- gatewayLogFile2 = gatewayLogFile, stopWorker2 = stopWorker, gatewayStop2 = gatewayStop, gatewayStatus2 = gatewayStatus, gatewayLogs2 = gatewayLogs;
11747
+ gatewayLogFile2 = gatewayLogFile, gatewayLogs2 = gatewayLogs;
11474
11748
  const action = process.argv[3] ?? "";
11475
11749
  const rest = process.argv.slice(4);
11476
11750
  const hubArg = (() => {
@@ -11482,22 +11756,22 @@ if (argUrl === "gateway") {
11482
11756
  console.log(`[gateway] already running (pid ${gatewayPid()})`);
11483
11757
  process.exit(0);
11484
11758
  }
11485
- const home = (0, import_node_os6.homedir)();
11759
+ const home = (0, import_node_os7.homedir)();
11486
11760
  const entry = entryScript();
11487
- const cfgDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR);
11488
- const credPath = (0, import_node_path6.join)(cfgDir, CREDENTIALS_FILE);
11489
- const cfgPath = (0, import_node_path6.join)(cfgDir, "config.json");
11490
- const hasCfg = (0, import_node_fs4.existsSync)(cfgPath);
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);
11491
11764
  if (!entry) {
11492
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");
11493
11766
  process.exit(1);
11494
11767
  }
11495
- if (!hasCfg || !(0, import_node_fs4.existsSync)(credPath)) {
11496
- console.error("[gateway] \u5C1A\u672A\u914D\u7F6E/\u6388\u6743\u3002\u8BF7\u5148\u4EA4\u4E92\u5F0F\u8FD0\u884C\u4E00\u6B21 cli-local \u5B8C\u6210\u767B\u5F55\u4E0E\u8BBE\u5907\u6388\u6743\u3002");
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");
11497
11771
  process.exit(1);
11498
11772
  }
11499
- (0, import_node_fs4.mkdirSync)((0, import_node_path6.dirname)(gatewayLogFile()), { recursive: true });
11500
- const out = (0, import_node_fs4.openSync)(gatewayLogFile(), "a");
11773
+ (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(gatewayLogFile()), { recursive: true });
11774
+ const out = (0, import_node_fs5.openSync)(gatewayLogFile(), "a");
11501
11775
  const env = {};
11502
11776
  for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
11503
11777
  const args = [entry, "gateway", "run"];
@@ -11509,41 +11783,124 @@ if (argUrl === "gateway") {
11509
11783
  });
11510
11784
  child.unref();
11511
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
+ }
11512
11795
  console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11513
11796
  console.log("[gateway] \u72B6\u6001: cli-local gateway status");
11514
11797
  }
11515
- switch (action) {
11516
- case "start":
11517
- gatewayStart().then(() => process.exit(0)).catch((e) => {
11518
- console.error(`[gateway] ${e.message}`);
11519
- process.exit(1);
11520
- });
11521
- break;
11522
- case "stop":
11523
- gatewayStop();
11524
- break;
11525
- case "restart":
11526
- stopWorker();
11527
- gatewayStart().then(() => process.exit(0)).catch((e) => {
11528
- console.error(`[gateway] ${e.message}`);
11529
- process.exit(1);
11530
- });
11531
- break;
11532
- case "status":
11533
- gatewayStatus();
11534
- break;
11535
- case "logs":
11536
- gatewayLogs();
11537
- break;
11538
- case "run":
11539
- startLocal({ url: hubArg, daemon: true }).catch((err) => {
11540
- if (err instanceof AuthError) console.error(`[local] ${err.message}`);
11541
- else console.error("[local] fatal:", err);
11542
- process.exit(1);
11543
- });
11544
- break;
11545
- default:
11546
- console.log(`cli-local gateway \u2014 \u540E\u53F0\u8FD0\u884C\u7BA1\u7406
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
11547
11904
 
11548
11905
  usage:
11549
11906
  cli-local gateway start [--hub URL] \u540E\u53F0\u542F\u52A8\uFF08\u81EA\u52A8\u91CD\u8FDE\uFF09
@@ -11553,24 +11910,55 @@ usage:
11553
11910
  cli-local gateway logs [-f] [-n N] \u67E5\u770B\u65E5\u5FD7\uFF08-f \u8DDF\u968F\uFF09
11554
11911
  cli-local gateway run [--hub URL] \u524D\u53F0\u8FD0\u884C\uFF08\u4F9B start \u62C9\u8D77\uFF09
11555
11912
 
11556
- \u8BF4\u660E\uFF1A\u540E\u53F0\u8FDB\u7A0B\u7531 pid \u6587\u4EF6\u7BA1\u7406\uFF08~/.cli-remote/gateway.pid\uFF09\uFF0C\u65E5\u5FD7\u5728
11557
- ~/.cli-remote/logs/gateway.log\u3002\u9996\u6B21\u4F7F\u7528\u8BF7\u5148\u4EA4\u4E92\u5F0F\u8FD0\u884C cli-local
11558
- \u5B8C\u6210\u767B\u5F55\u4E0E\u8BBE\u5907\u6388\u6743\u3002macOS \u60F3\u5F00\u673A\u81EA\u542F\u7528 launchd\uFF1Acli-local install\u3002`);
11559
- process.exit(action ? 1 : 0);
11560
- }
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
+ });
11561
11923
  }
11562
11924
  var gatewayLogFile2;
11563
- var stopWorker2;
11564
- var gatewayStop2;
11565
- var gatewayStatus2;
11566
11925
  var gatewayLogs2;
11567
- startLocal(argMatch ? { url: argUrl } : {}).catch((err) => {
11568
- if (err instanceof AuthError) {
11569
- console.error(`[local] ${err.message}`);
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
+ });
11570
11952
  } else {
11571
- 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
+ });
11572
11961
  }
11573
- process.exit(1);
11574
- });
11962
+ }
11575
11963
  process.on("SIGINT", () => process.exit(0));
11576
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.6",
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": {