@skylandnpm/octopus-proxy 0.1.52 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ Proxy 主动连接 Hub 的 `/ws/proxy-chat`,无需开放入站端口或安装
20
20
 
21
21
  macOS 12.3 及以上运行 `octopus-proxy onboard` 时,配置保存后、服务启动前会准备截屏辅助程序并申请系统屏幕录制权限;已授权时不会重复弹窗。交互模式和 `--non-interactive` 均执行此步骤,`--no-install-service` 也会申请。请在系统弹窗中允许访问;拒绝、超时或辅助程序编译失败时,向导提示原因与授权路径,并继续初始化。在“系统设置 → 隐私与安全性 → 屏幕录制(或屏幕与系统音频录制)”中应授权弹窗显示的实际程序,可能是 Terminal、iTerm、编辑器或 node,不保证出现 `octopus-desktop-capture`。onboard 和 doctor 会显示辅助程序路径,文件名不等于系统授权名称。终端授权不代表后台服务已授权,需在 Claw 点击截屏,让实际服务申请权限,按实际名称授权后重启 Proxy。Linux 没有通用的截屏授权弹窗,需使用桌面用户的图形会话环境;平台依赖与设置见[桌面快照说明](../../docs/claw-desktop-snapshots.md)。
22
22
 
23
- 添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
23
+ 基础设置完成后,向导会提示至少需要配置一个工作项目。新项目依次填写项目 ID、名称(默认使用项目 ID)和目录;完成第一个项目后,可选择继续添加更多项目,或直接回车完成项目配置并继续保存。只有选择继续添加时,才会进入下一个项目 ID 的输入流程。
24
24
 
25
25
  `octopus-proxy doctor` 会显示“桌面截屏支持”和“屏幕录制权限”:macOS 检查系统版本、辅助程序、活动显示器和当前授权状态;未授权时给出 onboard 申请及系统设置指引。Linux 检查图形会话、显示器和截屏依赖,缺少依赖、无桌面或不支持的 Wayland 环境会显示具体提示;Linux 无统一的授权查询接口,因此会明确显示权限尚无法确认,需在 Claw 实际截屏验证。诊断不触发授权弹窗、不截屏,`doctor --fix` 也不会自动申请截屏权限。这些检查使用运行 doctor 的用户与环境;服务以其他启动程序或图形会话运行时,其权限与环境需要单独确认。
26
26
 
@@ -102,6 +102,33 @@ octopus-proxy update
102
102
 
103
103
  安装或版本校验失败时不会触发服务重启;包安装失败不保证旧版本文件完整保留。已安装新版本但重启失败时会单独提示,可运行 `octopus-proxy doctor` 和 `octopus-proxy logs` 排查。此命令用于 npm 全局安装,覆盖安装前会检查 npm 安装目录是否与当前命令一致;源码、本地安装或 Node.js 环境不一致时会中止并给出提示。
104
104
 
