cabletidy 0.4.0-rc.1 → 0.4.0-rc.3
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 +26 -6
- package/bin/cabletidy.mjs +15 -6
- package/bin/update.mjs +192 -0
- package/native/darwin-arm64/build.json +2 -2
- package/native/darwin-arm64/cabletidy +0 -0
- package/native/darwin-x64/build.json +2 -2
- package/native/darwin-x64/cabletidy +0 -0
- package/native/linux-arm64/build.json +2 -2
- package/native/linux-arm64/cabletidy +0 -0
- package/native/linux-x64/build.json +2 -2
- package/native/linux-x64/cabletidy +0 -0
- package/native/win32-x64/build.json +2 -2
- package/native/win32-x64/cabletidy.exe +0 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,17 +16,32 @@ CableTidy 的核心不是“提供一个统一 OpenAI API”,而是把复杂
|
|
|
16
16
|
|
|
17
17
|
本地管理台的“审计记录”按会话组织。会话列表显示输入摘要、请求数量、最高风险与检查状态;打开会话后,通过请求概览、按顺序排列的轨迹和右侧详情回看过程。页面固定为“仅记录”模式。仅审计经过代理的 Agent Responses / Messages 请求以及流式与普通响应;CableTidy 自身的配置提交、配置启停、客户端配置应用和恢复不在审计范围内。检测结果不触发通知、阻断或人工授权,也不会额外调用模型。
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
19
|
+
- 风险分为敏感数据与凭据、破坏性操作、权限与安全配置变更、外部代码执行、疑似指令操纵;详情分别显示严重程度、置信度、判断依据和检查完整性。敏感数据除凭据外还标识服务器地址:覆盖 IPv4、IPv6 及整数、十六进制、八进制和百分号编码写法,公网地址泄露后可被直接访问,严重程度高于内网地址;点分版本号、MAC 地址、时间戳和 `std::collections` 这类路径不计为地址。凭据判定按格式前缀、长度和熵分层,自然语言词与 `ssh://git@host` 这类公开写法不再误报。
|
|
20
|
+
- 支持“仅看有风险”及时间、配置、最高严重程度、分类、证据阶段等筛选。命中任一请求即可找到其会话,打开后保留全部已留存上下文;会话数、请求数和风险发现数分开统计。轨迹可上下连续滚动,接近底部时按需追加请求;支持已加载摘要搜索、风险筛选;概览默认按请求数量分配宽度,选中“耗时占比”按耗时分配,再次点击恢复默认,请求顺序始终不变;每条请求同时展示客户端模型与上游模型,支持按两种模型名搜索;详情依次分为概览、风险、请求内容、响应内容和原始内容五个页签。页面手动刷新,读取本身不增加审计记录。
|
|
21
21
|
- 根据客户端提供的会话标识归组,并按配置入口与协议隔离。支持 Codex 常见会话头、显式 `metadata.session_id` 和 Claude Code 的会话元数据;这些标识属于客户端报告。没有标识的 Agent 请求与历史 Agent 记录单独展示,不根据时间、相同内容或用户账号猜测会话。
|
|
22
22
|
- 区分本轮工具调用提议、历史调用、客户端报告的工具结果和文本内容。工具的真实执行状态为未知;未命中规则也不表示已证明安全。
|
|
23
|
+
- Codex 请求概览按顺序解析系统提示词、开发者指令、全部用户输入、历史模型消息、工具调用及结果、推理与历史引用;统计内容组成,每项完整展示、默认展开,支持分页和标题行右侧的原文定位,可用工具定义单独折叠。已有保留正文也可生成概览,缺失和非文本内容明确标示。结构调研与展示边界见 [Codex 请求内容概览](docs/codex-request-overview.md)。
|
|
24
|
+
- Claude Code 请求概览按 Messages 内容块解析系统提示词、用户与历史模型内容、思考、工具调用及结果;每项完整展示,`user` 消息内的工具结果单独计数,支持跨页关联、错误与缓存标记、多模态类型和标题行右侧的精确原文定位。结构调研与展示边界见 [Claude Code 请求内容概览](docs/claude-request-overview.md)。
|
|
25
|
+
- “响应内容”页签完整展示本轮模型文本、推理 / 思考、工具调用与服务端结果;支持分类计数、独立分页、完整结构和原文定位。Codex 合并 Responses 增量并去重最终输出,Claude Code 分开拼接文本、思考、签名和工具 JSON;缺失与中断明确提示。会话风险跳转到请求内容或响应内容中的对应位置,自动加载所需分页;请求内容、响应内容和原始内容中的风险位置在普通浏览时也保持高亮。调研及边界见 [Codex 响应内容概览](docs/codex-response-overview.md) 和 [Claude Code 响应内容概览](docs/claude-response-overview.md)。
|
|
23
26
|
- 审计存入数据目录中的 `audit.sqlite3`,默认保留 30 天,容量预算 128 MiB,接近容量时可能提前清理较旧记录。页面显示实际保留范围、写入状态和记录缺口。
|
|
24
27
|
- 保留网关观察到的请求与响应正文,包括对话、模型输出、工具参数和客户端报告的工具结果;凭据、认证头、Cookie、私钥等保留原文并记录检测位置,详情页自动高亮敏感内容。点击风险可定位对应内容并查看上下文;流式命中保存检测当时的独立快照。普通对话、命令和路径保持可读。
|
|
25
28
|
- 正文不再设单条固定长度上限,网关也不再按 8 MiB 拒绝请求。采用有界内存、临时加密暂存、原文分段落盘和正文按页加载;后台完整扫描保留的可检测内容,不再只检查前 1 MiB。共享内存、临时空间或审计存储预算不足时明确显示进度、失败和缺口;旧记录没有正文时不作补造。
|
|
26
29
|
|
|
27
30
|
检查使用本地规则和有界缓冲。未知工具、动态脚本、未覆盖的推理内容、图片、加密内容以及超限情况会显示覆盖不足。数据库不可用或队列满时代理继续运行,页面显示审计退化状态;正常退出会排空已接收的写入,强制退出可能丢失未提交记录。它是本地可追溯日志,不提供零丢失或防篡改保证。
|
|
28
31
|
|
|
29
|
-
分类调研、规则清单、固定预算和 HTTP 查询接口见 [代理安全 V1 设计与实现](docs/agent-security-v1.md)。基础安全审计自 `0.3.0` 提供;会话轨迹、敏感原文保留与高亮纳入 `0.4.0-rc.1`,发布与验收状态见 [候选版发布说明](docs/releases/0.4.0-rc.
|
|
32
|
+
分类调研、规则清单、固定预算和 HTTP 查询接口见 [代理安全 V1 设计与实现](docs/agent-security-v1.md)。基础安全审计自 `0.3.0` 提供;会话轨迹、敏感原文保留与高亮纳入 `0.4.0-rc.1`,发布与验收状态见 [候选版发布说明](docs/releases/0.4.0-rc.3.md)。
|
|
33
|
+
|
|
34
|
+
## 配置套装导出与导入
|
|
35
|
+
|
|
36
|
+
在管理台的“配置套装”列表点击“导出配置”,勾选需要的套装后下载 JSON 文件。文件包含所选套装的客户端绑定、模型映射与可选参数、路由、上游地址和认证方式;共用的路由与上游只保存一次。
|
|
37
|
+
|
|
38
|
+
“包含认证凭据”复选框默认不勾选:导出不含 API Key、上游密钥、密钥引用或密钥配置状态,上游 URL 中的用户名、密码、查询参数和片段也会移除;如地址需要查询参数,请在导入后重新填写。勾选后导出所选套装的已保存密钥,并保留完整上游 URL,文件包含明文凭据,请妥善保管。无论是否勾选,均不包含客户端文件、应用与恢复记录、审计日志、管理台端口和其他机器设置;仅导出受支持的配置字段,未知扩展字段不携带。默认不含凭据时,若已保存的密钥出现在名称、模型设置或 URL 路径中,导出会拒绝并提示先移除该凭据。
|
|
39
|
+
|
|
40
|
+
点击“导入配置”选择文件,先预览待导入的配置,再点击“确认导入”。遇到同名配置时,每套配置须选择“更新已有配置”或“创建新配置”。更新会保留配置名称和本地入口,替换套装设置;创建新配置会添加 ` (import 2)` 等后缀。路由和上游 ID 冲突会自动调整。当文件包含认证凭据时,预览显示“使用导入配置中的认证凭据”复选框,默认勾选:文件中的密钥和 URL 认证信息会使用到导入配置,更新时优先使用文件密钥。取消勾选会忽略文件密钥并移除文件 URL 中的认证信息。未使用文件密钥时,更新已有配置保留该配置原有密钥,无需重新填写;原配置尚未设置密钥时仍需填写。创建新配置不继承本机已有凭据。使用文件密钥会分配独立引用,不会覆盖其他套装共享的本机密钥。更新后不再使用的旧路由和上游会清理,仍被其他套装使用的共享连接和凭据会保留。导入后按需应用到客户端。导入不自动修改 Codex 或 Claude Code 的本地文件。
|
|
41
|
+
|
|
42
|
+
导入复用配置提交的校验、修订冲突检查和备份流程,文件无效时不会部分写入;预览后若本机配置已变更,需要重新预览。带可选模型设置的 Codex 套装会按本机官方目录校验,纯透传套装不依赖该目录。
|
|
43
|
+
|
|
44
|
+
本地管理接口:`POST /api/v1/config/export` 接收 `{ "bindingIds": ["配置ID"], "includeCredentials": false }` 并返回套装文件;`POST /api/v1/config/import` 接收 `{ "bundle": 文件内容, "preview": true }` 返回预览、`baseRevision` 和 `hasCredentials`,正式导入使用 `{ "bundle": 文件内容, "baseRevision": 预览修订号, "choices": { "同名的源配置ID": "update 或 create" }, "useImportedCredentials": true }`。预览中的每套配置提供 `sourceId` 和 `conflict`,所有同名配置都必须明确选择;`choices` 也可用于预览所选更新方案。含凭据的文件通过顶层 `upstreamSecrets` 保存所选上游 ID 与密钥的对应关系,不包含本机密钥引用。`includeCredentials` 默认 `false`,`useImportedCredentials` 默认 `true`。文件标识为 `cabletidy.configuration-suites`,文件版本为 `1`,内部配置版本为 `2`。
|
|
30
45
|
|
|
31
46
|
## 模型透传与可选设置
|
|
32
47
|
|
|
@@ -49,7 +64,7 @@ Codex 的可选模型设置目前只支持与本机官方 GPT 目录明确对应
|
|
|
49
64
|
|
|
50
65
|
npm 安装方式推荐 Node.js 24,也支持 Node.js 22.13 及以上的 22.x 版本;完整版本约束见 `package.json`。原生程序可独立运行。发行包支持 Linux x64 / arm64、macOS x64 / arm64、Windows x64;源码构建需要稳定版 Rust 和系统 C 编译器 / 链接器。CI 执行源码测试与安装冒烟测试,跨平台结果以对应提交的 CI 为准。Windows 使用原生进程 API 查询进程身份;Codex 目录查询仍需要系统 Windows PowerShell,并允许 `Add-Type` 调用 Windows Job Object API。
|
|
51
66
|
|
|
52
|
-
当前源码版本为 `0.4.0-rc.
|
|
67
|
+
当前源码版本为 `0.4.0-rc.3`,在上一候选版基础上新增完整响应展示、风险内容定位与持续高亮、配置套装导入导出及 `restart` / `update` 命令,并修复凭据误报、标识服务器地址。候选版使用 npm `next`;默认 npm 安装命令仍选择 `latest` 指向的稳定版 `0.3.0`。候选版的发布状态、验收范围与升级步骤见 [0.4.0-rc.3 发布说明](docs/releases/0.4.0-rc.3.md);GitHub Release 与 npm 分别发布。稳定版见 [0.3.0 发布说明](docs/releases/0.3.0.md)。
|
|
53
68
|
|
|
54
69
|
### 从 npm 官方源安装
|
|
55
70
|
|
|
@@ -65,10 +80,10 @@ cabletidy start
|
|
|
65
80
|
npm exec --yes --registry=https://registry.npmjs.org/ --package=cabletidy -- cabletidy start
|
|
66
81
|
```
|
|
67
82
|
|
|
68
|
-
试用 `0.4.0` 候选版时,先按[候选版升级说明](docs/releases/0.4.0-rc.
|
|
83
|
+
试用 `0.4.0` 候选版时,先按[候选版升级说明](docs/releases/0.4.0-rc.3.md)停止服务并备份数据,再安装指定版本:
|
|
69
84
|
|
|
70
85
|
```bash
|
|
71
|
-
npm install -g cabletidy@0.4.0-rc.
|
|
86
|
+
npm install -g cabletidy@0.4.0-rc.3 --registry=https://registry.npmjs.org/
|
|
72
87
|
```
|
|
73
88
|
|
|
74
89
|
### 从本地安装包使用
|
|
@@ -95,6 +110,8 @@ npm 启动器需要本机 Node.js;发行包自带对应平台的原生二进
|
|
|
95
110
|
|
|
96
111
|
`cabletidy stop` 通过本地控制通道请求正常退出,等待已开始的请求、子进程和配置写入完成,再清理 `runtime.json` 并释放操作系统文件锁;不设置强制退出倒计时。主机改名不影响启停。Ctrl+C / SIGTERM 在发送前取消操作,发送过程中取消会提示请求可能已提交,收到接受确认后只取消客户端等待,daemon 继续退出;退出码分别为 130 / 143。`status` 区分运行中、退出排空中、持锁但控制通道无响应和未运行。未运行时 `stop` 成功返回提示,连接提前中断则报告未确认正常退出。
|
|
97
112
|
|
|
113
|
+
`cabletidy restart` 先按 `stop` 的流程等待当前服务正常退出,再使用原配置启动,保留已保存的管理台地址、配置和密钥。未运行时直接启动;停止失败或等待被取消时不会启动新服务。安装版默认后台重启,源码版和直接运行原生程序保持前台;npm 启动器支持 `cabletidy restart --foreground`。
|
|
114
|
+
|
|
98
115
|
`daemon.lock` 现在是持久的普通文件,退出后保留;它存在不代表服务正在运行,请勿删除或替换。异常退出后锁由操作系统释放,无需等待旧版的 10 秒心跳过期。数据目录应位于本地文件系统,不支持跨机器共享目录的锁协调。此功能不包含开机自启或崩溃自动重启。
|
|
99
116
|
|
|
100
117
|
源码调试时的 `npm start` 和 `cargo run --bin cabletidy -- start` 仍在前台运行,便于查看日志和使用 Ctrl+C。安装后的 CLI 也可使用 `cabletidy start --foreground` 临时前台运行。
|
|
@@ -102,6 +119,7 @@ npm 启动器需要本机 Node.js;发行包自带对应平台的原生二进
|
|
|
102
119
|
```bash
|
|
103
120
|
cabletidy --help
|
|
104
121
|
cabletidy status
|
|
122
|
+
cabletidy restart
|
|
105
123
|
cabletidy stop
|
|
106
124
|
```
|
|
107
125
|
|
|
@@ -116,6 +134,8 @@ npm install -g cabletidy@latest --registry=https://registry.npmjs.org/
|
|
|
116
134
|
cabletidy start
|
|
117
135
|
```
|
|
118
136
|
|
|
137
|
+
已支持 `update` 的版本可以直接执行 `cabletidy update`(默认 `latest`,也可指定版本或 `next` 等标签;加 `--check` 只检查不安装,`--registry` 指定的源会保留在提示命令中)。它只更新当前包所在的全局 npm 前缀,因此在 nvm 等多 Node 环境中不会装到别处;运行状态只通过实例锁和 `runtime.json` 判断,配置文件损坏不影响更新。macOS 和 Linux 下 daemon 运行时可直接更新,完成后询问是否重启;重启使用 `stop` 后 `start`,回退到没有 `restart` 的版本(如 `0.3.0`)也可用。Windows 下运行中的可执行文件会被锁定,需要确认先停止 daemon,并等待原进程真正退出后再安装,更新后询问是否启动;无法确认运行状态时不更新。非交互终端中确认均视为否,可加 `--yes` 自动确认。
|
|
138
|
+
|
|
119
139
|
如果新版提示 `ELOCKLEGACY`,说明数据目录中仍有旧版的 `daemon.lock` **目录**。请先从 `runtime.json` 查看 PID,并用系统进程工具核对;旧版 `stop` 不可用时,可手动执行 `kill <实际 PID>`(Windows 使用 `taskkill /PID <实际 PID>`),等待并确认旧进程退出后,再删除这个旧锁目录并启动新版。这里的 `<实际 PID>` 必须替换为核对后的数字;不要删除配置、密钥或新版的普通锁文件。新版不自动控制旧 daemon 或回收旧锁。
|
|
120
140
|
|
|
121
141
|
从本地安装包升级时,将安装目标换成新版本 `.tgz`。卸载前先执行 `cabletidy stop`,再使用 `npm uninstall -g cabletidy`,不会删除 `~/.cabletidy` 或撤销已应用的客户端配置;彻底停用前应先把客户端切换到其他接入。
|
package/bin/cabletidy.mjs
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { launch, root, startBackground } from "./native.mjs";
|
|
4
|
+
import { update } from "./update.mjs";
|
|
4
5
|
|
|
5
6
|
try {
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const command = args[0] || "status";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const starts = ["start", "restart"].includes(command);
|
|
10
|
+
if ((starts || command === "stop") && args.length > 1 && !(starts && args.length === 2 && args[1] === "--foreground")) {
|
|
11
|
+
throw new Error("Usage: cabletidy start [--foreground] | restart [--foreground] | stop");
|
|
10
12
|
}
|
|
11
|
-
if (command === "
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (command === "update") {
|
|
14
|
+
process.exitCode = await update(args.slice(1));
|
|
15
|
+
} else if (starts && args[1] !== "--foreground" && !existsSync(`${root}/Cargo.toml`)) {
|
|
16
|
+
const code = command === "restart" ? await launch(["stop"]).closed : 0;
|
|
17
|
+
if (code !== 0) {
|
|
18
|
+
process.exitCode = code;
|
|
19
|
+
} else {
|
|
20
|
+
const runtime = await startBackground();
|
|
21
|
+
console.log(`CableTidy ${command === "restart" ? "restarted" : "started"}: ${runtime.web.url}\nLogs: ${runtime.logPath}`);
|
|
22
|
+
}
|
|
14
23
|
} else {
|
|
15
|
-
process.exitCode = await launch(
|
|
24
|
+
process.exitCode = await launch(starts ? [command] : args).closed;
|
|
16
25
|
}
|
|
17
26
|
} catch (error) {
|
|
18
27
|
console.error(error.message);
|
package/bin/update.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { executable, root } from "./native.mjs";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const packageName = "cabletidy";
|
|
10
|
+
const defaultRegistry = "https://registry.npmjs.org/";
|
|
11
|
+
const usage = "Usage: cabletidy update [<version|tag>] [--check] [--yes] [--registry <url>]";
|
|
12
|
+
|
|
13
|
+
export function parseArgs(args) {
|
|
14
|
+
let spec;
|
|
15
|
+
let check = false;
|
|
16
|
+
let yes = false;
|
|
17
|
+
let registry = defaultRegistry;
|
|
18
|
+
for (let index = 0; index < args.length; index++) {
|
|
19
|
+
const arg = args[index];
|
|
20
|
+
if (arg === "--check") check = true;
|
|
21
|
+
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
22
|
+
else if (arg === "--registry" && index + 1 < args.length) registry = args[++index];
|
|
23
|
+
else if (arg.startsWith("--registry=")) registry = arg.slice("--registry=".length);
|
|
24
|
+
// Only versions and dist-tags; npm would also accept paths, URLs and git specs.
|
|
25
|
+
else if (spec === undefined && /^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(arg)) spec = arg;
|
|
26
|
+
else throw new Error(usage);
|
|
27
|
+
}
|
|
28
|
+
if (!/^https?:$/.test(URL.parse(registry)?.protocol ?? "")) throw new Error(`Invalid registry: ${registry}`);
|
|
29
|
+
return { spec: spec ?? "latest", explicit: spec !== undefined, check, yes, registry };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function compareVersions(left, right) {
|
|
33
|
+
const parse = version => {
|
|
34
|
+
const [core, pre] = version.split("+")[0].split(/-(.*)/s);
|
|
35
|
+
return [core.split(".").map(Number), pre ? pre.split(".") : []];
|
|
36
|
+
};
|
|
37
|
+
const [leftCore, leftPre] = parse(left);
|
|
38
|
+
const [rightCore, rightPre] = parse(right);
|
|
39
|
+
for (let index = 0; index < 3; index++) {
|
|
40
|
+
if (leftCore[index] !== rightCore[index]) return Math.sign(leftCore[index] - rightCore[index]);
|
|
41
|
+
}
|
|
42
|
+
// A release sorts after its prereleases.
|
|
43
|
+
if (!leftPre.length || !rightPre.length) return Math.sign(rightPre.length - leftPre.length);
|
|
44
|
+
for (let index = 0; index < Math.min(leftPre.length, rightPre.length); index++) {
|
|
45
|
+
const [a, b] = [leftPre[index], rightPre[index]];
|
|
46
|
+
if (a === b) continue;
|
|
47
|
+
const [numericA, numericB] = [/^\d+$/.test(a), /^\d+$/.test(b)];
|
|
48
|
+
if (numericA && numericB) return Math.sign(Number(a) - Number(b));
|
|
49
|
+
if (numericA !== numericB) return numericA ? -1 : 1;
|
|
50
|
+
return a < b ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
return Math.sign(leftPre.length - rightPre.length);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The prefix that owns this package, or null when it is not a global npm install (local dependency, npx cache).
|
|
56
|
+
// Updating that exact prefix keeps nvm and custom npm prefixes from installing somewhere else.
|
|
57
|
+
export function globalPrefix(packageDir) {
|
|
58
|
+
const windows = process.platform === "win32";
|
|
59
|
+
const modules = path.dirname(packageDir);
|
|
60
|
+
if (path.basename(modules) !== "node_modules") return null;
|
|
61
|
+
const parent = path.dirname(modules);
|
|
62
|
+
if (!windows && path.basename(parent) !== "lib") return null;
|
|
63
|
+
const prefix = windows ? parent : path.dirname(parent);
|
|
64
|
+
const shim = windows ? path.join(prefix, `${packageName}.cmd`) : path.join(prefix, "bin", packageName);
|
|
65
|
+
return existsSync(shim) ? prefix : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Prefer the npm bundled with the running node over whichever npm is first on PATH.
|
|
69
|
+
function npm() {
|
|
70
|
+
const directory = path.dirname(process.execPath);
|
|
71
|
+
const cli = process.platform === "win32"
|
|
72
|
+
? path.join(directory, "node_modules", "npm", "bin", "npm-cli.js")
|
|
73
|
+
: path.join(directory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
74
|
+
if (existsSync(cli)) return [process.execPath, [cli]];
|
|
75
|
+
if (process.platform === "win32") return null;
|
|
76
|
+
return ["npm", []];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function run(command, args) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(command, args, { stdio: "inherit", windowsHide: true });
|
|
82
|
+
child.once("error", reject);
|
|
83
|
+
child.once("close", code => resolve(code ?? 1));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Quote an argument for a command the user is told to run in their shell.
|
|
88
|
+
export function shellArg(value, platform = process.platform) {
|
|
89
|
+
if (/^[\w@%+=:,./-]+$/.test(value)) return value;
|
|
90
|
+
return platform === "win32" ? `"${value}"` : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function confirm(question, yes) {
|
|
94
|
+
if (yes) {
|
|
95
|
+
console.log(`${question} [y/N] y (--yes)`);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (!process.stdin.isTTY) {
|
|
99
|
+
console.log(`${question} [y/N] (no terminal; assuming no, pass --yes to confirm)`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
103
|
+
try {
|
|
104
|
+
return /^y(es)?$/i.test((await prompt.question(`${question} [y/N] `)).trim());
|
|
105
|
+
} finally {
|
|
106
|
+
prompt.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Probes the instance lock and runtime.json without parsing config.json.
|
|
111
|
+
async function instance(binary) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse((await execFileAsync(binary, ["__instance"], { windowsHide: true })).stdout);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return { error: (error.stderr || error.message).trim() };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function update(args) {
|
|
120
|
+
const options = parseArgs(args);
|
|
121
|
+
if (existsSync(path.join(root, "Cargo.toml"))) {
|
|
122
|
+
throw new Error("CableTidy is running from a source checkout; update it with git pull and npm run build.");
|
|
123
|
+
}
|
|
124
|
+
const current = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
125
|
+
const registry = `--registry=${options.registry}`;
|
|
126
|
+
const manual = `npm install -g ${packageName}@${options.spec} ${shellArg(registry)}`;
|
|
127
|
+
const [npmCommand, npmArgs] = npm() ?? [];
|
|
128
|
+
if (!npmCommand) throw new Error(`Cannot find the npm bundled with ${process.execPath}; update manually:\n ${manual}`);
|
|
129
|
+
|
|
130
|
+
const { stdout } = await execFileAsync(npmCommand, [...npmArgs, "view", `${packageName}@${options.spec}`, "version", "--json", registry], { windowsHide: true });
|
|
131
|
+
const published = stdout.trim() ? JSON.parse(stdout) : null;
|
|
132
|
+
const target = Array.isArray(published) ? published.at(-1) : published;
|
|
133
|
+
if (!target) throw new Error(`No published ${packageName} version matches ${options.spec}`);
|
|
134
|
+
const order = compareVersions(target, current);
|
|
135
|
+
if (order === 0) {
|
|
136
|
+
console.log(`CableTidy ${current} is up to date`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
if (order < 0 && !options.explicit) {
|
|
140
|
+
console.log(`CableTidy ${current} is newer than ${options.spec} (${target}); pass a version to change it`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
if (options.check) {
|
|
144
|
+
const command = ["cabletidy update", options.explicit && options.spec, options.registry !== defaultRegistry && shellArg(registry)];
|
|
145
|
+
console.log(`CableTidy ${current} -> ${target} available; run: ${command.filter(Boolean).join(" ")}`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const prefix = globalPrefix(path.resolve(root));
|
|
150
|
+
if (!prefix) throw new Error(`CableTidy was not installed with npm install -g; update it the way it was installed, e.g.\n ${manual}`);
|
|
151
|
+
const windows = process.platform === "win32";
|
|
152
|
+
const launcher = path.join(root, "bin", "cabletidy.mjs");
|
|
153
|
+
const state = await instance(executable());
|
|
154
|
+
if (state.error) {
|
|
155
|
+
// Windows cannot replace the package while the daemon may still be using its executable.
|
|
156
|
+
if (windows) throw new Error(`Cannot determine whether CableTidy is running: ${state.error}\nStop it manually, then run cabletidy update again.`);
|
|
157
|
+
console.warn(`Cannot determine whether CableTidy is running: ${state.error}`);
|
|
158
|
+
}
|
|
159
|
+
if (state.running && windows) {
|
|
160
|
+
if (!state.process) throw new Error("Cannot verify the running CableTidy process (it may be starting or shutting down); stop it manually, then run cabletidy update again.");
|
|
161
|
+
// Windows locks the running executable, so npm cannot replace the package until the daemon exits.
|
|
162
|
+
if (!await confirm("The running CableTidy daemon must be stopped before updating. Stop it now?", options.yes)) {
|
|
163
|
+
console.log("Update cancelled");
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
const stopped = await run(process.execPath, [launcher, "stop"]);
|
|
167
|
+
if (stopped !== 0) return stopped;
|
|
168
|
+
// stop returns once shutdown completes; the process may still be closing other control connections.
|
|
169
|
+
const exited = await run(executable(), ["__instance", "--wait-exit", JSON.stringify(state.process)]);
|
|
170
|
+
if (exited !== 0) return exited;
|
|
171
|
+
}
|
|
172
|
+
console.log(`Updating CableTidy ${current} -> ${target} in ${prefix}`);
|
|
173
|
+
const installed = await run(npmCommand, [...npmArgs, "install", "-g", `--prefix=${prefix}`, registry, `${packageName}@${target}`]);
|
|
174
|
+
if (installed !== 0) {
|
|
175
|
+
console.error(`npm install failed${state.running && windows ? "; CableTidy is stopped, run cabletidy start to resume" : ""}`);
|
|
176
|
+
return installed;
|
|
177
|
+
}
|
|
178
|
+
// stop + start is supported by every version that has stop; restart is newer.
|
|
179
|
+
const switchCommand = windows ? "cabletidy start" : "cabletidy stop && cabletidy start";
|
|
180
|
+
if (state.error) console.log(`If CableTidy is running, run ${switchCommand} to use ${target}`);
|
|
181
|
+
if (!state.running) return 0;
|
|
182
|
+
if (!await confirm(windows ? "Start CableTidy again?" : `Restart CableTidy now? The daemon is still running ${current}.`, options.yes)) {
|
|
183
|
+
console.log(`Run ${switchCommand} when ready`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
// Fresh processes load the newly installed launcher and executable.
|
|
187
|
+
if (!windows) {
|
|
188
|
+
const stopped = await run(process.execPath, [launcher, "stop"]);
|
|
189
|
+
if (stopped !== 0) return stopped;
|
|
190
|
+
}
|
|
191
|
+
return run(process.execPath, [launcher, "start"]);
|
|
192
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.0-rc.
|
|
2
|
+
"version": "0.4.0-rc.3",
|
|
3
3
|
"platform": "darwin-arm64",
|
|
4
4
|
"target": "aarch64-apple-darwin",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "6e03356848797f965731b7a1b31dc194c3c7c61c8fa0e610afdf83a1c74456c3"
|
|
6
6
|
}
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.0-rc.
|
|
2
|
+
"version": "0.4.0-rc.3",
|
|
3
3
|
"platform": "darwin-x64",
|
|
4
4
|
"target": "x86_64-apple-darwin",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "dbf9b0992f28769956f5ad14746229c6e24545f37f4a88189fc900868f209cca"
|
|
6
6
|
}
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.0-rc.
|
|
2
|
+
"version": "0.4.0-rc.3",
|
|
3
3
|
"platform": "linux-arm64",
|
|
4
4
|
"target": "aarch64-unknown-linux-musl",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "64479c16d3c391136dc88bc7331f11781bc8284d40d7ca9e735c1b06bbf52ee6"
|
|
6
6
|
}
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.0-rc.
|
|
2
|
+
"version": "0.4.0-rc.3",
|
|
3
3
|
"platform": "linux-x64",
|
|
4
4
|
"target": "x86_64-unknown-linux-musl",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "b011bc999695615547e43a594aaf8d39d9621e08c4359984ca0d862641e58ebd"
|
|
6
6
|
}
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.4.0-rc.
|
|
2
|
+
"version": "0.4.0-rc.3",
|
|
3
3
|
"platform": "win32-x64",
|
|
4
4
|
"target": "x86_64-pc-windows-msvc",
|
|
5
|
-
"sha256": "
|
|
5
|
+
"sha256": "1194736d1258c7b944e69710cf2eb63efbb0c9ef7e8b621c1894c12e87324beb"
|
|
6
6
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cabletidy",
|
|
3
|
-
"version": "0.4.0-rc.
|
|
3
|
+
"version": "0.4.0-rc.3",
|
|
4
4
|
"description": "A local multi-target coding CLI gateway and provider manager.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"dev:status": "node scripts/dev.mjs status",
|
|
35
35
|
"test": "node scripts/test.mjs",
|
|
36
36
|
"test:security:browser": "node scripts/build-native.mjs --debug --test && node scripts/security-browser.mjs",
|
|
37
|
+
"test:config:browser": "node scripts/build-native.mjs --debug --test && node scripts/config-transfer-browser.mjs",
|
|
37
38
|
"test:package": "npm run build && node --test scripts/package-smoke.mjs",
|
|
38
39
|
"test:claude": "node scripts/build-native.mjs --debug --test && node --test scripts/claude-smoke.mjs",
|
|
39
40
|
"check:release": "node scripts/check-release.mjs",
|