@cli-remote/local 0.0.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +777 -349
  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: randomBytes3, 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 = randomBytes3(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);
@@ -9981,6 +9981,7 @@ var ACCESS_TOKEN_TTL_MS = 60 * 6e4;
9981
9981
  var REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 6e4;
9982
9982
  var REFRESH_MARGIN_MS = 5 * 6e4;
9983
9983
  var OUTPUT_BUFFER_SIZE = 500;
9984
+ var AUDIT_RETENTION_DAYS = 14;
9984
9985
  var FILE_CHUNK_SIZE = 64 * 1024;
9985
9986
  var MAX_TRANSFER_BYTES = 500 * 1024 * 1024;
9986
9987
  var UPLOAD_DIR = "uploads";
@@ -9991,59 +9992,6 @@ var CONFIG_FILE = "config.json";
9991
9992
  var LOG_DIR = "logs";
9992
9993
 
9993
9994
  // ../shared/src/whitelist.ts
9994
- var DEFAULT_WHITELIST = [
9995
- "ls",
9996
- "cd",
9997
- "pwd",
9998
- "cat",
9999
- "less",
10000
- "head",
10001
- "tail",
10002
- "mkdir",
10003
- "rmdir",
10004
- "rm",
10005
- "cp",
10006
- "mv",
10007
- "touch",
10008
- "find",
10009
- "stat",
10010
- "tree",
10011
- "grep",
10012
- "awk",
10013
- "sed",
10014
- "sort",
10015
- "uniq",
10016
- "wc",
10017
- "cut",
10018
- "tr",
10019
- "xargs",
10020
- "git",
10021
- "ps",
10022
- "top",
10023
- "htop",
10024
- "kill",
10025
- "df",
10026
- "du",
10027
- "free",
10028
- "uname",
10029
- "whoami",
10030
- "env",
10031
- "echo",
10032
- "node",
10033
- "npm",
10034
- "pnpm",
10035
- "yarn",
10036
- "python",
10037
- "python3",
10038
- "pip",
10039
- "go",
10040
- "cargo",
10041
- "make",
10042
- "docker",
10043
- "claude",
10044
- "codex",
10045
- "kimi-cli"
10046
- ];
10047
9995
  var DANGEROUS_PATTERNS = [
10048
9996
  /\brm\s+-[rfRF]+\s+\/(\s|$)/,
10049
9997
  /\bsudo\b/,
@@ -10054,6 +10002,9 @@ var DANGEROUS_PATTERNS = [
10054
10002
  /\bshutdown\b/,
10055
10003
  /\breboot\b/
10056
10004
  ];
10005
+ function findDangerousPattern(line) {
10006
+ return DANGEROUS_PATTERNS.find((p) => p.test(line));
10007
+ }
10057
10008
 
10058
10009
  // src/pty-manager.ts
10059
10010
  var import_node_pty = require("node-pty");
@@ -10186,20 +10137,55 @@ var PtyManager = class {
10186
10137
  var import_node_fs = require("node:fs");
10187
10138
  var import_node_os2 = require("node:os");
10188
10139
  var import_node_path2 = require("node:path");
10140
+ function logRoot() {
10141
+ return (0, import_node_path2.join)((0, import_node_os2.homedir)(), DEFAULT_CONFIG_DIR, LOG_DIR);
10142
+ }
10189
10143
  function todayDir() {
10190
10144
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10191
10145
  }
10146
+ function pruneAuditLogs(retentionDays) {
10147
+ if (retentionDays <= 0) return 0;
10148
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 6e4).toISOString().slice(0, 10);
10149
+ let removed = 0;
10150
+ let entries;
10151
+ try {
10152
+ entries = (0, import_node_fs.readdirSync)(logRoot());
10153
+ } catch {
10154
+ return 0;
10155
+ }
10156
+ for (const name of entries) {
10157
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) continue;
10158
+ if (name >= cutoff) continue;
10159
+ try {
10160
+ (0, import_node_fs.rmSync)((0, import_node_path2.join)(logRoot(), name), { recursive: true, force: true });
10161
+ removed++;
10162
+ } catch {
10163
+ }
10164
+ }
10165
+ return removed;
10166
+ }
10192
10167
  var SessionLogger = class {
10193
- path;
10194
- closed = false;
10195
- constructor(sessionId, cmd) {
10196
- const dir = (0, import_node_path2.join)((0, import_node_os2.homedir)(), DEFAULT_CONFIG_DIR, LOG_DIR, todayDir());
10197
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
10168
+ constructor(sessionId, cmd, mode = "full") {
10169
+ this.mode = mode;
10170
+ if (mode === "off") {
10171
+ this.path = "";
10172
+ return;
10173
+ }
10174
+ const dir = (0, import_node_path2.join)(logRoot(), todayDir());
10175
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
10176
+ try {
10177
+ (0, import_node_fs.chmodSync)(dir, 448);
10178
+ } catch {
10179
+ }
10198
10180
  this.path = (0, import_node_path2.join)(dir, `${sessionId}.log`);
10199
- this.write({ ts: Date.now(), event: "spawn", cmd });
10181
+ (0, import_node_fs.appendFileSync)(this.path, "", { mode: 384 });
10182
+ (0, import_node_fs.chmodSync)(this.path, 384);
10183
+ this.write({ ts: Date.now(), event: "spawn", cmd, mode });
10200
10184
  }
10185
+ path;
10186
+ closed = false;
10201
10187
  logInput(data) {
10202
- if (this.closed) return;
10188
+ if (this.closed || this.mode === "off") return;
10203
10189
  this.write({
10204
10190
  ts: Date.now(),
10205
10191
  dir: "in",
@@ -10208,7 +10194,7 @@ var SessionLogger = class {
10208
10194
  });
10209
10195
  }
10210
10196
  logOutput(data) {
10211
- if (this.closed) return;
10197
+ if (this.closed || this.mode !== "full") return;
10212
10198
  this.write({
10213
10199
  ts: Date.now(),
10214
10200
  dir: "out",
@@ -10217,119 +10203,40 @@ var SessionLogger = class {
10217
10203
  });
10218
10204
  }
10219
10205
  logRejected(cmd, reason, dangerous) {
10220
- if (this.closed) return;
10206
+ if (this.closed || this.mode === "off") return;
10221
10207
  this.write({ ts: Date.now(), event: "rejected", cmd, reason, dangerous });
10222
10208
  }
10223
10209
  logForced(cmd) {
10224
- if (this.closed) return;
10210
+ if (this.closed || this.mode === "off") return;
10225
10211
  this.write({ ts: Date.now(), event: "forced", cmd });
10226
10212
  }
10227
10213
  logExit(code) {
10228
- if (this.closed) return;
10214
+ if (this.closed || this.mode === "off") return;
10229
10215
  this.write({ ts: Date.now(), event: "exit", code });
10230
10216
  this.closed = true;
10231
10217
  }
10232
10218
  write(record) {
10219
+ if (!this.path) return;
10233
10220
  try {
10234
- (0, import_node_fs.appendFileSync)(this.path, JSON.stringify(record) + "\n", { encoding: "utf-8" });
10221
+ (0, import_node_fs.appendFileSync)(this.path, JSON.stringify(record) + "\n", { encoding: "utf-8", mode: 384 });
10235
10222
  } catch {
10236
10223
  }
10237
10224
  }
10238
10225
  };
10239
10226
 
10240
10227
  // src/whitelist-checker.ts
10241
- function splitCommands(line) {
10242
- const parts = [];
10243
- let current = "";
10244
- let inSingle = false;
10245
- let inDouble = false;
10246
- for (let i = 0; i < line.length; i++) {
10247
- const c = line[i];
10248
- if (c === "'" && !inDouble) {
10249
- inSingle = !inSingle;
10250
- current += c;
10251
- continue;
10252
- }
10253
- if (c === '"' && !inSingle) {
10254
- inDouble = !inDouble;
10255
- current += c;
10256
- continue;
10257
- }
10258
- if (!inSingle && !inDouble) {
10259
- const next = line[i + 1];
10260
- if (c === "&" && next === "&") {
10261
- parts.push(current);
10262
- current = "";
10263
- i++;
10264
- continue;
10265
- }
10266
- if (c === "|" && next === "|") {
10267
- parts.push(current);
10268
- current = "";
10269
- i++;
10270
- continue;
10271
- }
10272
- if (c === ";" || c === "|") {
10273
- parts.push(current);
10274
- current = "";
10275
- continue;
10276
- }
10277
- }
10278
- current += c;
10279
- }
10280
- if (current.trim()) parts.push(current);
10281
- return parts;
10282
- }
10283
- function firstToken(cmd) {
10284
- const trimmed = cmd.trim();
10285
- if (!trimmed) return null;
10286
- const tokens = trimmed.split(/\s+/);
10287
- for (const t of tokens) {
10288
- if (!t) continue;
10289
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) continue;
10290
- return t;
10291
- }
10292
- return null;
10293
- }
10294
- function checkCommand(line, whitelist, dangerousPatterns = DANGEROUS_PATTERNS) {
10228
+ function checkCommand(line, dangerConfirm) {
10295
10229
  const trimmed = line.trim();
10296
- if (!trimmed) return { allowed: true };
10297
- if (line.includes("$(") || line.includes("`")) {
10298
- return {
10299
- allowed: false,
10300
- rejectedCmd: trimmed,
10301
- reason: "\u5305\u542B\u547D\u4EE4\u66FF\u6362 ($() \u6216 `)\uFF0C\u5DF2\u88AB\u7981\u6B62"
10302
- };
10303
- }
10304
- if (trimmed.startsWith("(")) {
10230
+ if (!trimmed || !dangerConfirm) return { allowed: true };
10231
+ const pattern = findDangerousPattern(trimmed);
10232
+ if (pattern) {
10305
10233
  return {
10306
10234
  allowed: false,
10307
10235
  rejectedCmd: trimmed,
10308
- reason: "\u5305\u542B\u5B50 shell ( )\uFF0C\u5DF2\u88AB\u7981\u6B62"
10236
+ reason: "\u5339\u914D\u5371\u9669\u547D\u4EE4\u6A21\u5F0F\uFF0C\u8BF7\u4E8C\u6B21\u786E\u8BA4",
10237
+ dangerous: true
10309
10238
  };
10310
10239
  }
10311
- for (const p of dangerousPatterns) {
10312
- if (p.test(line)) {
10313
- return {
10314
- allowed: false,
10315
- rejectedCmd: trimmed,
10316
- reason: "\u5339\u914D\u5371\u9669\u547D\u4EE4\u6A21\u5F0F",
10317
- dangerous: true
10318
- };
10319
- }
10320
- }
10321
- const segments = splitCommands(line);
10322
- for (const seg of segments) {
10323
- const cmd = firstToken(seg);
10324
- if (!cmd) continue;
10325
- if (!whitelist.has(cmd)) {
10326
- return {
10327
- allowed: false,
10328
- rejectedCmd: cmd,
10329
- reason: `"${cmd}" \u4E0D\u5728\u767D\u540D\u5355`
10330
- };
10331
- }
10332
- }
10333
10240
  return { allowed: true };
10334
10241
  }
10335
10242
 
@@ -10510,15 +10417,199 @@ async function exists(p) {
10510
10417
  }
10511
10418
  }
10512
10419
 
10420
+ // src/gateway-api.ts
10421
+ var import_node_http = require("node:http");
10422
+ var import_node_fs3 = require("node:fs");
10423
+ var import_node_os4 = require("node:os");
10424
+ var import_node_path4 = require("node:path");
10425
+ var DEFAULT_API_PORT = 18790;
10426
+ function apiPortFromEnv() {
10427
+ return Number(process.env.CLI_LOCAL_API_PORT) || DEFAULT_API_PORT;
10428
+ }
10429
+ function readConfiguredPort() {
10430
+ if (process.env.CLI_LOCAL_API_PORT) return apiPortFromEnv();
10431
+ try {
10432
+ const cfg = JSON.parse(
10433
+ (0, import_node_fs3.readFileSync)((0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, "config.json"), "utf-8")
10434
+ );
10435
+ return Number(cfg.gateway?.apiPort) || DEFAULT_API_PORT;
10436
+ } catch {
10437
+ return DEFAULT_API_PORT;
10438
+ }
10439
+ }
10440
+ function startControlApi(opts) {
10441
+ const server = (0, import_node_http.createServer)((req, res) => {
10442
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
10443
+ const route = `${req.method} ${url.pathname}`;
10444
+ const json = (code, body) => {
10445
+ res.writeHead(code, { "content-type": "application/json" });
10446
+ res.end(JSON.stringify(body));
10447
+ };
10448
+ if (route === "GET /healthz") return json(200, { ok: true });
10449
+ if (route === "GET /status") return json(200, opts.getStatus());
10450
+ if (route === "GET /sessions") {
10451
+ return json(200, { sessions: opts.getStatus().sessions });
10452
+ }
10453
+ if (route === "GET /logs") {
10454
+ const n = Math.min(500, Math.max(1, Number(url.searchParams.get("n")) || 200));
10455
+ const lines = opts.getLogs().slice(-n);
10456
+ return json(200, { lines });
10457
+ }
10458
+ if (route === "POST /stop") {
10459
+ json(200, { ok: true });
10460
+ opts.onStop();
10461
+ return;
10462
+ }
10463
+ const killMatch = /^POST \/sessions\/([\w-]+)\/kill$/.exec(`${req.method} ${url.pathname}`);
10464
+ if (killMatch) {
10465
+ const ok = opts.killSession(killMatch[1]);
10466
+ return json(ok ? 200 : 404, ok ? { ok: true } : { ok: false, error: "no such session" });
10467
+ }
10468
+ json(404, { error: "not found" });
10469
+ });
10470
+ server.listen(opts.port, "127.0.0.1");
10471
+ return {
10472
+ port: opts.port,
10473
+ close: () => server.close()
10474
+ };
10475
+ }
10476
+ async function apiGet(port, path, timeoutMs = 800) {
10477
+ try {
10478
+ const ctrl = new AbortController();
10479
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
10480
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
10481
+ signal: ctrl.signal
10482
+ });
10483
+ clearTimeout(timer);
10484
+ if (!res.ok) return null;
10485
+ return await res.json();
10486
+ } catch {
10487
+ return null;
10488
+ }
10489
+ }
10490
+ async function apiPost(port, path, timeoutMs = 800) {
10491
+ try {
10492
+ const ctrl = new AbortController();
10493
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
10494
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
10495
+ method: "POST",
10496
+ signal: ctrl.signal
10497
+ });
10498
+ clearTimeout(timer);
10499
+ return res.ok;
10500
+ } catch {
10501
+ return false;
10502
+ }
10503
+ }
10504
+
10505
+ // src/setup-callback.ts
10506
+ var import_node_http2 = require("node:http");
10507
+ var import_node_crypto = require("node:crypto");
10508
+ var BODY_LIMIT = 64 * 1024;
10509
+ function startSetupCallback(timeoutMs = 3 * 6e4) {
10510
+ return new Promise((resolveStart, rejectStart) => {
10511
+ const chan = (0, import_node_crypto.randomBytes)(8).toString("hex");
10512
+ let timer = null;
10513
+ let settled = false;
10514
+ let server = null;
10515
+ const cors = {
10516
+ "access-control-allow-origin": "*",
10517
+ "access-control-allow-methods": "POST, OPTIONS",
10518
+ "access-control-allow-headers": "content-type",
10519
+ "access-control-allow-private-network": "true",
10520
+ "access-control-max-age": "600"
10521
+ };
10522
+ const done = new Promise((resolve2, reject) => {
10523
+ timer = setTimeout(() => {
10524
+ if (settled) return;
10525
+ settled = true;
10526
+ stop();
10527
+ reject(new Error("\u7B49\u5F85\u7F51\u9875\u56DE\u8C03\u8D85\u65F6"));
10528
+ }, timeoutMs);
10529
+ function stop() {
10530
+ if (timer) clearTimeout(timer);
10531
+ server?.close();
10532
+ }
10533
+ function finish(fn) {
10534
+ if (settled) return;
10535
+ settled = true;
10536
+ if (timer) clearTimeout(timer);
10537
+ server?.close();
10538
+ fn();
10539
+ }
10540
+ const tryPort = (port) => {
10541
+ if (port > 18802) {
10542
+ rejectStart(new Error("no free port for the setup callback"));
10543
+ return;
10544
+ }
10545
+ const s = (0, import_node_http2.createServer)((req, res) => {
10546
+ const reply = (code, body) => {
10547
+ res.writeHead(code, { "content-type": "application/json", ...cors });
10548
+ res.end(JSON.stringify(body));
10549
+ };
10550
+ if (req.method === "OPTIONS") {
10551
+ reply(204, {});
10552
+ return;
10553
+ }
10554
+ if (req.method !== "POST" || !req.url?.startsWith("/setup")) {
10555
+ reply(404, { ok: false, error: "not found" });
10556
+ return;
10557
+ }
10558
+ let size = 0;
10559
+ const chunks = [];
10560
+ req.on("data", (c) => {
10561
+ size += c.length;
10562
+ if (size > BODY_LIMIT) {
10563
+ req.destroy();
10564
+ return;
10565
+ }
10566
+ chunks.push(c);
10567
+ });
10568
+ req.on("end", () => {
10569
+ try {
10570
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
10571
+ if (body.chan !== chan || !body.registrationKey) {
10572
+ reply(400, { ok: false, error: "bad request" });
10573
+ return;
10574
+ }
10575
+ reply(200, { ok: true });
10576
+ const key = body.registrationKey;
10577
+ finish(() => resolve2(key));
10578
+ } catch {
10579
+ reply(400, { ok: false, error: "bad request" });
10580
+ }
10581
+ });
10582
+ });
10583
+ s.once("error", () => {
10584
+ s.close();
10585
+ tryPort(port + 1);
10586
+ });
10587
+ s.listen(port, "127.0.0.1", () => {
10588
+ server = s;
10589
+ resolveStart({
10590
+ port,
10591
+ chan,
10592
+ done,
10593
+ stop: () => {
10594
+ finish(() => reject(new Error("callback stopped")));
10595
+ }
10596
+ });
10597
+ });
10598
+ };
10599
+ tryPort(18793);
10600
+ });
10601
+ });
10602
+ }
10603
+
10513
10604
  // src/auth.ts
10514
10605
  var import_node_child_process = require("node:child_process");
10515
- var import_node_crypto = require("node:crypto");
10606
+ var import_node_crypto2 = require("node:crypto");
10516
10607
  var import_promises2 = require("node:fs/promises");
10517
- var import_node_os4 = require("node:os");
10518
- var import_node_path4 = require("node:path");
10608
+ var import_node_os5 = require("node:os");
10609
+ var import_node_path5 = require("node:path");
10519
10610
  var import_qrcode = __toESM(require_lib(), 1);
10520
10611
  function credentialsPath() {
10521
- return (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10612
+ return (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10522
10613
  }
10523
10614
  async function loadCredentials() {
10524
10615
  try {
@@ -10531,19 +10622,13 @@ async function loadCredentials() {
10531
10622
  }
10532
10623
  async function saveCredentials(creds) {
10533
10624
  const path = credentialsPath();
10534
- await (0, import_promises2.mkdir)((0, import_node_path4.join)(path, ".."), { recursive: true });
10625
+ await (0, import_promises2.mkdir)((0, import_node_path5.join)(path, ".."), { recursive: true });
10535
10626
  await (0, import_promises2.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10536
10627
  try {
10537
10628
  await (0, import_promises2.chmod)(path, 384);
10538
10629
  } catch {
10539
10630
  }
10540
10631
  }
10541
- async function clearCredentials() {
10542
- try {
10543
- await (0, import_promises2.writeFile)(credentialsPath(), "");
10544
- } catch {
10545
- }
10546
- }
10547
10632
  function baseUrlFromWsUrl(wsUrl) {
10548
10633
  const http = wsUrl.replace(/^ws/, "http");
10549
10634
  try {
@@ -10588,8 +10673,8 @@ async function loginAndFetchRegistrationKey(baseUrl, email, password) {
10588
10673
  return { registrationKey: data.registrationKey, user: data.user?.email ?? email };
10589
10674
  }
10590
10675
  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);
10676
+ const dir = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR);
10677
+ const path = (0, import_node_path5.join)(dir, CONFIG_FILE);
10593
10678
  let existing = {};
10594
10679
  try {
10595
10680
  existing = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
@@ -10610,7 +10695,7 @@ async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
10610
10695
  const res = await postJson(`${baseUrl}/oauth/device_authorization`, {
10611
10696
  registrationKey,
10612
10697
  device_id: deviceId,
10613
- device_name: (0, import_node_os4.hostname)(),
10698
+ device_name: (0, import_node_os5.hostname)(),
10614
10699
  platform: process.platform
10615
10700
  });
10616
10701
  if (res.status !== 200) {
@@ -10699,6 +10784,20 @@ async function ensureFreshAccessToken(baseUrl, creds) {
10699
10784
  }
10700
10785
  return refreshInFlight;
10701
10786
  }
10787
+ async function ensureMachineId() {
10788
+ const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10789
+ try {
10790
+ const cfg = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
10791
+ if (typeof cfg.machineId === "string" && cfg.machineId) return cfg.machineId;
10792
+ } catch {
10793
+ }
10794
+ const machineId = "m_" + (0, import_node_crypto2.randomBytes)(16).toString("hex");
10795
+ await mergeLocalConfig({ machineId });
10796
+ return machineId;
10797
+ }
10798
+ async function stableDeviceId() {
10799
+ return "dev_" + await ensureMachineId();
10800
+ }
10702
10801
  async function ensureCredentials(baseUrl, registrationKey) {
10703
10802
  const existing = await loadCredentials();
10704
10803
  if (existing) {
@@ -10708,23 +10807,31 @@ async function ensureCredentials(baseUrl, registrationKey) {
10708
10807
  console.error(`[local] ${e.message}\uFF0C\u91CD\u65B0\u8D70\u6388\u6743\u6D41\u7A0B`);
10709
10808
  }
10710
10809
  }
10711
- return deviceFlowAuthorize(baseUrl, registrationKey, "dev_" + (0, import_node_crypto.randomUUID)());
10810
+ return deviceFlowAuthorize(baseUrl, registrationKey, await stableDeviceId());
10712
10811
  }
10713
10812
 
10714
10813
  // src/client.ts
10814
+ function isPermanentAuthFailure(e) {
10815
+ return e instanceof AuthError && e.message.includes("invalid_grant");
10816
+ }
10715
10817
  async function loadConfig() {
10716
- const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10818
+ const path = (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10717
10819
  try {
10718
- return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
10820
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf-8"));
10719
10821
  } catch {
10720
10822
  return {};
10721
10823
  }
10722
10824
  }
10723
- function buildWhitelist(cfg) {
10724
- const set = new Set(DEFAULT_WHITELIST);
10725
- cfg.whitelist?.add?.forEach((c) => set.add(c));
10726
- cfg.whitelist?.remove?.forEach((c) => set.delete(c));
10727
- return set;
10825
+ function applyConfigDeprecations(cfg) {
10826
+ if (cfg.whitelist) {
10827
+ console.log(
10828
+ '[local] config "whitelist" \u5DF2\u5E9F\u5F03\uFF08v0.4\uFF09\uFF1Abash \u5168\u900F\u4F20\uFF0C\u4E0D\u518D\u6709\u767D\u540D\u5355\u3002\u5982\u9700\u5371\u9669\u547D\u4EE4\u4E8C\u6B21\u786E\u8BA4\uFF0C\u8BBE\u7F6E dangerConfirm: true'
10829
+ );
10830
+ }
10831
+ if (cfg.auditMode && !["full", "commands", "off"].includes(cfg.auditMode)) {
10832
+ console.log(`[local] \u65E0\u6548\u7684 auditMode "${cfg.auditMode}"\uFF0C\u56DE\u9000 full`);
10833
+ cfg.auditMode = "full";
10834
+ }
10728
10835
  }
10729
10836
  function isTTY() {
10730
10837
  return process.stdin.isTTY === true;
@@ -10794,19 +10901,18 @@ async function setupWizard(config, opts) {
10794
10901
  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");
10795
10902
  return null;
10796
10903
  }
10797
- console.log(" 1) \u767B\u5F55\u8D26\u53F7\u81EA\u52A8\u83B7\u53D6\uFF08\u63A8\u8350\uFF09");
10798
- console.log(" 2) \u4ECE\u7F51\u9875\u300C\u63A7\u5236\u9762\u677F\u300D\u590D\u5236\u7C98\u8D34");
10904
+ console.log(" 1) \u901A\u8FC7\u7F51\u9875\u767B\u5F55\u83B7\u53D6\uFF08\u63A8\u8350\uFF0C\u81EA\u52A8\u5B8C\u6210\uFF0C\u65E0\u9700\u590D\u5236\u7C98\u8D34\uFF09");
10905
+ console.log(" 2) \u76F4\u63A5\u8F93\u5165 registrationKey");
10906
+ console.log(" 3) \u5728\u6B64\u8F93\u5165\u8D26\u53F7\u5BC6\u7801\u83B7\u53D6");
10799
10907
  const mode = (await readInput("\u9009\u62E9 [1]: ")).trim();
10800
10908
  if (mode === "2") {
10801
- openBrowser(base);
10802
- 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`);
10803
10909
  const ans = (await readInput("\u7C98\u8D34 registrationKey: ")).trim();
10804
10910
  if (!ans) {
10805
10911
  console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10806
10912
  return null;
10807
10913
  }
10808
10914
  registrationKey = ans;
10809
- } else {
10915
+ } else if (mode === "3") {
10810
10916
  for (; ; ) {
10811
10917
  const email = (await readInput("\u90AE\u7BB1: ")).trim();
10812
10918
  const password = (await readInput("\u5BC6\u7801: ")).trim();
@@ -10824,6 +10930,26 @@ async function setupWizard(config, opts) {
10824
10930
  console.log("[local] \u91CD\u8BD5\uFF08\u6216 Ctrl+C \u9000\u51FA\uFF09\u3002");
10825
10931
  }
10826
10932
  }
10933
+ } else {
10934
+ const cb = await startSetupCallback();
10935
+ const setupUrl = `${base}/local-setup?port=${cb.port}&chan=${cb.chan}`;
10936
+ console.log(`[local] \u5DF2\u6253\u5F00\u6D4F\u89C8\u5668\uFF1A${setupUrl}`);
10937
+ console.log("[local] \u672A\u767B\u5F55\u65F6\u4F1A\u5148\u8FDB\u5165\u767B\u5F55\u9875\uFF0C\u767B\u5F55\u540E\u81EA\u52A8\u7EE7\u7EED\uFF0C\u65E0\u9700\u56DE\u5230\u7EC8\u7AEF");
10938
+ openBrowser(setupUrl);
10939
+ try {
10940
+ registrationKey = await cb.done;
10941
+ console.log("[local] \u5DF2\u4ECE\u7F51\u9875\u83B7\u53D6 registrationKey");
10942
+ } catch {
10943
+ console.log("[local] \u672A\u6536\u5230\u7F51\u9875\u56DE\u8C03\uFF0C\u53EF\u624B\u52A8\u7C98\u8D34 registrationKey\uFF08\u56DE\u8F66\u8DF3\u8FC7\u9000\u51FA\uFF09\uFF1A");
10944
+ const ans = (await readInput("registrationKey: ")).trim();
10945
+ if (!ans) {
10946
+ cb.stop();
10947
+ console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10948
+ return null;
10949
+ }
10950
+ registrationKey = ans;
10951
+ }
10952
+ cb.stop();
10827
10953
  }
10828
10954
  changed = true;
10829
10955
  }
@@ -10844,7 +10970,10 @@ async function startLocal(opts = {}) {
10844
10970
  registrationKey = setup.registrationKey;
10845
10971
  }
10846
10972
  const baseUrl = baseUrlFromWsUrl(url);
10847
- const whitelist = buildWhitelist(config);
10973
+ applyConfigDeprecations(config);
10974
+ const dangerConfirm = config.dangerConfirm === true;
10975
+ const auditMode = config.auditMode ?? "full";
10976
+ const retentionDays = config.auditRetentionDays ?? AUDIT_RETENTION_DAYS;
10848
10977
  const pty = new PtyManager();
10849
10978
  const loggers = /* @__PURE__ */ new Map();
10850
10979
  const lineBuffers = /* @__PURE__ */ new Map();
@@ -10852,17 +10981,130 @@ async function startLocal(opts = {}) {
10852
10981
  (data) => safeSend(data),
10853
10982
  (msg) => console.log(`[local] ${msg}`)
10854
10983
  );
10855
- let credentials = await ensureCredentials(baseUrl, registrationKey);
10984
+ const other = acquireSingleInstanceLock();
10985
+ if (other !== null) {
10986
+ console.error(`[local] \u53E6\u4E00\u4E2A cli-local \u6B63\u5728\u8FD0\u884C (pid ${other})\uFF0C\u62D2\u7EDD\u53CC\u5F00\u3002`);
10987
+ console.error("[local] \u53CC\u5F00\u4F1A\u4E92\u76F8\u5237\u65B0 token \u5BFC\u81F4\u8BBE\u5907\u88AB\u670D\u52A1\u7AEF\u540A\u9500\u3002");
10988
+ console.error("[local] \u540E\u53F0\u5E38\u9A7B\u8BF7\u7528: cli-local gateway start / restart / status");
10989
+ process.exit(1);
10990
+ }
10991
+ const startedAt = Date.now();
10992
+ const logRing = [];
10993
+ if (opts.daemon) {
10994
+ writeGatewayPid();
10995
+ patchConsoleIntoRing(logRing);
10996
+ }
10997
+ let credentials;
10998
+ if (opts.daemon) {
10999
+ const existing = await loadCredentials();
11000
+ if (!existing) {
11001
+ credentials = {
11002
+ deviceId: "",
11003
+ accessToken: "",
11004
+ refreshToken: "",
11005
+ accessTokenExpiresAt: 0
11006
+ };
11007
+ } else {
11008
+ credentials = existing;
11009
+ try {
11010
+ credentials = await ensureFreshAccessToken(baseUrl, existing);
11011
+ } catch {
11012
+ }
11013
+ }
11014
+ } else {
11015
+ credentials = await ensureCredentials(baseUrl, registrationKey);
11016
+ }
10856
11017
  const commands = detectCommands();
10857
- if (opts.daemon) writeGatewayPid();
10858
11018
  console.log(`[local] connecting to ${url}`);
10859
- console.log(`[local] deviceId=${credentials.deviceId}`);
10860
- console.log(`[local] whitelist: ${whitelist.size} commands`);
11019
+ console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
11020
+ console.log(`[local] bash: passthrough (audit: ${auditMode}, retention: ${retentionDays === 0 ? "forever" : retentionDays + "d"}${dangerConfirm ? ", dangerConfirm: on" : ""})`);
10861
11021
  console.log(`[local] available CLIs: ${commands.join(", ")}`);
11022
+ const pruned = pruneAuditLogs(retentionDays);
11023
+ if (pruned > 0) console.log(`[local] pruned ${pruned} expired audit log dir(s)`);
11024
+ const pruneTimer = setInterval(() => {
11025
+ const n = pruneAuditLogs(retentionDays);
11026
+ if (n > 0) console.log(`[local] pruned ${n} expired audit log dir(s)`);
11027
+ }, 24 * 60 * 6e4);
11028
+ pruneTimer.unref?.();
11029
+ let phase = "starting";
11030
+ let phaseReason = "";
11031
+ let reconnectAttempts = 0;
11032
+ const getStatus = () => ({
11033
+ phase,
11034
+ ...phaseReason ? { reason: phaseReason } : {},
11035
+ deviceId: credentials.deviceId || null,
11036
+ hubUrl: url,
11037
+ uptimeMs: Date.now() - startedAt,
11038
+ reconnectAttempts,
11039
+ accessTokenExpiresAt: credentials.accessTokenExpiresAt || null,
11040
+ sessions: pty.list().map((s) => ({
11041
+ sessionId: s.id,
11042
+ cmd: s.cmd,
11043
+ startedAt: s.startedAt
11044
+ }))
11045
+ });
11046
+ function setPhase(p, reason = "") {
11047
+ phase = p;
11048
+ phaseReason = reason;
11049
+ }
11050
+ let pairingTimer = null;
11051
+ let pairingCredMtime = 0;
11052
+ function enterNeedsPairing(reason) {
11053
+ setPhase("needs-pairing", reason);
11054
+ console.error(`[local] \u9700\u8981\u91CD\u65B0\u6388\u6743: ${reason}`);
11055
+ console.error("[local] \u5728\u4EFB\u610F\u7EC8\u7AEF\u8FD0\u884C: cli-local pair");
11056
+ 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");
11057
+ if (pairingTimer) return;
11058
+ try {
11059
+ pairingCredMtime = (0, import_node_fs4.statSync)(credentialsPathOf()).mtimeMs;
11060
+ } catch {
11061
+ pairingCredMtime = 0;
11062
+ }
11063
+ pairingTimer = setInterval(async () => {
11064
+ let mtime;
11065
+ try {
11066
+ mtime = (0, import_node_fs4.statSync)(credentialsPathOf()).mtimeMs;
11067
+ } catch {
11068
+ return;
11069
+ }
11070
+ if (mtime <= pairingCredMtime) return;
11071
+ const fresh = await loadCredentials();
11072
+ if (!fresh?.deviceId || !fresh?.refreshToken) return;
11073
+ pairingCredMtime = mtime;
11074
+ credentials = fresh;
11075
+ console.log("[local] \u68C0\u6D4B\u5230\u65B0\u51ED\u8BC1\uFF0C\u91CD\u8FDE\u4E2D\u2026");
11076
+ if (pairingTimer) clearInterval(pairingTimer);
11077
+ pairingTimer = null;
11078
+ reconnectAttempts = 0;
11079
+ setPhase("connecting");
11080
+ connect();
11081
+ }, 2e3);
11082
+ }
11083
+ let controlApi = null;
11084
+ if (opts.daemon) {
11085
+ const port = readConfiguredPort();
11086
+ controlApi = startControlApi({
11087
+ port,
11088
+ getStatus,
11089
+ getLogs: () => [...logRing],
11090
+ onStop: () => {
11091
+ console.log("[local] \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF08\u63A7\u5236 API\uFF09");
11092
+ exitProcess(0);
11093
+ },
11094
+ killSession: (id) => {
11095
+ try {
11096
+ pty.kill(id);
11097
+ return true;
11098
+ } catch {
11099
+ return false;
11100
+ }
11101
+ }
11102
+ });
11103
+ console.log(`[local] control api: http://127.0.0.1:${port} (\u4EC5\u672C\u673A)`);
11104
+ }
10862
11105
  let ws = null;
10863
11106
  let heartbeatTimer = null;
10864
11107
  let reconnectTimer = null;
10865
- let reconnectAttempts = 0;
10866
11108
  let userExitCode = null;
10867
11109
  function stopHeartbeat() {
10868
11110
  if (heartbeatTimer) clearInterval(heartbeatTimer);
@@ -10885,6 +11127,7 @@ async function startLocal(opts = {}) {
10885
11127
  ws = sock;
10886
11128
  sock.on("open", () => {
10887
11129
  console.log("[local] connected");
11130
+ if (phase !== "needs-pairing") setPhase("connecting");
10888
11131
  sendAuth();
10889
11132
  stopHeartbeat();
10890
11133
  heartbeatTimer = setInterval(() => {
@@ -10901,6 +11144,7 @@ async function startLocal(opts = {}) {
10901
11144
  process.exit(userExitCode);
10902
11145
  }
10903
11146
  console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
11147
+ if (phase !== "needs-pairing") setPhase("reconnecting");
10904
11148
  scheduleReconnect();
10905
11149
  });
10906
11150
  sock.on("error", (err) => {
@@ -10909,6 +11153,7 @@ async function startLocal(opts = {}) {
10909
11153
  }
10910
11154
  async function scheduleReconnect() {
10911
11155
  if (reconnectTimer || userExitCode !== null) return;
11156
+ if (phase === "needs-pairing") return;
10912
11157
  const idx = Math.min(reconnectAttempts, RECONNECT_DELAYS_MS.length - 1);
10913
11158
  const delay = RECONNECT_DELAYS_MS[idx];
10914
11159
  reconnectAttempts++;
@@ -10918,11 +11163,13 @@ async function startLocal(opts = {}) {
10918
11163
  ensureFreshAccessToken(baseUrl, credentials).then((creds) => {
10919
11164
  credentials = creds;
10920
11165
  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);
11166
+ }).catch((e) => {
11167
+ if (isPermanentAuthFailure(e)) {
11168
+ enterNeedsPairing(e.message);
11169
+ return;
11170
+ }
11171
+ console.error(`[local] token \u5237\u65B0\u6682\u65F6\u5931\u8D25: ${e.message}`);
11172
+ scheduleReconnect();
10926
11173
  });
10927
11174
  }, delay);
10928
11175
  }
@@ -10930,6 +11177,9 @@ async function startLocal(opts = {}) {
10930
11177
  userExitCode = code;
10931
11178
  if (reconnectTimer) clearTimeout(reconnectTimer);
10932
11179
  reconnectTimer = null;
11180
+ if (pairingTimer) clearInterval(pairingTimer);
11181
+ pairingTimer = null;
11182
+ controlApi?.close();
10933
11183
  if (opts.daemon) removeGatewayPid();
10934
11184
  transfers.abortAll();
10935
11185
  pty.killAll();
@@ -10959,6 +11209,7 @@ async function startLocal(opts = {}) {
10959
11209
  console.log(`[local] reconnected after ${reconnectAttempts} attempt(s); ${pty.list().length} PTY session(s) alive`);
10960
11210
  }
10961
11211
  reconnectAttempts = 0;
11212
+ setPhase("connected");
10962
11213
  console.log(`[local] auth ok, deviceId=${frame.channelId}`);
10963
11214
  break;
10964
11215
  case "auth-err":
@@ -11001,25 +11252,31 @@ async function startLocal(opts = {}) {
11001
11252
  }
11002
11253
  }
11003
11254
  async function handleAuthErr(reason) {
11004
- if (reason === "access token expired") {
11255
+ if (reason === "access token expired" || reason === "invalid access token") {
11005
11256
  try {
11006
11257
  credentials = await ensureFreshAccessToken(baseUrl, credentials);
11007
11258
  console.log("[local] token refreshed, retrying auth");
11008
11259
  sendAuth();
11009
11260
  return;
11010
11261
  } catch (e) {
11011
- console.error(`[local] ${e.message}`);
11262
+ if (isPermanentAuthFailure(e)) {
11263
+ enterNeedsPairing(e.message);
11264
+ return;
11265
+ }
11266
+ console.error(`[local] token \u5237\u65B0\u6682\u65F6\u5931\u8D25\uFF08${e.message}\uFF09\uFF0C\u8F6C\u5165\u91CD\u8FDE`);
11267
+ try {
11268
+ ws?.close();
11269
+ } catch {
11270
+ }
11271
+ return;
11012
11272
  }
11013
11273
  }
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);
11274
+ enterNeedsPairing(reason);
11018
11275
  }
11019
11276
  function handleSpawn(frame) {
11020
11277
  const sessionId = frame.sessionId;
11021
11278
  try {
11022
- const logger = new SessionLogger(sessionId, frame.cmd);
11279
+ const logger = new SessionLogger(sessionId, frame.cmd, auditMode);
11023
11280
  pty.spawn(
11024
11281
  sessionId,
11025
11282
  { cmd: frame.cmd, cols: frame.cols, rows: frame.rows, cwd: frame.cwd },
@@ -11097,7 +11354,7 @@ async function startLocal(opts = {}) {
11097
11354
  if (char === "\r" || char === "\n") {
11098
11355
  const line = buf;
11099
11356
  buf = "";
11100
- const result = checkCommand(line, whitelist);
11357
+ const result = checkCommand(line, dangerConfirm);
11101
11358
  if (result.allowed) {
11102
11359
  ptyWriteChar(sessionId, "\r");
11103
11360
  logger.logInput(new TextEncoder().encode(line + "\r"));
@@ -11160,7 +11417,57 @@ async function startLocal(opts = {}) {
11160
11417
  }
11161
11418
  process.on("SIGINT", () => cleanup(0));
11162
11419
  process.on("SIGTERM", () => cleanup(0));
11163
- connect();
11420
+ if (credentials.deviceId) {
11421
+ setPhase("connecting");
11422
+ connect();
11423
+ } else {
11424
+ enterNeedsPairing("\u65E0\u51ED\u8BC1\uFF08credentials.json \u7F3A\u5931\u6216\u4E3A\u7A7A\uFF09");
11425
+ }
11426
+ }
11427
+ function credentialsPathOf() {
11428
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "credentials.json");
11429
+ }
11430
+ function instanceLockPath() {
11431
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "instance.lock");
11432
+ }
11433
+ function processAlive(pid) {
11434
+ try {
11435
+ process.kill(pid, 0);
11436
+ return true;
11437
+ } catch {
11438
+ return false;
11439
+ }
11440
+ }
11441
+ function acquireSingleInstanceLock() {
11442
+ const path = instanceLockPath();
11443
+ try {
11444
+ const pid = Number((0, import_node_fs4.readFileSync)(path, "utf-8").trim());
11445
+ if (Number.isFinite(pid) && pid !== process.pid && processAlive(pid)) {
11446
+ return pid;
11447
+ }
11448
+ } catch {
11449
+ }
11450
+ try {
11451
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(path, ".."), { recursive: true });
11452
+ (0, import_node_fs4.writeFileSync)(path, String(process.pid));
11453
+ } catch {
11454
+ }
11455
+ return null;
11456
+ }
11457
+ function patchConsoleIntoRing(ring) {
11458
+ const CAP = 500;
11459
+ const push = (line) => {
11460
+ ring.push(line);
11461
+ if (ring.length > CAP) ring.splice(0, ring.length - CAP);
11462
+ };
11463
+ const wrap = (orig) => {
11464
+ return (...args) => {
11465
+ push(args.map((a) => typeof a === "string" ? a : String(a)).join(" "));
11466
+ orig(...args);
11467
+ };
11468
+ };
11469
+ console.log = wrap(console.log.bind(console));
11470
+ console.error = wrap(console.error.bind(console));
11164
11471
  }
11165
11472
  function toUint8(raw) {
11166
11473
  if (raw instanceof Buffer) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
@@ -11171,21 +11478,21 @@ function toUint8(raw) {
11171
11478
  throw new Error("unexpected raw type");
11172
11479
  }
11173
11480
  function gatewayPidFile() {
11174
- return (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11481
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid");
11175
11482
  }
11176
11483
  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));
11484
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)((0, import_node_os6.homedir)(), DEFAULT_CONFIG_DIR), { recursive: true });
11485
+ (0, import_node_fs4.writeFileSync)(gatewayPidFile(), String(process.pid));
11179
11486
  }
11180
11487
  function removeGatewayPid() {
11181
11488
  try {
11182
- (0, import_node_fs3.rmSync)(gatewayPidFile());
11489
+ (0, import_node_fs4.rmSync)(gatewayPidFile());
11183
11490
  } catch {
11184
11491
  }
11185
11492
  }
11186
11493
  function gatewayPid() {
11187
11494
  try {
11188
- const pid = Number((0, import_node_fs3.readFileSync)(gatewayPidFile(), "utf-8").trim());
11495
+ const pid = Number((0, import_node_fs4.readFileSync)(gatewayPidFile(), "utf-8").trim());
11189
11496
  return Number.isInteger(pid) && pid > 0 ? pid : null;
11190
11497
  } catch {
11191
11498
  return null;
@@ -11219,15 +11526,15 @@ function resolveHere() {
11219
11526
  if (DEBUG) console.error("[debug] cannot resolve script path");
11220
11527
  return;
11221
11528
  }
11222
- const hereDir = (0, import_node_path6.dirname)(here);
11529
+ const hereDir = (0, import_node_path7.dirname)(here);
11223
11530
  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")
11531
+ (0, import_node_path7.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11532
+ (0, import_node_path7.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11533
+ (0, import_node_path7.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
11227
11534
  ];
11228
11535
  let prebuildsDir = null;
11229
11536
  for (const c of candidates) {
11230
- if ((0, import_node_fs4.existsSync)(c)) {
11537
+ if ((0, import_node_fs5.existsSync)(c)) {
11231
11538
  prebuildsDir = c;
11232
11539
  break;
11233
11540
  }
@@ -11237,11 +11544,11 @@ function resolveHere() {
11237
11544
  return;
11238
11545
  }
11239
11546
  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)) {
11547
+ for (const sub of (0, import_node_fs5.readdirSync)(prebuildsDir)) {
11548
+ const helper = (0, import_node_path7.join)(prebuildsDir, sub, "spawn-helper");
11549
+ if ((0, import_node_fs5.existsSync)(helper)) {
11243
11550
  try {
11244
- (0, import_node_fs4.chmodSync)(helper, 493);
11551
+ (0, import_node_fs5.chmodSync)(helper, 493);
11245
11552
  n++;
11246
11553
  } catch {
11247
11554
  }
@@ -11252,49 +11559,56 @@ function resolveHere() {
11252
11559
  var argUrl = process.argv[2];
11253
11560
  var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
11254
11561
  if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl === "help")) {
11255
- console.log(`cli-local \u2014 local PTY agent for cli-remote
11562
+ console.log(`cli-local \u2014 local PTY agent for cli-remote (gateway \u5E38\u9A7B\u670D\u52A1)
11256
11563
 
11257
11564
  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
11565
+ cli-local \u65E0 gateway \u4E14\u5DF2\u914D\u7F6E\u65F6\u524D\u53F0\u8FD0\u884C\uFF1B\u5426\u5219\u663E\u793A\u72B6\u6001
11566
+ cli-local pair \u91CD\u65B0\u6388\u6743\uFF08\u540A\u9500/\u8FC7\u671F\u540E\u91CD\u914D\u5BF9\uFF1Bgateway \u70ED\u52A0\u8F7D\uFF09
11567
+ cli-local gateway start \u540E\u53F0\u542F\u52A8\u5E38\u9A7B gateway\uFF08\u63A8\u8350\uFF09
11568
+ cli-local gateway stop|restart|status|logs [-f] [-n N]
11569
+ cli-local install [HUB_URL] \u5B89\u88C5 macOS launchd \u5E38\u9A7B\u670D\u52A1
11570
+ cli-local uninstall \u5378\u8F7D launchd \u5E38\u9A7B\u670D\u52A1
11571
+
11572
+ \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
11573
+ \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
11574
 
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
11575
+ \u88AB\u540A\u9500/\u51ED\u8BC1\u8FC7\u671F\u65F6 gateway \u4E0D\u4F1A\u9000\u51FA\uFF0C\u800C\u662F\u8FDB\u5165 needs-pairing \u7B49\u5F85\uFF1B
11576
+ \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
11577
 
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
11578
+ \u672C\u5730\u63A7\u5236 API\uFF08\u4EC5 127.0.0.1\uFF0Cgateway \u8FD0\u884C\u65F6\uFF09\uFF1A
11579
+ GET /status /healthz /logs /sessions POST /stop
11580
+ \u7AEF\u53E3\u9ED8\u8BA4 18790\uFF0Cconfig.json gateway.apiPort \u6216 CLI_LOCAL_API_PORT \u53EF\u6539
11270
11581
 
11271
11582
  environment:
11272
11583
  RELAY_URL hub ws url (overrides argv and config)
11273
11584
  REGISTRATION_KEY registration key (fallback of config.json)
11274
11585
  CLI_LOCAL_NO_BROWSER=1 \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF08\u53EA\u6253\u5370\u94FE\u63A5\uFF09
11586
+ CLI_LOCAL_API_PORT gateway \u63A7\u5236API\u7AEF\u53E3
11275
11587
  HOME where ~/.cli-remote/ lives
11276
11588
 
11277
11589
  config (~/.cli-remote/config.json):
11278
11590
  {
11279
11591
  "relayUrl": "wss://your-hub/ws",
11280
11592
  "registrationKey": "from-the-web-devices-page",
11593
+ "gateway": { "apiPort": 18790 },
11281
11594
  "whitelist": { "add": [], "remove": [] }
11282
11595
  }
11283
11596
 
11284
11597
  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`);
11598
+ cli-local pair # \u9996\u6B21/\u91CD\u65B0\u6388\u6743
11599
+ cli-local gateway start # \u540E\u53F0\u5E38\u9A7B
11600
+ cli-local gateway status # \u67E5\u770B\u8FDE\u63A5\u72B6\u6001`);
11287
11601
  process.exit(0);
11288
11602
  }
11289
11603
  var LAUNCHD_LABEL = "net.cli-remote.local";
11290
11604
  function launchdPlistPath() {
11291
- return (0, import_node_path6.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11605
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11292
11606
  }
11293
11607
  function entryScript() {
11294
11608
  const a1 = process.argv[1];
11295
11609
  if (!a1 || a1.endsWith(".ts")) return null;
11296
11610
  try {
11297
- return (0, import_node_fs4.realpathSync)(a1);
11611
+ return (0, import_node_fs5.realpathSync)(a1);
11298
11612
  } catch {
11299
11613
  return a1;
11300
11614
  }
@@ -11312,7 +11626,7 @@ function launchdInstall(urlArg) {
11312
11626
  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
11627
  process.exit(1);
11314
11628
  }
11315
- const home = (0, import_node_os6.homedir)();
11629
+ const home = (0, import_node_os7.homedir)();
11316
11630
  const entry = entryScript();
11317
11631
  if (!entry) {
11318
11632
  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 +11634,28 @@ function launchdInstall(urlArg) {
11320
11634
  process.exit(1);
11321
11635
  }
11322
11636
  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");
11637
+ const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
11638
+ (0, import_node_fs5.mkdirSync)(cfgDir, { recursive: true });
11639
+ const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
11326
11640
  let cfg = {};
11327
11641
  try {
11328
- cfg = JSON.parse((0, import_node_fs4.readFileSync)(cfgPath, "utf-8"));
11642
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11329
11643
  } catch {
11330
11644
  }
11331
11645
  cfg.relayUrl = urlArg;
11332
- (0, import_node_fs4.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11646
+ (0, import_node_fs5.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11333
11647
  console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11334
11648
  }
11335
- const credPath = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11336
- if (!(0, import_node_fs4.existsSync)(credPath)) {
11649
+ const credPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11650
+ if (!(0, import_node_fs5.existsSync)(credPath)) {
11337
11651
  console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11338
11652
  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
11653
  process.exit(1);
11340
11654
  }
11341
11655
  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 });
11656
+ const logDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "logs");
11657
+ (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(plist), { recursive: true });
11658
+ (0, import_node_fs5.mkdirSync)(logDir, { recursive: true });
11345
11659
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
11346
11660
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11347
11661
  <plist version="1.0">
@@ -11355,12 +11669,12 @@ function launchdInstall(urlArg) {
11355
11669
  <key>RunAtLoad</key><true/>
11356
11670
  <key>KeepAlive</key><true/>
11357
11671
  <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>
11672
+ <key>StandardOutPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.out.log")}</string>
11673
+ <key>StandardErrorPath</key><string>${(0, import_node_path7.join)(logDir, "daemon.err.log")}</string>
11360
11674
  </dict>
11361
11675
  </plist>
11362
11676
  `;
11363
- (0, import_node_fs4.writeFileSync)(plist, xml);
11677
+ (0, import_node_fs5.writeFileSync)(plist, xml);
11364
11678
  const uid = process.getuid?.();
11365
11679
  if (uid === void 0) {
11366
11680
  console.error("[local] \u65E0\u6CD5\u786E\u5B9A uid\uFF0Claunchd \u5B89\u88C5\u5931\u8D25");
@@ -11378,7 +11692,7 @@ function launchdInstall(urlArg) {
11378
11692
  }
11379
11693
  function launchdUninstall() {
11380
11694
  const plist = launchdPlistPath();
11381
- if (!(0, import_node_fs4.existsSync)(plist)) {
11695
+ if (!(0, import_node_fs5.existsSync)(plist)) {
11382
11696
  console.log("[local] \u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u5E38\u9A7B\u670D\u52A1\uFF08nothing to uninstall\uFF09");
11383
11697
  process.exit(0);
11384
11698
  }
@@ -11386,7 +11700,7 @@ function launchdUninstall() {
11386
11700
  const guiDomain = `gui/${uid ?? 0}`;
11387
11701
  run("launchctl", ["bootout", guiDomain, plist], true);
11388
11702
  run("launchctl", ["unload", "-w", plist], true);
11389
- (0, import_node_fs4.rmSync)(plist);
11703
+ (0, import_node_fs5.rmSync)(plist);
11390
11704
  console.log(`[local] launchd \u670D\u52A1\u5DF2\u505C\u6B62\u5E76\u5378\u8F7D\uFF1A${LAUNCHD_LABEL}`);
11391
11705
  }
11392
11706
  if (argUrl && !argMatch && argUrl === "install") {
@@ -11397,70 +11711,70 @@ if (argUrl && !argMatch && argUrl === "uninstall") {
11397
11711
  launchdUninstall();
11398
11712
  process.exit(0);
11399
11713
  }
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
- }
11714
+ if (argUrl === "pair") {
11715
+ void (async () => {
11716
+ const home = (0, import_node_os7.homedir)();
11717
+ const cfgPath = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR, "config.json");
11718
+ let cfg = {};
11409
11719
  try {
11410
- process.kill(pid, "SIGTERM");
11720
+ cfg = JSON.parse((0, import_node_fs5.readFileSync)(cfgPath, "utf-8"));
11411
11721
  } catch {
11412
11722
  }
11413
- const t0 = Date.now();
11414
- while (Date.now() - t0 < 5e3 && gatewayAlive()) {
11415
- const end = Date.now() + 150;
11416
- while (Date.now() < end) {
11417
- }
11723
+ const url = process.argv[4] ?? process.env.RELAY_URL ?? cfg.relayUrl;
11724
+ if (!url) {
11725
+ 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");
11726
+ process.exit(1);
11418
11727
  }
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");
11728
+ if (!cfg.registrationKey && !process.env.REGISTRATION_KEY) {
11729
+ console.error("[pair] \u7F3A\u5C11 registrationKey\uFF1A\u5199\u5165 ~/.cli-remote/config.json \u6216\u8BBE\u7F6E REGISTRATION_KEY");
11429
11730
  process.exit(1);
11430
11731
  }
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 {
11732
+ const prev = await loadCredentials();
11733
+ const deviceId = prev?.deviceId || await stableDeviceId();
11734
+ console.log(`[pair] hub: ${url}\uFF08\u8BBE\u5907 ${deviceId}${prev ? "\uFF0C\u590D\u7528" : "\uFF0C\u7531\u673A\u5668\u7801\u6D3E\u751F"}\uFF09`);
11735
+ const creds = await deviceFlowAuthorize(
11736
+ baseUrlFromWsUrl(url),
11737
+ cfg.registrationKey ?? process.env.REGISTRATION_KEY,
11738
+ deviceId
11739
+ );
11740
+ console.log(`[pair] \u6388\u6743\u5B8C\u6210 deviceId=${creds.deviceId}`);
11741
+ const port = readConfiguredPort();
11742
+ const health = await apiGet(port, "/healthz");
11743
+ if (health?.ok) {
11744
+ 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");
11745
+ } else {
11746
+ console.log("[pair] gateway \u672A\u8FD0\u884C\uFF1Acli-local gateway start \u542F\u52A8\u5E38\u9A7B\u670D\u52A1");
11437
11747
  }
11438
- console.log(`[gateway] running (pid ${pid})`);
11439
- console.log(`[gateway] hub: ${hub}`);
11440
- console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11441
- process.exit(0);
11748
+ })().catch((e) => {
11749
+ console.error(`[pair] ${e.message}`);
11750
+ process.exit(1);
11751
+ });
11752
+ }
11753
+ if (argUrl === "gateway") {
11754
+ let gatewayLogFile = function() {
11755
+ return (0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "logs", "gateway.log");
11442
11756
  }, gatewayLogs = function() {
11443
11757
  const follow = rest.includes("-f") || rest.includes("--follow");
11444
11758
  const li = rest.findIndex((a) => a === "-n" || a === "--lines");
11445
11759
  const lines = li >= 0 ? Number(rest[li + 1]) || 50 : 50;
11446
11760
  const file = gatewayLogFile();
11447
- if (!(0, import_node_fs4.existsSync)(file)) {
11761
+ if (!(0, import_node_fs5.existsSync)(file)) {
11448
11762
  console.log(`[gateway] \u6682\u65E0\u65E5\u5FD7\uFF08${file} \u4E0D\u5B58\u5728\uFF09`);
11449
11763
  process.exit(0);
11450
11764
  }
11451
- const tail = (0, import_node_fs4.readFileSync)(file, "utf-8").trimEnd().split("\n").slice(-lines);
11765
+ const tail = (0, import_node_fs5.readFileSync)(file, "utf-8").trimEnd().split("\n").slice(-lines);
11452
11766
  console.log(tail.join("\n"));
11453
11767
  if (!follow) process.exit(0);
11454
- let size = (0, import_node_fs4.statSync)(file).size;
11768
+ let size = (0, import_node_fs5.statSync)(file).size;
11455
11769
  console.log("--- following (Ctrl+C \u9000\u51FA) ---");
11456
11770
  setInterval(() => {
11457
11771
  try {
11458
- const st = (0, import_node_fs4.statSync)(file);
11772
+ const st = (0, import_node_fs5.statSync)(file);
11459
11773
  if (st.size > size) {
11460
- const fd = (0, import_node_fs4.openSync)(file, "r");
11774
+ const fd = (0, import_node_fs5.openSync)(file, "r");
11461
11775
  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);
11776
+ (0, import_node_fs5.readSync)(fd, buf, 0, buf.length, size);
11777
+ (0, import_node_fs5.closeSync)(fd);
11464
11778
  size = st.size;
11465
11779
  process.stdout.write(buf.toString("utf-8"));
11466
11780
  } else if (st.size < size) {
@@ -11470,7 +11784,7 @@ if (argUrl === "gateway") {
11470
11784
  }
11471
11785
  }, 1e3);
11472
11786
  };
11473
- gatewayLogFile2 = gatewayLogFile, stopWorker2 = stopWorker, gatewayStop2 = gatewayStop, gatewayStatus2 = gatewayStatus, gatewayLogs2 = gatewayLogs;
11787
+ gatewayLogFile2 = gatewayLogFile, gatewayLogs2 = gatewayLogs;
11474
11788
  const action = process.argv[3] ?? "";
11475
11789
  const rest = process.argv.slice(4);
11476
11790
  const hubArg = (() => {
@@ -11482,22 +11796,22 @@ if (argUrl === "gateway") {
11482
11796
  console.log(`[gateway] already running (pid ${gatewayPid()})`);
11483
11797
  process.exit(0);
11484
11798
  }
11485
- const home = (0, import_node_os6.homedir)();
11799
+ const home = (0, import_node_os7.homedir)();
11486
11800
  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);
11801
+ const cfgDir = (0, import_node_path7.join)(home, DEFAULT_CONFIG_DIR);
11802
+ const cfgPath = (0, import_node_path7.join)(cfgDir, "config.json");
11803
+ const hasCfg = (0, import_node_fs5.existsSync)(cfgPath);
11491
11804
  if (!entry) {
11492
11805
  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
11806
  process.exit(1);
11494
11807
  }
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");
11808
+ if (!hasCfg) {
11809
+ 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");
11810
+ 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
11811
  process.exit(1);
11498
11812
  }
11499
- (0, import_node_fs4.mkdirSync)((0, import_node_path6.dirname)(gatewayLogFile()), { recursive: true });
11500
- const out = (0, import_node_fs4.openSync)(gatewayLogFile(), "a");
11813
+ (0, import_node_fs5.mkdirSync)((0, import_node_path7.dirname)(gatewayLogFile()), { recursive: true });
11814
+ const out = (0, import_node_fs5.openSync)(gatewayLogFile(), "a");
11501
11815
  const env = {};
11502
11816
  for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
11503
11817
  const args = [entry, "gateway", "run"];
@@ -11509,41 +11823,124 @@ if (argUrl === "gateway") {
11509
11823
  });
11510
11824
  child.unref();
11511
11825
  console.log(`[gateway] started (pid ${child.pid})`);
11826
+ const port = readConfiguredPort();
11827
+ for (let i = 0; i < 20; i++) {
11828
+ await new Promise((res) => setTimeout(res, 250));
11829
+ const h = await apiGet(port, "/healthz", 400);
11830
+ if (h?.ok) {
11831
+ console.log(`[gateway] \u63A7\u5236API\u5C31\u7EEA: http://127.0.0.1:${port}\uFF08\u4EC5\u672C\u673A\uFF09`);
11832
+ break;
11833
+ }
11834
+ }
11512
11835
  console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11513
11836
  console.log("[gateway] \u72B6\u6001: cli-local gateway status");
11514
11837
  }
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
11838
+ async function stopWorker() {
11839
+ const port = readConfiguredPort();
11840
+ if (await apiPost(port, "/stop", 1500)) {
11841
+ const t02 = Date.now();
11842
+ while (Date.now() - t02 < 5e3 && gatewayAlive()) {
11843
+ await new Promise((res) => setTimeout(res, 150));
11844
+ }
11845
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11846
+ return;
11847
+ }
11848
+ const pid = gatewayPid();
11849
+ if (pid === null || !gatewayAlive()) {
11850
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11851
+ return;
11852
+ }
11853
+ try {
11854
+ process.kill(pid, "SIGTERM");
11855
+ } catch {
11856
+ }
11857
+ const t0 = Date.now();
11858
+ while (Date.now() - t0 < 5e3 && gatewayAlive()) {
11859
+ await new Promise((res) => setTimeout(res, 150));
11860
+ }
11861
+ (0, import_node_fs5.rmSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), DEFAULT_CONFIG_DIR, "gateway.pid"), { force: true });
11862
+ }
11863
+ async function gatewayStop() {
11864
+ const pid = gatewayPid();
11865
+ const wasRunning = gatewayAlive();
11866
+ await stopWorker();
11867
+ console.log(wasRunning ? `[gateway] stopped (pid ${pid ?? "?"})` : "[gateway] not running");
11868
+ process.exit(0);
11869
+ }
11870
+ async function gatewayStatus() {
11871
+ const port = readConfiguredPort();
11872
+ const st = await apiGet(port, "/status", 600);
11873
+ if (st) {
11874
+ const phaseLabel = {
11875
+ starting: "\u542F\u52A8\u4E2D",
11876
+ connecting: "\u8FDE\u63A5\u4E2D",
11877
+ connected: "\u5DF2\u8FDE\u63A5",
11878
+ reconnecting: "\u91CD\u8FDE\u4E2D",
11879
+ "needs-pairing": "\u9700\u8981\u91CD\u65B0\u6388\u6743"
11880
+ };
11881
+ const up = Math.floor(st.uptimeMs / 1e3);
11882
+ 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`;
11883
+ const exp = st.accessTokenExpiresAt ? new Date(st.accessTokenExpiresAt).toLocaleTimeString() : "\u2014";
11884
+ console.log(`[gateway] running (pid ${gatewayPid() ?? "?"}, api :${port})`);
11885
+ console.log(`[gateway] \u72B6\u6001: ${phaseLabel[st.phase] ?? st.phase}${st.reason ? ` (${st.reason})` : ""}`);
11886
+ console.log(`[gateway] hub: ${st.hubUrl}`);
11887
+ console.log(`[gateway] deviceId: ${st.deviceId ?? "\u2014"}`);
11888
+ console.log(`[gateway] uptime: ${upStr}, token \u5230\u671F: ${exp}`);
11889
+ console.log(`[gateway] PTY \u4F1A\u8BDD: ${st.sessions.length} \u4E2A${st.sessions.length ? ` (${st.sessions.map((s) => s.cmd).join(", ")})` : ""}`);
11890
+ if (st.phase === "needs-pairing") {
11891
+ console.log("[gateway] \u26A0 \u8FD0\u884C cli-local pair \u91CD\u65B0\u6388\u6743\uFF08\u5B8C\u6210\u540E\u81EA\u52A8\u91CD\u8FDE\uFF09");
11892
+ }
11893
+ process.exit(0);
11894
+ }
11895
+ if (!gatewayAlive()) {
11896
+ console.log("[gateway] stopped");
11897
+ process.exit(1);
11898
+ }
11899
+ const pid = gatewayPid();
11900
+ let hub = "(\u672A\u914D\u7F6E)";
11901
+ try {
11902
+ 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"));
11903
+ hub = cfg.relayUrl ?? hub;
11904
+ } catch {
11905
+ }
11906
+ console.log(`[gateway] running (pid ${pid}) \u2014 \u63A7\u5236API\u672A\u54CD\u5E94\uFF08\u65E7\u7248\u672C\uFF1F\uFF09`);
11907
+ console.log(`[gateway] hub: ${hub}`);
11908
+ console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11909
+ process.exit(0);
11910
+ }
11911
+ void (async () => {
11912
+ switch (action) {
11913
+ case "start":
11914
+ gatewayStart().then(() => process.exit(0)).catch((e) => {
11915
+ console.error(`[gateway] ${e.message}`);
11916
+ process.exit(1);
11917
+ });
11918
+ break;
11919
+ case "stop":
11920
+ gatewayStop();
11921
+ break;
11922
+ case "restart":
11923
+ await stopWorker();
11924
+ gatewayStart().then(() => process.exit(0)).catch((e) => {
11925
+ console.error(`[gateway] ${e.message}`);
11926
+ process.exit(1);
11927
+ });
11928
+ break;
11929
+ case "status":
11930
+ void gatewayStatus();
11931
+ break;
11932
+ case "logs":
11933
+ gatewayLogs();
11934
+ break;
11935
+ case "run":
11936
+ startLocal({ url: hubArg, daemon: true }).catch((err) => {
11937
+ if (err instanceof AuthError) console.error(`[local] ${err.message}`);
11938
+ else console.error("[local] fatal:", err);
11939
+ process.exit(1);
11940
+ });
11941
+ break;
11942
+ default:
11943
+ console.log(`cli-local gateway \u2014 \u540E\u53F0\u8FD0\u884C\u7BA1\u7406
11547
11944
 
11548
11945
  usage:
11549
11946
  cli-local gateway start [--hub URL] \u540E\u53F0\u542F\u52A8\uFF08\u81EA\u52A8\u91CD\u8FDE\uFF09
@@ -11553,24 +11950,55 @@ usage:
11553
11950
  cli-local gateway logs [-f] [-n N] \u67E5\u770B\u65E5\u5FD7\uFF08-f \u8DDF\u968F\uFF09
11554
11951
  cli-local gateway run [--hub URL] \u524D\u53F0\u8FD0\u884C\uFF08\u4F9B start \u62C9\u8D77\uFF09
11555
11952
 
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
- }
11953
+ \u8BF4\u660E\uFF1A\u540E\u53F0\u8FDB\u7A0B\u7531 pid \u6587\u4EF6\u7BA1\u7406\uFF08~/.cli-remote/gateway.pid\uFF09\uFF0C\u672C\u5730\u63A7\u5236API
11954
+ \u4EC5 127.0.0.1\uFF08\u9ED8\u8BA4 18790\uFF0Cconfig.json gateway.apiPort \u53EF\u6539\uFF09\uFF0C\u65E5\u5FD7\u5728
11955
+ ~/.cli-remote/logs/gateway.log\u3002\u65E0\u51ED\u8BC1\u4E5F\u80FD\u542F\u52A8\uFF08\u8FDB\u5165 needs-pairing \u7B49
11956
+ cli-local pair\uFF09\u3002macOS \u60F3\u5F00\u673A\u81EA\u542F\u7528 launchd\uFF1Acli-local install\u3002`);
11957
+ process.exit(action ? 1 : 0);
11958
+ }
11959
+ })().catch((e) => {
11960
+ console.error(`[gateway] ${e.message}`);
11961
+ process.exit(1);
11962
+ });
11561
11963
  }
11562
11964
  var gatewayLogFile2;
11563
- var stopWorker2;
11564
- var gatewayStop2;
11565
- var gatewayStatus2;
11566
11965
  var gatewayLogs2;
11567
- startLocal(argMatch ? { url: argUrl } : {}).catch((err) => {
11568
- if (err instanceof AuthError) {
11569
- console.error(`[local] ${err.message}`);
11966
+ if (!argUrl || argMatch) {
11967
+ if (!argUrl) {
11968
+ void (async () => {
11969
+ const port = readConfiguredPort();
11970
+ const health = await apiGet(port, "/healthz", 400);
11971
+ if (health?.ok) {
11972
+ console.log("[local] gateway \u5E38\u9A7B\u670D\u52A1\u8FD0\u884C\u4E2D\u3002\u5B9E\u65F6\u72B6\u6001:");
11973
+ const entry = process.argv[1];
11974
+ if (entry && !entry.endsWith(".ts")) {
11975
+ const { execFileSync: ex } = await import("node:child_process");
11976
+ try {
11977
+ ex(process.execPath, [entry, "gateway", "status"], { stdio: "inherit" });
11978
+ } catch {
11979
+ console.log(`[local] \u63A7\u5236API: http://127.0.0.1:${port}/status`);
11980
+ }
11981
+ } else {
11982
+ console.log(`[local] \u63A7\u5236API: http://127.0.0.1:${port}/status`);
11983
+ }
11984
+ process.exit(0);
11985
+ }
11986
+ await startLocal({});
11987
+ })().catch((err) => {
11988
+ if (err instanceof AuthError) console.error(`[local] ${err.message}`);
11989
+ else console.error("[local] fatal:", err);
11990
+ process.exit(1);
11991
+ });
11570
11992
  } else {
11571
- console.error("[local] fatal:", err);
11993
+ startLocal({ url: argUrl }).catch((err) => {
11994
+ if (err instanceof AuthError) {
11995
+ console.error(`[local] ${err.message}`);
11996
+ } else {
11997
+ console.error("[local] fatal:", err);
11998
+ }
11999
+ process.exit(1);
12000
+ });
11572
12001
  }
11573
- process.exit(1);
11574
- });
12002
+ }
11575
12003
  process.on("SIGINT", () => process.exit(0));
11576
12004
  process.on("SIGTERM", () => process.exit(0));