105
+ ## 服务启动时报 `Unexpected token '??='`
106
+
107
+ 这通常表示服务实际使用了旧版 Node.js。错误栈中 `.nvm/versions/node/v22.../lib/node_modules` 是包的安装位置,不能说明运行它的 Node.js 版本。安装器只会选择 Node.js 22 或更高版本;旧版安装器可能优先选中能执行 `--version` 的旧系统 Node。
108
+
109
+ 对于通过 NVM 安装的环境,可切换到安装 Proxy 的 Node.js 版本,并重写服务定义,明确指定服务使用的 Node。例如包安装在 `v22.22.0` 下时:
110
+
111
+ ```sh
112
+ nvm use 22.22.0
113
+ node --version
114
+ octopus-proxy install --force --node-path "$(node -p 'process.execPath')"
115
+ octopus-proxy status
116
+ ```
117
+
118
+ `install --force` 会重写并重新加载服务定义,保留配置和状态数据;自定义配置时带上原来的 `--config <path>`。单独运行 `restart` 会继续使用已有服务定义中的 Node 路径。NVM 中被固定的版本需要保留,删除该版本后应重新安装服务。
119
+
120
+ `octopus-proxy doctor` 会分别显示当前命令和已安装服务的 Node.js 路径、版本及最低要求(Node.js >= 22)。服务检查读取已保存的 launchd/systemd 定义,再执行其中 Node 的 `--version`;版本过低、路径失效或版本输出无法识别都会标记为失败。配置无效时仍会检查 Node;尚未安装服务时提示未检查服务版本。`doctor --fix` 重写服务定义后会重新检查保存的 Node 路径和版本。
121
+
122
+ ## 连接工作区时报 `ec_asn1.cc / p_ec.cc DECODE_ERROR`
123
+
124
+ 如果服务在线,但 Claw 在 TLS 握手时显示上述错误,可能是主机身份由 macOS 自带的 LibreSSL 生成,证书使用了客户端不支持的显式椭圆曲线参数。这与服务是否在线、密码是否正确是不同的检查阶段;旧版本更新包也会保留已有证书。
125
+
126
+ 这不是旧 macOS 独有的问题:在 macOS 26.6.2 的 `/usr/bin/openssl`(LibreSSL 3.3.6)上也已复现。Proxy 通过 PATH 查找 `openssl`,只用它生成身份文件;实际 TLS 连接使用 Node 自身的 OpenSSL。交互终端和 launchd 服务的 PATH 可能不同,因此终端安装新版 OpenSSL 不代表服务会使用它。升级 macOS 或替换命令不会自动转换已有身份文件。
127
+
128
+ 包含此修复的版本会在启动或运行 `octopus-proxy identity` 时检查身份编码,把旧的显式参数转换为 `named_curve` 并重新签发证书,保留原来的私钥数值和公钥。正常的命名曲线证书不会重写,已有指纹保持不变;TLS 1.3、主机密码认证和指纹校验保持原有要求。转换旧证书后,编码变化会使其 SHA-256 指纹变化;首次连接应与主机上的 `octopus-proxy identity` 输出核对。使用自定义配置时带上原来的 `--config <path>`。
129
+
130
+ 命名曲线编码遵循 [RFC 5480 §2.1.1](https://www.rfc-editor.org/rfc/rfc5480#section-2.1.1),继续使用 P-256 和 SHA-256。兼容处理不启用旧 TLS 协议、不放宽客户端证书解析,也不自动覆盖已固定的主机指纹。macOS 26 及后续版本保持这些安全要求;当前实机验证环境为 macOS 26.6.2,后续系统版本仍需实际回归验证。
131
+
105
132
  ## 发布
106
133
 
107
134
  在 `apps/octopus-proxy` 目录执行 `npm run publish`:先递增 `package.json` 的补丁版本,再编译,最后发布。即使编译或发布失败,版本号也会保留递增结果,重试时继续递增。版本更新不会自动创建 Git 提交或标签。先登录有权发布 `@skylandnpm` 包的 npm 账号:
@@ -7600,8 +7600,8 @@ var proxyVersion = JSON.parse(readFileSync(new URL("../package.json", import.met
7600
7600
 
7601
7601
  // src/host-identity.ts
7602
7602
  import { execFile } from "node:child_process";
7603
- import { createHash, randomUUID, X509Certificate } from "node:crypto";
7604
- import { chmod, mkdir, open, readFile as readFile2, rename, stat as stat2, unlink } from "node:fs/promises";
7603
+ import { createHash, createPrivateKey, createPublicKey, randomUUID, X509Certificate } from "node:crypto";
7604
+ import { chmod, mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "node:fs/promises";
7605
7605
  import { join as join2 } from "node:path";
7606
7606
  import { promisify } from "node:util";
7607
7607
  var run = promisify(execFile);
@@ -7613,20 +7613,43 @@ async function ensureHostIdentity(stateDirectory) {
7613
7613
  const certPath = join2(stateDirectory, "host-identity-cert.pem");
7614
7614
  const keyExists = await stat2(keyPath).then(() => true, () => false);
7615
7615
  const certExists = await stat2(certPath).then(() => true, () => false);
7616
+ for (const path of [keyPath, certPath]) {
7617
+ const info = await stat2(path).catch((error) => {
7618
+ if (error.code === "ENOENT") return void 0;
7619
+ throw error;
7620
+ });
7621
+ if (info && (info.mode & 63) !== 0) throw new Error("Host identity permissions must be 0600");
7622
+ }
7623
+ if (certExists && !keyExists) throw new Error("Host identity private key is missing");
7616
7624
  const suffix = randomUUID();
7617
7625
  const temporaryKey = join2(stateDirectory, `.host-key-${suffix}`);
7618
7626
  const temporaryCert = join2(stateDirectory, `.host-cert-${suffix}`);
7619
7627
  try {
7620
7628
  if (!keyExists) {
7621
7629
  await (await open(temporaryKey, "wx", 384)).close();
7622
- await run("openssl", ["genpkey", "-algorithm", "EC", "-pkeyopt", "ec_paramgen_curve:P-256", "-out", temporaryKey], { timeout: 15e3 });
7630
+ await run("openssl", ["genpkey", "-algorithm", "EC", "-pkeyopt", "ec_paramgen_curve:P-256", "-pkeyopt", "ec_param_enc:named_curve", "-out", temporaryKey], { timeout: 15e3 });
7623
7631
  await chmod(temporaryKey, 384);
7624
7632
  await rename(temporaryKey, keyPath);
7625
7633
  }
7626
- if (!certExists) {
7634
+ const privateKey = createPrivateKey(await readFile2(keyPath));
7635
+ const existingCertificate = certExists ? new X509Certificate(await readFile2(certPath)) : void 0;
7636
+ if (existingCertificate && !existingCertificate.checkPrivateKey(privateKey)) throw new Error("Host identity certificate does not match private key");
7637
+ const normalizedKey = privateKey.asymmetricKeyType === "ec" ? createPrivateKey({ key: privateKey.export({ format: "jwk" }), format: "jwk" }) : privateKey;
7638
+ const publicKey = createPublicKey(normalizedKey).export({ type: "spki", format: "der" });
7639
+ const normalizeKey = !createPublicKey(privateKey).export({ type: "spki", format: "der" }).equals(publicKey);
7640
+ const renewCertificate = !existingCertificate || !existingCertificate.publicKey.export({ type: "spki", format: "der" }).equals(publicKey);
7641
+ if (normalizeKey || renewCertificate) {
7642
+ await writeFile(temporaryKey, normalizedKey.export({ type: "pkcs8", format: "pem" }), { mode: 384, flag: "wx" });
7643
+ }
7644
+ if (renewCertificate) {
7627
7645
  await (await open(temporaryCert, "wx", 384)).close();
7628
- await run("openssl", ["req", "-new", "-x509", "-sha256", "-days", "3650", "-key", keyPath, "-out", temporaryCert, "-subj", "/CN=octopus-proxy"], { timeout: 15e3 });
7646
+ await run("openssl", ["req", "-new", "-x509", "-sha256", "-days", "3650", "-key", temporaryKey, "-out", temporaryCert, "-subj", "/CN=octopus-proxy"], { timeout: 15e3 });
7647
+ const certificate = new X509Certificate(await readFile2(temporaryCert));
7648
+ if (!certificate.checkPrivateKey(normalizedKey) || !certificate.publicKey.export({ type: "spki", format: "der" }).equals(publicKey)) throw new Error("Host identity certificate normalization failed");
7629
7649
  await chmod(temporaryCert, 384);
7650
+ }
7651
+ if (normalizeKey || renewCertificate) await rename(temporaryKey, keyPath);
7652
+ if (renewCertificate) {
7630
7653
  await rename(temporaryCert, certPath);
7631
7654
  }
7632
7655
  } finally {
@@ -7883,13 +7906,13 @@ import { basename as basename2, dirname as dirname3, join as join3 } from "node:
7883
7906
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
7884
7907
 
7885
7908
  // src/storage.ts
7886
- import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile } from "node:fs/promises";
7909
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
7887
7910
  import { dirname as dirname2 } from "node:path";
7888
7911
  import { randomUUID as randomUUID3 } from "node:crypto";
7889
7912
  async function atomicJson(path, value) {
7890
7913
  await mkdir3(dirname2(path), { recursive: true, mode: 448 });
7891
7914
  const temp = `${path}.${randomUUID3()}.tmp`;
7892
- await writeFile(temp, JSON.stringify(value), { mode: 384 });
7915
+ await writeFile2(temp, JSON.stringify(value), { mode: 384 });
7893
7916
  await rename2(temp, path);
7894
7917
  }
7895
7918
  async function readJson(path) {
@@ -8344,7 +8367,7 @@ function pageHistory(input, payload) {
8344
8367
  // src/artifacts.ts
8345
8368
  import { createHash as createHash5 } from "node:crypto";
8346
8369
  import { join as join5 } from "node:path";
8347
- import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
8370
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
8348
8371
  var Artifacts = class {
8349
8372
  constructor(workspace, directory, emit) {
8350
8373
  this.workspace = workspace;
@@ -8400,7 +8423,7 @@ var Artifacts = class {
8400
8423
  if (previous?.hash !== hash || previous.deleted) {
8401
8424
  const dir = join5(this.directory, project, "blobs");
8402
8425
  await mkdir4(dir, { recursive: true, mode: 448 });
8403
- await writeFile2(join5(dir, hash), data, { mode: 384 });
8426
+ await writeFile3(join5(dir, hash), data, { mode: 384 });
8404
8427
  record2.revisions.push({ hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8405
8428
  changed = true;
8406
8429
  }
@@ -8453,7 +8476,7 @@ var Artifacts = class {
8453
8476
  // src/secure-chat.ts
8454
8477
  import WebSocket from "ws";
8455
8478
  import { Duplex } from "node:stream";
8456
- import { createPublicKey, randomUUID as randomUUID7, verify } from "node:crypto";
8479
+ import { createPublicKey as createPublicKey2, randomUUID as randomUUID7, verify } from "node:crypto";
8457
8480
  import { userInfo as userInfo2 } from "node:os";
8458
8481
  import { TLSSocket, createSecureContext } from "node:tls";
8459
8482
 
@@ -8741,7 +8764,7 @@ var SecureChat = class {
8741
8764
  if (key.length !== 32 || signature.length !== 64) throw new Error("Invalid device proof");
8742
8765
  const exporter = session.tls.exportKeyingMaterial(32, "EXPORTER-octopus-device-v1", Buffer.from(this.config.botId)).toString("base64");
8743
8766
  const proof = Buffer.from(`${this.config.botId}\0${id}\0${exporter}`);
8744
- if (!verify(null, proof, createPublicKey({ key: Buffer.concat([ED25519_SPKI, key]), format: "der", type: "spki" }), signature)) throw new Error("Invalid device proof");
8767
+ if (!verify(null, proof, createPublicKey2({ key: Buffer.concat([ED25519_SPKI, key]), format: "der", type: "spki" }), signature)) throw new Error("Invalid device proof");
8745
8768
  const valid = await this.authenticate(this.config.stateDirectory, this.serviceAccount, String(message.password ?? ""));
8746
8769
  if (!valid) throw new Error("Host authentication failed");
8747
8770
  if (this.sessions.get(id) !== session) return;
@@ -10519,7 +10542,7 @@ function applyEdits(text, edits) {
10519
10542
  }
10520
10543
 
10521
10544
  // src/project-config.ts
10522
- import { readFile as readFile5, writeFile as writeFile3, unlink as unlink6 } from "node:fs/promises";
10545
+ import { readFile as readFile5, writeFile as writeFile4, unlink as unlink6 } from "node:fs/promises";
10523
10546
  import { createHash as createHash7, randomUUID as randomUUID10 } from "node:crypto";
10524
10547
  var ProjectConfig = class {
10525
10548
  constructor(workspace, request) {
@@ -10573,7 +10596,7 @@ var ProjectConfig = class {
10573
10596
  else {
10574
10597
  const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID10()}.tmp`, true);
10575
10598
  try {
10576
- await writeFile3(temporary, updated, { mode: 384, flag: "wx" });
10599
+ await writeFile4(temporary, updated, { mode: 384, flag: "wx" });
10577
10600
  await this.workspace.publish(project, temporary, name);
10578
10601
  } finally {
10579
10602
  await unlink6(temporary).catch(() => {
@@ -11034,7 +11057,7 @@ function redact(value) {
11034
11057
  // src/runtime.ts
11035
11058
  import { spawn as spawn2 } from "node:child_process";
11036
11059
  import { createHash as createHash8 } from "node:crypto";
11037
- import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as writeFile4 } from "node:fs/promises";
11060
+ import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as writeFile5 } from "node:fs/promises";
11038
11061
  import { createServer } from "node:net";
11039
11062
  import { join as join7 } from "node:path";
11040
11063
  var Runtime = class {
@@ -11138,7 +11161,7 @@ var Runtime = class {
11138
11161
  throw new Error(`\u65E0\u6CD5\u6267\u884C OpenCode \u547D\u4EE4 ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`);
11139
11162
  }
11140
11163
  try {
11141
- await writeFile4(this.endpointStatePath, JSON.stringify({
11164
+ await writeFile5(this.endpointStatePath, JSON.stringify({
11142
11165
  configuredBaseUrl: this.config.tool.baseUrl,
11143
11166
  baseUrl: url.origin,
11144
11167
  passwordHash: this.passwordHash,
@@ -13559,10 +13582,19 @@ import { join as join12 } from "node:path";
13559
13582
  import { homedir as homedir2, platform as platform4 } from "node:os";
13560
13583
  var run4 = promisify7(execFile7);
13561
13584
  var VERSION_MANAGED_SEGMENTS = /* @__PURE__ */ new Set([".nvm", "nvm", ".volta", "volta", ".fnm", "fnm", ".asdf", "asdf", ".mise", "mise"]);
13585
+ var MIN_NODE_MAJOR = 22;
13586
+ function parseNodeVersion(version) {
13587
+ const match = /^v(\d+)\.\d+\.\d+(?:-[\w.-]+)?$/.exec(version);
13588
+ if (!match) throw new Error(`\u65E0\u6CD5\u8BC6\u522B Node.js \u7248\u672C\uFF1A${version || "\u7A7A\u8F93\u51FA"}`);
13589
+ return { version, supported: Number(match[1]) >= MIN_NODE_MAJOR };
13590
+ }
13591
+ async function probeNodeVersion(path) {
13592
+ const { stdout } = await run4(path, ["--version"], { timeout: 5e3 });
13593
+ return parseNodeVersion(stdout.trim());
13594
+ }
13562
13595
  var execProbe = async (path) => {
13563
13596
  try {
13564
- await run4(path, ["--version"], { timeout: 5e3 });
13565
- return true;
13597
+ return (await probeNodeVersion(path)).supported;
13566
13598
  } catch {
13567
13599
  return false;
13568
13600
  }
@@ -13576,7 +13608,7 @@ function nodeCandidates() {
13576
13608
  async function resolveNodePath(options = {}) {
13577
13609
  const probe = options.probe ?? execProbe;
13578
13610
  if (options.pinned) {
13579
- if (!await probe(options.pinned)) throw new Error(`--node-path ${options.pinned} is not a working node executable`);
13611
+ if (!await probe(options.pinned)) throw new Error(`--node-path ${options.pinned} is not a working Node.js >= ${MIN_NODE_MAJOR} executable; check ${options.pinned} --version`);
13580
13612
  return { nodePath: options.pinned, source: "pinned" };
13581
13613
  }
13582
13614
  let exec2 = options.execPath ?? process.execPath;
@@ -13894,6 +13926,9 @@ export {
13894
13926
  checkTerminalSupport,
13895
13927
  PlatformDesktopSession,
13896
13928
  RemoteManager,
13929
+ MIN_NODE_MAJOR,
13930
+ parseNodeVersion,
13931
+ probeNodeVersion,
13897
13932
  isVersionManagedPath,
13898
13933
  resolveNodePath,
13899
13934
  toolSearchDirs,
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigError,
4
+ MIN_NODE_MAJOR,
4
5
  PlatformDesktopSession,
5
6
  RemoteManager,
6
7
  SERVICE_LABEL,
@@ -23,6 +24,8 @@ import {
23
24
  launchdLogPath,
24
25
  launchdPlistPath,
25
26
  loadConfig,
27
+ parseNodeVersion,
28
+ probeNodeVersion,
26
29
  proxyVersion,
27
30
  resolveCommandOnPath,
28
31
  resolveConfigPath,
@@ -31,7 +34,7 @@ import {
31
34
  supportedPlatform,
32
35
  systemdUnitPath,
33
36
  toolSearchDirs
34
- } from "./chunk-3KIA6HF6.js";
37
+ } from "./chunk-NJNANA6A.js";
35
38
  import {
36
39
  DESKTOP_PERMISSION_GUIDANCE,
37
40
  PlatformDesktop,
@@ -45,7 +48,7 @@ import { access as access4 } from "node:fs/promises";
45
48
 
46
49
  // src/doctor.ts
47
50
  import { access as access2, chmod, constants, mkdir as mkdir3, stat } from "node:fs/promises";
48
- import { isAbsolute as isAbsolute2, join } from "node:path";
51
+ import { isAbsolute as isAbsolute3, join } from "node:path";
49
52
  import { execFile as execFile3 } from "node:child_process";
50
53
  import { promisify as promisify3 } from "node:util";
51
54
 
@@ -233,7 +236,7 @@ async function readServiceFile() {
233
236
  async function buildServiceDefinition(opts) {
234
237
  const config = await loadConfig(opts.configPath);
235
238
  const resolution = await resolveNodePath({ pinned: opts.nodePath });
236
- if (!resolution.nodePath) throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 node \u53EF\u6267\u884C\u6587\u4EF6\uFF1B\u8BF7\u7528\u7CFB\u7EDF\u5305\u7BA1\u7406\u5668\u5B89\u88C5 node\uFF0C\u6216\u901A\u8FC7 --node-path \u6307\u5B9A\u7EDD\u5BF9\u8DEF\u5F84");
239
+ if (!resolution.nodePath) throw new Error(`\u672A\u627E\u5230 Node.js >= ${MIN_NODE_MAJOR} \u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF1B\u8BF7\u5B89\u88C5\u53D7\u652F\u6301\u7684 Node.js\uFF0C\u6216\u901A\u8FC7 --node-path \u6307\u5B9A\u7EDD\u5BF9\u8DEF\u5F84`);
237
240
  const opencodeDir = isAbsolute(config.tool.command) ? [dirname3(config.tool.command)] : [];
238
241
  const pathEnv = [dirname3(resolution.nodePath), ...opencodeDir, "/usr/local/bin", "/usr/bin", "/bin"].filter((dir, index, all) => all.indexOf(dir) === index).join(":");
239
242
  return { def: { nodePath: resolution.nodePath, entryPath: daemonEntryPath(), configPath: resolve(opts.configPath), pathEnv }, warning: resolution.warning };
@@ -291,12 +294,47 @@ function logLocationHint() {
291
294
  return isMacOS() ? launchdLogPath() : "journalctl --user -u octopus-proxy";
292
295
  }
293
296
 
297
+ // src/service-definition.ts
298
+ import { isAbsolute as isAbsolute2 } from "node:path";
299
+ function serviceNodePath(text, macOS) {
300
+ let path;
301
+ if (macOS) {
302
+ const match = /<key>\s*ProgramArguments\s*<\/key>\s*<array>\s*<string>([\s\S]*?)<\/string>/.exec(text);
303
+ const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
304
+ path = match?.[1]?.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => entities[entity]);
305
+ } else {
306
+ const match = /^[ \t]*ExecStart[ \t]*=[ \t]*(?:"((?:\\.|[^"\\])*)"|([^\s"]+))/m.exec(text);
307
+ path = (match?.[1] ?? match?.[2])?.replace(/\\(["\\])/g, "$1").replaceAll("%%", "%");
308
+ }
309
+ return path && isAbsolute2(path) ? path : null;
310
+ }
311
+
294
312
  // src/doctor.ts
295
313
  import { randomUUID } from "node:crypto";
296
314
  var run3 = promisify3(execFile3);
315
+ function nodeVersionCheck(name, path, info) {
316
+ return {
317
+ name,
318
+ status: info.supported ? "ok" : "fail",
319
+ detail: `${path}\uFF08${info.version}\uFF09\uFF1B\u8981\u6C42 Node.js >= ${MIN_NODE_MAJOR}${info.supported ? "" : "\uFF1B\u8BF7\u5347\u7EA7 Node.js\uFF0C\u5E76\u8FD0\u884C octopus-proxy install --force --node-path <Node\u7EDD\u5BF9\u8DEF\u5F84> \u91CD\u5199\u670D\u52A1\u5B9A\u4E49"}`
320
+ };
321
+ }
322
+ async function checkServiceNodeVersion() {
323
+ const name = "\u670D\u52A1 Node.js \u7248\u672C";
324
+ const existing = await readServiceFile();
325
+ if (existing === null) return { name, status: "warn", detail: "\u5C1A\u672A\u5B89\u88C5\u670D\u52A1\uFF0C\u65E0\u6CD5\u68C0\u67E5\u670D\u52A1\u4F7F\u7528\u7684 Node.js \u7248\u672C" };
326
+ const path = serviceNodePath(existing, isMacOS());
327
+ if (!path) return { name, status: "fail", detail: "\u65E0\u6CD5\u4ECE\u670D\u52A1\u5B9A\u4E49\u8BFB\u53D6 Node \u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u8FD0\u884C octopus-proxy install --force \u91CD\u5199\u670D\u52A1\u5B9A\u4E49" };
328
+ try {
329
+ return nodeVersionCheck(name, path, await probeNodeVersion(path));
330
+ } catch (error) {
331
+ return { name, status: "fail", detail: `${path} --version \u68C0\u67E5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\uFF1B\u8BF7\u786E\u8BA4 Node \u8DEF\u5F84\u6709\u6548\u5E76\u8FD0\u884C octopus-proxy install --force \u91CD\u5199\u670D\u52A1\u5B9A\u4E49` };
332
+ }
333
+ }
297
334
  async function runDoctor(opts) {
298
335
  const results = [];
299
336
  const record = (name, status, detail) => results.push({ name, status, detail });
337
+ results.push(nodeVersionCheck("\u5F53\u524D Node.js \u7248\u672C", process.execPath, parseNodeVersion(process.version)));
300
338
  results.push(await checkTerminalSupport(opts.fix));
301
339
  let config;
302
340
  try {
@@ -335,7 +373,7 @@ async function runDoctor(opts) {
335
373
  const descriptor = describeTool(config.tool.kind);
336
374
  const checkName = `${descriptor.label} \u547D\u4EE4`;
337
375
  const command2 = config.tool.command;
338
- if (!isAbsolute2(command2)) {
376
+ if (!isAbsolute3(command2)) {
339
377
  record(checkName, "warn", `\u5F53\u524D\u4E3A\u300C${command2}\u300D\uFF0C\u4F9D\u8D56 PATH \u67E5\u627E\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u6539\u7528\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u91CD\u65B0\u8FD0\u884C onboard \u6216\u624B\u5DE5\u4FEE\u6539\u914D\u7F6E\uFF09`);
340
378
  } else if (!await access2(command2, constants.X_OK).then(() => true, () => false)) {
341
379
  record(checkName, "fail", `${command2} \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C`);
@@ -399,6 +437,7 @@ async function runDoctor(opts) {
399
437
  record("\u670D\u52A1\u5B9A\u4E49", "fail", error instanceof Error ? error.message : String(error));
400
438
  }
401
439
  }
440
+ results.push(await checkServiceNodeVersion());
402
441
  const state = await serviceStatus();
403
442
  if (state.active) record("\u670D\u52A1\u72B6\u6001", "ok", `\u8FD0\u884C\u4E2D${state.pid ? `\uFF08pid ${state.pid}\uFF09` : ""}`);
404
443
  else if (!state.installed) record("\u670D\u52A1\u72B6\u6001", "warn", "\u5C1A\u672A\u5B89\u88C5\u670D\u52A1");
@@ -420,7 +459,7 @@ import { stdin, stdout } from "node:process";
420
459
  import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat2 } from "node:fs/promises";
421
460
  import { execFile as execFile4 } from "node:child_process";
422
461
  import { platform } from "node:os";
423
- import { isAbsolute as isAbsolute3, join as join2 } from "node:path";
462
+ import { isAbsolute as isAbsolute4, join as join2 } from "node:path";
424
463
  import { promisify as promisify4 } from "node:util";
425
464
  var DEFAULT_HUB_URL = "https://octopus.wuyehao.com";
426
465
  async function ask(rl, question, options = {}) {
@@ -459,7 +498,7 @@ var MACOS_TOOL_EXAMPLES = {
459
498
  };
460
499
  async function validateToolCommand(value, kind) {
461
500
  const command2 = expandHome(value);
462
- if (!isAbsolute3(command2)) return "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84";
501
+ if (!isAbsolute4(command2)) return "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84";
463
502
  try {
464
503
  if (!(await stat2(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
465
504
  await access3(command2, constants2.X_OK);
@@ -492,7 +531,7 @@ async function runOnboard(opts) {
492
531
  hubUrl = await ask(rl, `Hub \u5730\u5740\uFF08${hubUrl} , \u56DE\u8F66\u76F4\u63A5\u786E\u8BA4\uFF09`, { default: hubUrl, showDefault: false, validate: validUrl });
493
532
  botId = await ask(rl, "\u673A\u5668\u4EBA ID\uFF08\u5728 Claw \u6216 Taskhub \u4E2D\u521B\u5EFA\u5E76\u590D\u5236\uFF09", { default: botId || void 0, validate: validBotId });
494
533
  secretKey = await ask(rl, "\u673A\u5668\u4EBA Secret\uFF08\u5728 Claw \u6216 Taskhub \u521B\u5EFA\uFF1B\u8F93\u5165\u4F1A\u56DE\u663E\u5728\u7EC8\u7AEF\uFF09", { default: secretKey || void 0, defaultDisplay: existing?.secretKey ? "\u5DF2\u914D\u7F6E\uFF0C\u56DE\u8F66\u4FDD\u7559" : "\u5DF2\u63D0\u4F9B\uFF0C\u56DE\u8F66\u4F7F\u7528", validate: (value) => value.length >= 20 ? void 0 : "\u8BF7\u8F93\u5165\u5B8C\u6574\u7684\u673A\u5668\u4EBA Secret" });
495
- stateDirectory = expandHome(await ask(rl, "\u72B6\u6001\u76EE\u5F55", { default: stateDirectory, validate: (value) => isAbsolute3(expandHome(value)) ? void 0 : "\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" }));
534
+ stateDirectory = expandHome(await ask(rl, "\u72B6\u6001\u76EE\u5F55", { default: stateDirectory, validate: (value) => isAbsolute4(expandHome(value)) ? void 0 : "\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" }));
496
535
  const previousToolKind = toolKind;
497
536
  toolKind = await ask(rl, `\u7F16\u7801\u5DE5\u5177\uFF08${TOOL_KINDS.join("/")}\uFF09`, { default: toolKind, validate: (value) => TOOL_KINDS.includes(value) ? void 0 : `\u8F93\u5165 ${TOOL_KINDS.join(" \u6216 ")}` });
498
537
  if (toolKind !== previousToolKind) toolCommand = toolKind === existing?.tool.kind ? existing.tool.command : "";
@@ -514,12 +553,13 @@ async function runOnboard(opts) {
514
553
  toolCommand = expandHome(await ask(rl, `${descriptor.label} \u547D\u4EE4\uFF08\u56DE\u8F66\u4F1A\u9A8C\u8BC1\u8DEF\u5F84\uFF09`, { default: toolCommand || found || example, validate: (value) => validateToolCommand(value, toolKind) }));
515
554
  console.log(` \u5DF2\u786E\u8BA4 ${descriptor.label} \u547D\u4EE4\u53EF\u6267\u884C\uFF1A${toolCommand}`);
516
555
  }
556
+ console.log("\n\u57FA\u7840\u8BBE\u7F6E\u5DF2\u5B8C\u6210\uFF0C\u81F3\u5C11\u9700\u8981\u914D\u7F6E\u4E00\u4E2A\u5DE5\u4F5C\u9879\u76EE\u3002");
517
557
  if (projects.length) console.log("\n\u5DF2\u6709\u6388\u6743\u9879\u76EE\uFF08\u76F4\u63A5\u56DE\u8F66\u4FDD\u7559\u5404\u9879\uFF09\uFF1A");
518
558
  for (const [index, project] of projects.entries()) {
519
559
  project.name = await ask(rl, " \u9879\u76EE\u540D\u79F0", { default: project.name });
520
560
  for (; ; ) {
521
561
  project.directory = expandHome(await ask(rl, ` \u300C${project.name}\u300D\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09`, { default: project.directory }));
522
- if (!isAbsolute3(project.directory)) {
562
+ if (!isAbsolute4(project.directory)) {
523
563
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
524
564
  continue;
525
565
  }
@@ -531,14 +571,24 @@ async function runOnboard(opts) {
531
571
  }
532
572
  project.id = await ask(rl, " \u9879\u76EE ID", { default: project.id ?? slug(project.name), validate: (value) => validateProjectId(value, projects.slice(0, index)) });
533
573
  }
534
- console.log("\n\u6DFB\u52A0\u6388\u6743\u9879\u76EE\uFF08\u540D\u79F0\u7559\u7A7A\u7ED3\u675F\uFF09\uFF1A");
535
574
  for (; ; ) {
536
- const name = (await rl.question(" \u9879\u76EE\u540D\u79F0: ")).trim();
537
- if (!name) break;
575
+ if (projects.length) {
576
+ console.log(projects.length === 1 ? "\n\u60A8\u5DF2\u5B8C\u6210\u7B2C\u4E00\u4E2A\u9879\u76EE\u914D\u7F6E\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u6DFB\u52A0\u66F4\u591A\u9879\u76EE\uFF0C\u6216\u8005\u5148\u5B8C\u6210\u9879\u76EE\u914D\u7F6E\u3002" : `
577
+ \u60A8\u5DF2\u5B8C\u6210 ${projects.length} \u4E2A\u9879\u76EE\u914D\u7F6E\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u6DFB\u52A0\u66F4\u591A\u9879\u76EE\uFF0C\u6216\u8005\u5148\u5B8C\u6210\u9879\u76EE\u914D\u7F6E\u3002`);
578
+ const addMore = await ask(rl, " \u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\u66F4\u591A\u9879\u76EE\uFF1F(y/N\uFF0C\u56DE\u8F66\u5B8C\u6210\u9879\u76EE\u914D\u7F6E)", {
579
+ default: "n",
580
+ showDefault: false,
581
+ validate: (value) => /^(y|yes|n|no)$/i.test(value) ? void 0 : "\u8BF7\u8F93\u5165 y \u6216 n"
582
+ });
583
+ if (!/^(y|yes)$/i.test(addMore)) break;
584
+ }
585
+ console.log("\n\u6DFB\u52A0\u5DE5\u4F5C\u9879\u76EE\uFF1A");
586
+ const id = await ask(rl, " \u9879\u76EE ID", { validate: (value) => validateProjectId(value, projects) });
587
+ const name = await ask(rl, " \u9879\u76EE\u540D\u79F0", { default: id });
538
588
  let directory = "";
539
589
  for (; ; ) {
540
590
  directory = expandHome(await ask(rl, ` \u300C${name}\u300D\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09`));
541
- if (!isAbsolute3(directory)) {
591
+ if (!isAbsolute4(directory)) {
542
592
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
543
593
  continue;
544
594
  }
@@ -548,14 +598,7 @@ async function runOnboard(opts) {
548
598
  }
549
599
  break;
550
600
  }
551
- const id = await ask(rl, " \u9879\u76EE ID", { default: slug(name), validate: (value) => validateProjectId(value, projects) });
552
601
  projects.push({ id, name, directory });
553
- const addMore = await ask(rl, " \u7EE7\u7EED\u6DFB\u52A0\u66F4\u591A\u6388\u6743\u9879\u76EE\uFF1F(y/N)", {
554
- default: "n",
555
- showDefault: false,
556
- validate: (value) => /^(y|yes|n|no)$/i.test(value) ? void 0 : "\u8BF7\u8F93\u5165 y \u6216 n"
557
- });
558
- if (!/^(y|yes)$/i.test(addMore)) break;
559
602
  }
560
603
  rl.close();
561
604
  if (!projects.length) throw new Error("\u81F3\u5C11\u9700\u8981\u5F55\u5165\u4E00\u4E2A\u6388\u6743\u9879\u76EE");
@@ -577,10 +620,10 @@ async function runOnboard(opts) {
577
620
  if (seenIds.has(project.id)) throw new Error(`\u9879\u76EE ID\u300C${project.id}\u300D\u91CD\u590D\uFF0C\u8BF7\u8C03\u6574\u9879\u76EE\u540D\u79F0\u6216\u624B\u5DE5\u6307\u5B9A\u4E0D\u540C ID`);
578
621
  seenIds.add(project.id);
579
622
  }
580
- if (projects.some((project) => !isAbsolute3(project.directory))) throw new Error("--project \u7684\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
623
+ if (projects.some((project) => !isAbsolute4(project.directory))) throw new Error("--project \u7684\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
581
624
  const descriptor = describeTool(toolKind);
582
625
  toolCommand = toolCommand ? expandHome(toolCommand) : await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind)) ?? descriptor.defaultCommand;
583
- if (!isAbsolute3(toolCommand)) console.log(`\u63D0\u793A\uFF1A\u672A\u5728 PATH \u4E2D\u63A2\u6D4B\u5230 ${descriptor.label}\uFF0C\u914D\u7F6E\u4FDD\u7559\u547D\u4EE4\u540D\u300C${toolCommand}\u300D\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u53EF\u7528 --tool-command \u6307\u5B9A\uFF09`);
626
+ if (!isAbsolute4(toolCommand)) console.log(`\u63D0\u793A\uFF1A\u672A\u5728 PATH \u4E2D\u63A2\u6D4B\u5230 ${descriptor.label}\uFF0C\u914D\u7F6E\u4FDD\u7559\u547D\u4EE4\u540D\u300C${toolCommand}\u300D\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u53EF\u7528 --tool-command \u6307\u5B9A\uFF09`);
584
627
  }
585
628
  if (configExists) {
586
629
  if (opts.interactive) {
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-3KIA6HF6.js";
5
+ } from "./chunk-NJNANA6A.js";
6
6
  import "./chunk-XIJKEY4L.js";
7
7
 
8
8
  // src/main.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {