ai-agent-helper 0.1.1 → 0.2.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.
- package/CHANGELOG.md +8 -0
- package/README.md +42 -5
- package/dist/agents/codex.js +1 -0
- package/dist/cli.js +129 -23
- package/dist/config/store.d.ts +10 -0
- package/dist/config/store.js +167 -0
- package/dist/detection/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation/manage.d.ts +43 -0
- package/dist/installation/manage.js +132 -0
- package/dist/installation/process.d.ts +2 -0
- package/dist/installation/process.js +64 -0
- package/dist/installation/types.d.ts +4 -0
- package/dist/installation/types.js +1 -0
- package/dist/proxy/environment.d.ts +3 -0
- package/dist/proxy/environment.js +37 -0
- package/docs/configuration.md +14 -10
- package/examples/config.toml +1 -1
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- 实现 `config`、`config set key=value`、`config unset`,使用用户级 TOML 文件,保留注释并校验整份配置。
|
|
6
|
+
- 实现经默认代理安装/更新 npm Codex,支持临时代理、预览和重新安装。
|
|
7
|
+
- 实现 `update` 默认更新工具自身,以及 `--self`、`--agent codex`、`--all-agents` 互斥目标。
|
|
8
|
+
- 检查 npm 全局安装来源和 prefix,拒绝覆盖其他渠道或 `npm link` 安装。
|
|
9
|
+
- 增加配置、安装执行及真实 npm 本地代理集成测试。
|
|
10
|
+
|
|
3
11
|
## 0.1.1
|
|
4
12
|
|
|
5
13
|
- 定义 `~/.ai-agent-helper/config.toml` 的配置数据模型:`version`、`proxy`、`no_proxy`。
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ai-agent-helper
|
|
2
2
|
|
|
3
|
-
使用 TypeScript 和 Zod 开发的 AI agent CLI
|
|
3
|
+
使用 TypeScript 和 Zod 开发的 AI agent CLI 管理工具。检测本机 PATH 中的 agent,管理 TOML 配置,并通过默认代理安装或更新 Codex,也支持更新工具自身。
|
|
4
|
+
|
|
5
|
+
从 `0.2.0` 起提供 `config`、`install`、`update` 命令;安装和更新 agent 目前先支持 Codex。
|
|
4
6
|
|
|
5
7
|
需要 Node.js **22.13 或更高版本**。
|
|
6
8
|
|
|
@@ -34,6 +36,16 @@ agent-helper list --help
|
|
|
34
36
|
agent-helper list --all
|
|
35
37
|
agent-helper list --json
|
|
36
38
|
agent-helper list --all --json
|
|
39
|
+
agent-helper config
|
|
40
|
+
agent-helper config set proxy=http://127.0.0.1:7890
|
|
41
|
+
agent-helper config set no_proxy=localhost,127.0.0.1,::1
|
|
42
|
+
agent-helper config unset proxy
|
|
43
|
+
agent-helper install codex --dry-run
|
|
44
|
+
agent-helper install codex
|
|
45
|
+
agent-helper update --dry-run
|
|
46
|
+
agent-helper update --self
|
|
47
|
+
agent-helper update --agent codex
|
|
48
|
+
agent-helper update --all-agents
|
|
37
49
|
```
|
|
38
50
|
|
|
39
51
|
- 无参数时显示帮助。
|
|
@@ -85,11 +97,34 @@ agentListSchema.parse(all);
|
|
|
85
97
|
|
|
86
98
|
包提供 ESM 导出和类型声明。Zod 校验 CLI 选项及检测结果,并约束已检测到条目必须包含路径、未检测到条目的路径必须为 `null`。
|
|
87
99
|
|
|
88
|
-
##
|
|
100
|
+
## 配置与代理
|
|
101
|
+
|
|
102
|
+
配置文件为 `~/.ai-agent-helper/config.toml`,采用 TOML。`config` 查看配置与默认值;`config set key=value` 可以一次设置多项,只按首个等号分割。修改保留注释、字段顺序和换行风格,写入前校验整份配置,失败不覆盖文件。
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
version = 1
|
|
106
|
+
proxy = "http://127.0.0.1:7890"
|
|
107
|
+
no_proxy = ["localhost", "127.0.0.1", "::1"]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
可写项为 `proxy` 和 `no_proxy`;版本字段由工具维护。CLI 中的 `no_proxy` 使用逗号分隔,保存后为 TOML 数组。删除字段使用 `config unset`,空字符串不会当成删除。`config` 展示代理时隐藏账号和密码;文件仍保存实际配置值。
|
|
89
111
|
|
|
90
|
-
|
|
112
|
+
代理优先级为 `--proxy` → 配置文件的 `proxy` → npm 已有配置。代理及绕过规则只注入安装/更新子进程,不改动系统代理或全局 npm 配置。支持 HTTP/HTTPS 代理地址,绕过规则的匹配语义由 npm 决定。
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
详细约定见 [配置功能文档](docs/configuration.md),带注释的示例见 [config.toml](examples/config.toml)。也提供配置模型、`readConfig`、`writeConfig`、`parseAssignments` 等 TypeScript API。
|
|
115
|
+
|
|
116
|
+
## 安装与更新
|
|
117
|
+
|
|
118
|
+
- `install codex` 安装 npm 包 `@openai/codex`;已安装时跳过,`--reinstall` 可重装普通 npm 全局安装。
|
|
119
|
+
- `update` 默认等同于 `update --self`,更新 npm 包 `ai-agent-helper`。
|
|
120
|
+
- `update --agent codex` 只更新 Codex;`update --all-agents` 只更新 PATH 中已检测到且当前支持更新的 agent,目前为 Codex。
|
|
121
|
+
- `--self`、`--agent`、`--all-agents` 互斥。其他 agent 保留检测功能,暂不支持安装/更新。
|
|
122
|
+
- 安装/更新通过 npm 官方 registry 查询 `latest`,然后安装查询到的确切版本,保留当前 npm 的全局 prefix;更新不会自动降级。
|
|
123
|
+
- 仅管理当前 npm 全局目录中的普通安装。遇到源码、`npm link`、临时 `npx`、其他 prefix 或其他渠道的安装时,实际更新会报错并提示原安装方式,防止写入另一个位置。
|
|
124
|
+
- `--dry-run` 仅检查本地 npm 目录、已有版本和安装来源,展示计划;不联网查询版本,不执行安装。对于不适用的安装来源会展示原因。
|
|
125
|
+
- 安装完成后验证目标目录内的包版本和命令入口,不启动 Codex、不测试登录。首次安装如果命令目录尚未在 PATH 中,会提示检查 PATH 并重新打开终端。
|
|
126
|
+
|
|
127
|
+
Codex 的 npm 安装/更新方式见 [OpenAI 官方 CLI 文档](https://developers.openai.com/codex/cli#npm)。
|
|
93
128
|
|
|
94
129
|
## 开发与扩展
|
|
95
130
|
|
|
@@ -101,4 +136,6 @@ npm pack
|
|
|
101
136
|
|
|
102
137
|
每种 agent 在 `src/agents/` 中独立实现 `AgentProvider.detect()`,在 `src/agents/index.ts` 注册。当前共用 `src/detection/find-executable.ts` 的 PATH 查找能力;以后可在各自模块内添加专用安装路径或其他识别逻辑,不必修改 CLI。
|
|
103
138
|
|
|
104
|
-
`src/list-agents.ts`
|
|
139
|
+
`src/list-agents.ts` 负责检测汇总,`src/config/` 负责配置,`src/installation/` 负责 npm 安装与更新,`src/proxy/` 负责子进程代理环境,`src/cli.ts` 负责命令分发。Codex 的 npm 包及入口信息定义在独立 agent 模块中。
|
|
140
|
+
|
|
141
|
+
测试覆盖 CLI、配置注释保留、原子写入、更新目标互斥、安装来源检查及失败验证。代理集成测试使用真实 npm、本地 HTTP 测试代理和隔离全局目录中的测试包,验证元数据及安装包经过代理,并完成安装和升级;不安装真实 Codex。平台专属用例只在对应平台执行。
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findExecutable } from "../detection/find-executable.js";
|
|
2
2
|
export const codex = {
|
|
3
3
|
definition: { id: "codex", name: "Codex", command: "codex" },
|
|
4
|
+
installer: { packageName: "@openai/codex", entryPoint: "bin/codex.js" },
|
|
4
5
|
async detect(context) {
|
|
5
6
|
return findExecutable("codex", context);
|
|
6
7
|
},
|
package/dist/cli.js
CHANGED
|
@@ -2,36 +2,82 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { configPath, formatConfig, parseAssignments, readConfig, writeConfig } from "./config/store.js";
|
|
6
|
+
import { configKeySchema } from "./config/schema.js";
|
|
7
|
+
import { managePackage, resolveUpdateTarget } from "./installation/manage.js";
|
|
8
|
+
import { codex } from "./agents/codex.js";
|
|
9
|
+
import { redact } from "./proxy/environment.js";
|
|
5
10
|
import { listAgents } from "./list-agents.js";
|
|
6
|
-
const help = `agent-helper —
|
|
11
|
+
const help = `agent-helper — 管理本地 AI agent CLI
|
|
7
12
|
|
|
8
13
|
用法:
|
|
9
14
|
agent-helper list [--all] [--json]
|
|
15
|
+
agent-helper config [set <key=value> ... | unset <key>]
|
|
16
|
+
agent-helper install codex [--proxy <url>] [--dry-run] [--reinstall]
|
|
17
|
+
agent-helper update [--self | --agent codex | --all-agents] [--proxy <url>] [--dry-run]
|
|
10
18
|
agent-helper [--help | --version]
|
|
11
19
|
|
|
12
20
|
命令:
|
|
13
21
|
list 列出 PATH 中检测到的 agent
|
|
22
|
+
config 查看所有配置,或设置/删除配置项
|
|
23
|
+
install codex 通过 npm 安装 Codex
|
|
24
|
+
update 默认更新工具自身;--agent codex 更新 Codex
|
|
14
25
|
|
|
15
26
|
选项:
|
|
16
27
|
-a, --all 同时显示未检测到的 agent(仅用于 list)
|
|
17
28
|
--json 输出 JSON 数组(仅用于 list)
|
|
18
29
|
-h, --help 显示帮助,支持 list --help
|
|
19
30
|
-v, --version 显示 agent-helper 版本
|
|
31
|
+
--self 更新工具自身(update 的默认目标)
|
|
32
|
+
--agent 指定要更新的 agent,目前仅 codex
|
|
33
|
+
--all-agents 更新所有已安装且支持更新的 agent,目前仅 codex
|
|
34
|
+
--proxy 本次安装/更新使用的 HTTP/HTTPS 代理
|
|
35
|
+
--dry-run 本地预览,不联网查询版本,不执行安装
|
|
36
|
+
--reinstall 重新安装已存在的 npm Codex(仅用于 install)
|
|
20
37
|
|
|
21
38
|
示例:
|
|
22
39
|
agent-helper list
|
|
23
40
|
agent-helper list --all
|
|
24
41
|
agent-helper list --json
|
|
25
42
|
agent-helper list --all --json
|
|
43
|
+
agent-helper config set proxy=http://127.0.0.1:7890
|
|
44
|
+
agent-helper config set no_proxy=localhost,127.0.0.1,::1
|
|
45
|
+
agent-helper config unset proxy
|
|
46
|
+
agent-helper install codex
|
|
47
|
+
agent-helper update
|
|
48
|
+
agent-helper update --agent codex
|
|
26
49
|
|
|
27
50
|
检测范围:Claude Code、Codex、Gemini CLI、OpenCode、Aider、GitHub Copilot。
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
list 只查找 PATH 中的入口;安装/更新仅支持 npm Codex 和 npm 全局安装的工具自身。
|
|
52
|
+
配置文件:~/.ai-agent-helper/config.toml;配置只作用于安装/更新子进程。`;
|
|
53
|
+
const commandHelp = {
|
|
54
|
+
config: `用法:
|
|
55
|
+
agent-helper config
|
|
56
|
+
agent-helper config set <key=value> [key=value ...]
|
|
57
|
+
agent-helper config unset <key>
|
|
58
|
+
|
|
59
|
+
可写项:proxy(HTTP/HTTPS URL);no_proxy(逗号分隔的非空规则)。
|
|
60
|
+
配置文件:~/.ai-agent-helper/config.toml;省略 proxy 时继承安装工具原有配置。
|
|
61
|
+
示例:agent-helper config set proxy=http://127.0.0.1:7890 no_proxy=localhost,127.0.0.1
|
|
62
|
+
version 由工具管理;未知键、重复键和无效值会报错,修改保留原有注释。`,
|
|
63
|
+
install: `用法:agent-helper install codex [--proxy <url>] [--dry-run] [--reinstall]
|
|
64
|
+
仅支持 npm 包 @openai/codex,安装目标为当前 npm 全局目录。
|
|
65
|
+
代理优先级:--proxy → config 中的 proxy → npm 原有配置。
|
|
66
|
+
--dry-run 只做本地检查;不会联网查询、下载或修改安装。
|
|
67
|
+
示例:agent-helper install codex --proxy http://127.0.0.1:7890`,
|
|
68
|
+
update: `用法:agent-helper update [--self | --agent codex | --all-agents] [--proxy <url>] [--dry-run]
|
|
69
|
+
不指定目标时等同于 --self;三个目标参数互斥。
|
|
70
|
+
--all-agents 仅更新已检测到且支持的 agent,目前只有 codex。
|
|
71
|
+
只支持普通 npm 全局安装;源码、npm link、npx 或其他安装渠道请通过原方式更新。
|
|
72
|
+
--dry-run 不联网、不安装;实际更新先查询 latest,再安装该具体版本。
|
|
73
|
+
示例:agent-helper update --agent codex --dry-run`,
|
|
74
|
+
};
|
|
75
|
+
function printResult(result) {
|
|
76
|
+
console.log(`${result.target}: ${result.status} (${result.from ?? "未安装"} → ${result.to})`);
|
|
77
|
+
console.log(`代理:${result.proxy ?? "继承 npm 原有配置"}`);
|
|
78
|
+
console.log(`命令:npm ${result.args.map((argument) => JSON.stringify(argument)).join(" ")}`);
|
|
79
|
+
console.log(result.message);
|
|
80
|
+
}
|
|
35
81
|
function printTable(agents) {
|
|
36
82
|
if (agents.length === 0) {
|
|
37
83
|
console.log("未在 PATH 中检测到支持的 agent。使用 agent-helper list --all 查看检测范围。");
|
|
@@ -50,36 +96,96 @@ async function main() {
|
|
|
50
96
|
json: { type: "boolean" },
|
|
51
97
|
help: { type: "boolean", short: "h" },
|
|
52
98
|
version: { type: "boolean", short: "v" },
|
|
99
|
+
self: { type: "boolean" },
|
|
100
|
+
agent: { type: "string" },
|
|
101
|
+
"all-agents": { type: "boolean" },
|
|
102
|
+
proxy: { type: "string" },
|
|
103
|
+
"dry-run": { type: "boolean" },
|
|
104
|
+
reinstall: { type: "boolean" },
|
|
53
105
|
},
|
|
54
106
|
allowPositionals: true,
|
|
55
107
|
strict: true,
|
|
56
108
|
});
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
throw new Error(
|
|
109
|
+
const command = positionals[0];
|
|
110
|
+
if (command !== undefined && !["list", "config", "install", "update"].includes(command)) {
|
|
111
|
+
throw new Error("未知命令。使用 agent-helper --help 查看帮助。");
|
|
60
112
|
}
|
|
61
|
-
if (
|
|
62
|
-
console.log(help);
|
|
113
|
+
if (values.help) {
|
|
114
|
+
console.log(command === undefined ? help : commandHelp[command] ?? help);
|
|
63
115
|
return;
|
|
64
116
|
}
|
|
65
|
-
|
|
117
|
+
const allowed = {
|
|
118
|
+
root: ["version"], list: ["all", "json"], config: [],
|
|
119
|
+
install: ["proxy", "dry-run", "reinstall"],
|
|
120
|
+
update: ["self", "agent", "all-agents", "proxy", "dry-run"],
|
|
121
|
+
};
|
|
122
|
+
for (const key of Object.keys(values)) {
|
|
123
|
+
if (!allowed[command ?? "root"].includes(key))
|
|
124
|
+
throw new Error(`当前命令不支持 --${key}。使用 --help 查看帮助。`);
|
|
125
|
+
}
|
|
126
|
+
if (values.version) {
|
|
66
127
|
const metadata = z.object({ version: z.string().min(1) }).parse(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")));
|
|
67
128
|
console.log(metadata.version);
|
|
68
129
|
return;
|
|
69
130
|
}
|
|
70
|
-
if (
|
|
71
|
-
if (options.all || options.json)
|
|
72
|
-
throw new Error("--all 和 --json 需要与 list 命令一起使用。");
|
|
131
|
+
if (command === undefined) {
|
|
73
132
|
console.log(help);
|
|
74
133
|
return;
|
|
75
134
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
if (command === "list") {
|
|
136
|
+
if (positionals.length !== 1)
|
|
137
|
+
throw new Error("list 不接受额外位置参数。");
|
|
138
|
+
const agents = await listAgents({ includeMissing: values.all ?? false });
|
|
139
|
+
if (values.json)
|
|
140
|
+
console.log(JSON.stringify(agents, null, 2));
|
|
141
|
+
else
|
|
142
|
+
printTable(agents);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (command === "config") {
|
|
146
|
+
const action = positionals[1];
|
|
147
|
+
if (action === undefined)
|
|
148
|
+
console.log(`# ${configPath()}\n${formatConfig(await readConfig())}`);
|
|
149
|
+
else if (action === "set") {
|
|
150
|
+
await writeConfig(parseAssignments(positionals.slice(2)));
|
|
151
|
+
console.log(`配置已保存:${configPath()}`);
|
|
152
|
+
}
|
|
153
|
+
else if (action === "unset" && positionals.length === 3) {
|
|
154
|
+
const key = configKeySchema.safeParse(positionals[2]);
|
|
155
|
+
if (!key.success)
|
|
156
|
+
throw new Error("可删除配置项仅支持 proxy、no_proxy。");
|
|
157
|
+
await writeConfig(new Map([[key.data, undefined]]));
|
|
158
|
+
console.log(`配置项已恢复默认:${key.data}`);
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
throw new Error("用法:config [set <key=value> ... | unset <key>]");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const config = await readConfig();
|
|
165
|
+
const common = { config, dryRun: values["dry-run"] ?? false, ...(values.proxy === undefined ? {} : { proxy: values.proxy }) };
|
|
166
|
+
if (command === "install") {
|
|
167
|
+
if (positionals.length !== 2 || positionals[1] !== "codex")
|
|
168
|
+
throw new Error("用法:install codex。目前仅支持 codex。");
|
|
169
|
+
printResult(await managePackage({ ...common, target: "codex", action: "install", reinstall: values.reinstall ?? false }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (positionals.length !== 1)
|
|
173
|
+
throw new Error("update 不接受位置参数,请使用 --agent codex。");
|
|
174
|
+
const target = resolveUpdateTarget({
|
|
175
|
+
...(values.self === undefined ? {} : { self: values.self }),
|
|
176
|
+
...(values.agent === undefined ? {} : { agent: values.agent }),
|
|
177
|
+
...(values["all-agents"] === undefined ? {} : { allAgents: values["all-agents"] }),
|
|
178
|
+
});
|
|
179
|
+
if (target.kind === "all-agents") {
|
|
180
|
+
const installed = await codex.detect({ env: process.env, platform: process.platform, cwd: process.cwd() });
|
|
181
|
+
if (!installed) {
|
|
182
|
+
console.log("未在 PATH 中检测到可更新的 Codex;其他 agent 尚未支持更新。");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
printResult(await managePackage({ ...common, target: target.kind === "self" ? "self" : "codex", action: "update" }));
|
|
81
187
|
}
|
|
82
188
|
main().catch((error) => {
|
|
83
|
-
console.error(`agent-helper: ${error instanceof Error ? error.message : String(error)}`);
|
|
189
|
+
console.error(`agent-helper: ${redact(error instanceof Error ? error.message : String(error))}`);
|
|
84
190
|
process.exitCode = 1;
|
|
85
191
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Config, type ConfigKey } from "./schema.js";
|
|
2
|
+
export declare function configPath(home?: string): string;
|
|
3
|
+
export declare function readConfig(path?: string): Promise<Config>;
|
|
4
|
+
export type ConfigChanges = Map<ConfigKey, string | string[] | undefined>;
|
|
5
|
+
/** Split on the first equals sign: credentials may themselves contain '='. */
|
|
6
|
+
export declare function parseAssignments(assignments: string[]): ConfigChanges;
|
|
7
|
+
/** Edit only AST value ranges, leaving comments, whitespace and key order intact. */
|
|
8
|
+
export declare function editConfig(source: string, changes: ConfigChanges): string;
|
|
9
|
+
export declare function writeConfig(changes: ConfigChanges, path?: string): Promise<void>;
|
|
10
|
+
export declare function formatConfig(config: Config): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
5
|
+
import { stringify } from "smol-toml";
|
|
6
|
+
import { getStaticTOMLValue, parseForESLint } from "toml-eslint-parser";
|
|
7
|
+
import { configKeySchema, configSchema } from "./schema.js";
|
|
8
|
+
export function configPath(home = homedir()) {
|
|
9
|
+
return join(home, ".ai-agent-helper", "config.toml");
|
|
10
|
+
}
|
|
11
|
+
async function readOptional(path) {
|
|
12
|
+
try {
|
|
13
|
+
return await readFile(path, "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT")
|
|
17
|
+
return undefined;
|
|
18
|
+
throw new Error(`无法读取配置文件:${path}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function validate(value) {
|
|
22
|
+
const result = configSchema.safeParse(value);
|
|
23
|
+
if (!result.success) {
|
|
24
|
+
const fields = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "根节点/未知字段"))];
|
|
25
|
+
throw new Error(`配置校验失败(${fields.join("、")}),请检查字段、类型、代理 URL 和格式版本。`);
|
|
26
|
+
}
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
29
|
+
function parseConfig(source) {
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = getStaticTOMLValue(parseForESLint(source, { tomlVersion: "1.0" }).ast);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error("TOML 语法错误,请检查引号、重复键和数组格式。");
|
|
36
|
+
}
|
|
37
|
+
return validate(value);
|
|
38
|
+
}
|
|
39
|
+
export async function readConfig(path = configPath()) {
|
|
40
|
+
const source = await readOptional(path);
|
|
41
|
+
try {
|
|
42
|
+
return parseConfig(source ?? "");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error(`${path}:${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Split on the first equals sign: credentials may themselves contain '='. */
|
|
49
|
+
export function parseAssignments(assignments) {
|
|
50
|
+
if (assignments.length === 0)
|
|
51
|
+
throw new Error("用法:config set <key=value> [key=value ...]");
|
|
52
|
+
const changes = new Map();
|
|
53
|
+
for (const assignment of assignments) {
|
|
54
|
+
const separator = assignment.indexOf("=");
|
|
55
|
+
if (separator <= 0)
|
|
56
|
+
throw new Error("配置格式应为 key=value。");
|
|
57
|
+
const keyResult = configKeySchema.safeParse(assignment.slice(0, separator));
|
|
58
|
+
if (!keyResult.success)
|
|
59
|
+
throw new Error("可写配置项仅支持 proxy、no_proxy。");
|
|
60
|
+
const key = keyResult.data;
|
|
61
|
+
if (changes.has(key))
|
|
62
|
+
throw new Error(`配置项重复:${key}`);
|
|
63
|
+
const value = assignment.slice(separator + 1);
|
|
64
|
+
changes.set(key, key === "no_proxy" ? value.split(",") : value);
|
|
65
|
+
}
|
|
66
|
+
const candidate = Object.fromEntries(changes);
|
|
67
|
+
validate(candidate);
|
|
68
|
+
return changes;
|
|
69
|
+
}
|
|
70
|
+
/** Edit only AST value ranges, leaving comments, whitespace and key order intact. */
|
|
71
|
+
export function editConfig(source, changes) {
|
|
72
|
+
const current = parseConfig(source);
|
|
73
|
+
const candidate = { ...current };
|
|
74
|
+
for (const [key, value] of changes) {
|
|
75
|
+
configKeySchema.parse(key);
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
delete candidate[key];
|
|
78
|
+
else
|
|
79
|
+
candidate[key] = value;
|
|
80
|
+
}
|
|
81
|
+
validate(candidate);
|
|
82
|
+
const { ast } = parseForESLint(source, { tomlVersion: "1.0" });
|
|
83
|
+
const patches = [];
|
|
84
|
+
const additions = [];
|
|
85
|
+
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
|
86
|
+
const nodes = ast.body[0].body.filter((node) => node.type === "TOMLKeyValue");
|
|
87
|
+
if (!nodes.some((node) => getStaticTOMLValue(node.key)[0] === "version"))
|
|
88
|
+
additions.push("version = 1");
|
|
89
|
+
for (const [key, value] of changes) {
|
|
90
|
+
const node = nodes.find((node) => getStaticTOMLValue(node.key)[0] === key);
|
|
91
|
+
const innerComments = node ? ast.comments
|
|
92
|
+
.filter((comment) => comment.range[0] >= node.value.range[0] && comment.range[1] <= node.value.range[1])
|
|
93
|
+
.map((comment) => source.slice(comment.range[0], comment.range[1])) : [];
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
if (node)
|
|
96
|
+
patches.push({ start: node.range[0], end: node.range[1], value: innerComments.join(newline) });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Let the TOML serializer escape strings instead of treating CLI input as TOML code.
|
|
100
|
+
const encoded = stringify({ value }).trim().slice("value = ".length);
|
|
101
|
+
if (node)
|
|
102
|
+
patches.push({ start: node.value.range[0], end: node.value.range[1], value: encoded.replace(/\n/g, newline) + (innerComments.length ? newline + innerComments.join(newline) : "") });
|
|
103
|
+
else
|
|
104
|
+
additions.push(`${key} = ${encoded.replace(/\n/g, newline)}`);
|
|
105
|
+
}
|
|
106
|
+
let output = source;
|
|
107
|
+
for (const patch of patches.sort((a, b) => b.start - a.start)) {
|
|
108
|
+
output = output.slice(0, patch.start) + patch.value + output.slice(patch.end);
|
|
109
|
+
}
|
|
110
|
+
if (additions.length)
|
|
111
|
+
output += (output && !output.endsWith("\n") ? newline : "") + additions.join(newline) + newline;
|
|
112
|
+
parseConfig(output);
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
export async function writeConfig(changes, path = configPath()) {
|
|
116
|
+
const original = await readOptional(path);
|
|
117
|
+
const updated = editConfig(original ?? "", changes);
|
|
118
|
+
// Unsetting a missing key must not create a new configuration file.
|
|
119
|
+
if (original === undefined && [...changes.values()].every((value) => value === undefined))
|
|
120
|
+
return;
|
|
121
|
+
if (original === updated)
|
|
122
|
+
return;
|
|
123
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
124
|
+
const lockPath = `${path}.lock`;
|
|
125
|
+
let lock;
|
|
126
|
+
try {
|
|
127
|
+
lock = await open(lockPath, "wx", 0o600);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`无法锁定配置文件,请确认没有其他写入进程:${lockPath}`);
|
|
131
|
+
}
|
|
132
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
133
|
+
try {
|
|
134
|
+
if ((await readOptional(path)) !== original)
|
|
135
|
+
throw new Error("配置文件已被其他进程修改,请重试。");
|
|
136
|
+
if (original !== undefined && (await lstat(path)).isSymbolicLink())
|
|
137
|
+
throw new Error("配置文件是符号链接,请直接编辑其目标文件。");
|
|
138
|
+
const file = await open(temporary, "wx", 0o600);
|
|
139
|
+
try {
|
|
140
|
+
await file.writeFile(updated, "utf8");
|
|
141
|
+
await file.sync();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
await file.close();
|
|
145
|
+
}
|
|
146
|
+
if ((await readOptional(path)) !== original)
|
|
147
|
+
throw new Error("配置文件已被其他进程修改,请重试。");
|
|
148
|
+
await rename(temporary, path);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await unlink(temporary).catch(() => { });
|
|
152
|
+
await lock.close();
|
|
153
|
+
await unlink(lockPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function formatConfig(config) {
|
|
157
|
+
const copy = { ...config };
|
|
158
|
+
if (copy.proxy) {
|
|
159
|
+
const url = new URL(copy.proxy);
|
|
160
|
+
if (url.username || url.password) {
|
|
161
|
+
url.username = "REDACTED";
|
|
162
|
+
url.password = "REDACTED";
|
|
163
|
+
copy.proxy = url.toString();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return stringify(copy);
|
|
167
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentDefinition } from "../schema.js";
|
|
2
|
+
import type { NpmAgentInstaller } from "../installation/types.js";
|
|
2
3
|
export interface DetectionContext {
|
|
3
4
|
env: NodeJS.ProcessEnv;
|
|
4
5
|
platform: NodeJS.Platform;
|
|
@@ -7,4 +8,5 @@ export interface DetectionContext {
|
|
|
7
8
|
export interface AgentProvider {
|
|
8
9
|
definition: AgentDefinition;
|
|
9
10
|
detect(context: DetectionContext): Promise<string | null>;
|
|
11
|
+
installer?: NpmAgentInstaller;
|
|
10
12
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { listAgents, type ListAgentsOptions } from "./list-agents.js";
|
|
2
2
|
export { agentDefinitionSchema, agentResultSchema, agentListSchema, type AgentDefinition, type AgentResult } from "./schema.js";
|
|
3
3
|
export { configSchema, configKeySchema, proxyUrlSchema, type ConfigInput, type Config, type ConfigKey } from "./config/schema.js";
|
|
4
|
+
export { configPath, readConfig, writeConfig, parseAssignments, formatConfig, type ConfigChanges } from "./config/store.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { listAgents } from "./list-agents.js";
|
|
2
2
|
export { agentDefinitionSchema, agentResultSchema, agentListSchema } from "./schema.js";
|
|
3
3
|
export { configSchema, configKeySchema, proxyUrlSchema } from "./config/schema.js";
|
|
4
|
+
export { configPath, readConfig, writeConfig, parseAssignments, formatConfig } from "./config/store.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Config } from "../config/schema.js";
|
|
2
|
+
import type { DetectionContext } from "../detection/types.js";
|
|
3
|
+
import { type NpmRunner } from "./process.js";
|
|
4
|
+
export declare const npmRegistry = "https://registry.npmjs.org/";
|
|
5
|
+
export declare const selfRoot: string;
|
|
6
|
+
export interface ManageOptions {
|
|
7
|
+
target: "self" | string;
|
|
8
|
+
action: "install" | "update";
|
|
9
|
+
config: Config;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
proxy?: string;
|
|
12
|
+
reinstall?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface InstallRuntime {
|
|
15
|
+
context: DetectionContext;
|
|
16
|
+
selfRoot: string;
|
|
17
|
+
runNpm: NpmRunner;
|
|
18
|
+
}
|
|
19
|
+
export interface InstallResult {
|
|
20
|
+
status: "planned" | "skipped" | "installed" | "updated";
|
|
21
|
+
target: string;
|
|
22
|
+
packageName: string;
|
|
23
|
+
from: string | null;
|
|
24
|
+
to: string;
|
|
25
|
+
prefix: string;
|
|
26
|
+
proxy: string | null;
|
|
27
|
+
args: string[];
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function managePackage(options: ManageOptions, suppliedRuntime?: InstallRuntime): Promise<InstallResult>;
|
|
31
|
+
export type UpdateTarget = {
|
|
32
|
+
kind: "self";
|
|
33
|
+
} | {
|
|
34
|
+
kind: "agent";
|
|
35
|
+
id: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "all-agents";
|
|
38
|
+
};
|
|
39
|
+
export declare function resolveUpdateTarget(options: {
|
|
40
|
+
self?: boolean;
|
|
41
|
+
agent?: string;
|
|
42
|
+
allAgents?: boolean;
|
|
43
|
+
}): UpdateTarget;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { access, lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { builtInAgents } from "../agents/index.js";
|
|
7
|
+
import { installationEnvironment, redact } from "../proxy/environment.js";
|
|
8
|
+
import { npmRunner } from "./process.js";
|
|
9
|
+
export const npmRegistry = "https://registry.npmjs.org/";
|
|
10
|
+
const registryArgument = `--registry=${npmRegistry}`;
|
|
11
|
+
const versionSchema = z.string().refine((value) => semver.valid(value) !== null, "无效的包版本");
|
|
12
|
+
const metadataSchema = z.object({ name: z.string(), version: versionSchema });
|
|
13
|
+
export const selfRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
14
|
+
async function metadata(directory, expectedName) {
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
text = await readFile(join(directory, "package.json"), "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === "ENOENT")
|
|
21
|
+
return null;
|
|
22
|
+
throw new Error(`无法读取已安装的包信息:${directory}`);
|
|
23
|
+
}
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = metadataSchema.parse(JSON.parse(text));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`已安装的包信息无效:${directory}`);
|
|
30
|
+
}
|
|
31
|
+
if (value.name !== expectedName)
|
|
32
|
+
throw new Error(`安装位置的包名称不匹配:${directory}`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function samePath(a, b, platform) {
|
|
36
|
+
return platform === "win32" ? resolve(a).toLowerCase() === resolve(b).toLowerCase() : resolve(a) === resolve(b);
|
|
37
|
+
}
|
|
38
|
+
export async function managePackage(options, suppliedRuntime) {
|
|
39
|
+
const env = installationEnvironment(options.config, options.proxy);
|
|
40
|
+
const runtime = suppliedRuntime ?? {
|
|
41
|
+
context: { env, cwd: process.cwd(), platform: process.platform },
|
|
42
|
+
selfRoot, runNpm: npmRunner(env),
|
|
43
|
+
};
|
|
44
|
+
const isSelf = options.target === "self";
|
|
45
|
+
const provider = builtInAgents.find((provider) => provider.definition.id === options.target);
|
|
46
|
+
if (!isSelf && !provider?.installer)
|
|
47
|
+
throw new Error("当前仅支持安装和更新 codex。其他 agent 暂时只支持 list 检测。");
|
|
48
|
+
if (isSelf && options.action === "install")
|
|
49
|
+
throw new Error("工具自身请使用 update 或 update --self。");
|
|
50
|
+
const packageName = isSelf ? "ai-agent-helper" : provider.installer.packageName;
|
|
51
|
+
const entryPoint = isSelf ? "dist/cli.js" : provider.installer.entryPoint;
|
|
52
|
+
const [root, prefix] = await Promise.all([runtime.runNpm(["root", "--global"]), runtime.runNpm(["prefix", "--global"])]);
|
|
53
|
+
if (!root || !prefix)
|
|
54
|
+
throw new Error("无法确定 npm 全局安装位置。");
|
|
55
|
+
const directory = join(root, ...packageName.split("/"));
|
|
56
|
+
const installed = await metadata(directory, packageName);
|
|
57
|
+
const detected = isSelf ? null : await provider.detect(runtime.context);
|
|
58
|
+
const command = provider?.definition.command ?? "agent-helper";
|
|
59
|
+
const expectedBin = runtime.context.platform === "win32" ? prefix : join(prefix, "bin");
|
|
60
|
+
const executableNames = runtime.context.platform === "win32" ? [command, `${command}.cmd`, `${command}.ps1`] : [command];
|
|
61
|
+
let detectedInPrefix = detected === null || executableNames
|
|
62
|
+
.some((name) => samePath(detected, join(expectedBin, name), runtime.context.platform));
|
|
63
|
+
if (detected !== null && detectedInPrefix && installed && runtime.context.platform !== "win32") {
|
|
64
|
+
// npm's POSIX entry is a symlink. A same-directory Homebrew/standalone link is a different install.
|
|
65
|
+
try {
|
|
66
|
+
detectedInPrefix = samePath(await realpath(detected), await realpath(join(directory, entryPoint)), runtime.context.platform);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
detectedInPrefix = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const linked = installed !== null && (await lstat(directory)).isSymbolicLink();
|
|
73
|
+
let blocked;
|
|
74
|
+
if (isSelf) {
|
|
75
|
+
if (!installed || linked || !samePath(await realpath(directory), await realpath(runtime.selfRoot), runtime.context.platform)) {
|
|
76
|
+
blocked = "当前 helper 不是此 npm 全局目录中的普通安装(可能是源码、npm link、npx 或其他 prefix),请通过原安装方式更新。";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (linked || !detectedInPrefix || (detected !== null && installed === null)) {
|
|
80
|
+
blocked = "Codex 来自其他目录、安装渠道或 npm link,请使用原安装方式更新,避免产生重复安装。";
|
|
81
|
+
}
|
|
82
|
+
const base = {
|
|
83
|
+
target: options.target, packageName, from: installed?.version ?? null, to: "latest", prefix,
|
|
84
|
+
proxy: (options.proxy ?? options.config.proxy) ? redact(options.proxy ?? options.config.proxy) : null,
|
|
85
|
+
args: ["install", "--global", `${packageName}@latest`, "--prefix", prefix, registryArgument],
|
|
86
|
+
};
|
|
87
|
+
if (options.action === "install" && !options.reinstall && (installed || detected)) {
|
|
88
|
+
return { ...base, status: "skipped", message: "已安装,跳过;需要重新安装时使用 --reinstall。" };
|
|
89
|
+
}
|
|
90
|
+
if (!isSelf && options.action === "update" && !installed && !detected)
|
|
91
|
+
throw new Error("Codex 尚未安装,请先运行 agent-helper install codex。");
|
|
92
|
+
if (options.dryRun)
|
|
93
|
+
return { ...base, status: "planned", message: blocked ?? "仅预览;未联网查询版本,未执行安装。执行时解析 latest 并固定版本。" };
|
|
94
|
+
if (blocked)
|
|
95
|
+
throw new Error(blocked);
|
|
96
|
+
const response = await runtime.runNpm(["view", `${packageName}@latest`, "version", "--json", registryArgument]);
|
|
97
|
+
let latest;
|
|
98
|
+
try {
|
|
99
|
+
latest = versionSchema.parse(JSON.parse(response));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error("npm 返回的 latest 版本无效。");
|
|
103
|
+
}
|
|
104
|
+
const args = ["install", "--global", `${packageName}@${latest}`, "--prefix", prefix, registryArgument];
|
|
105
|
+
if (options.action === "update" && installed && semver.gte(installed.version, latest)) {
|
|
106
|
+
return { ...base, args, to: latest, status: "skipped", message: "当前版本已是最新版或更新的版本。" };
|
|
107
|
+
}
|
|
108
|
+
await runtime.runNpm(args, true);
|
|
109
|
+
const after = await metadata(directory, packageName);
|
|
110
|
+
if (after?.version !== latest)
|
|
111
|
+
throw new Error("npm 已退出,但目标目录的包版本验证失败。");
|
|
112
|
+
try {
|
|
113
|
+
await access(join(directory, entryPoint));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error("包版本已写入,但命令入口缺失,安装验证失败。");
|
|
117
|
+
}
|
|
118
|
+
const message = isSelf ? "工具已更新,请重新运行 agent-helper。"
|
|
119
|
+
: detected === null ? `安装已完成;命令目录为 ${expectedBin},请确认它在 PATH 中并重新打开终端。` : "Codex 包版本及命令入口验证通过。";
|
|
120
|
+
return { ...base, args, to: latest, status: options.action === "install" ? "installed" : "updated", message };
|
|
121
|
+
}
|
|
122
|
+
export function resolveUpdateTarget(options) {
|
|
123
|
+
if (Number(options.self === true) + Number(options.agent !== undefined) + Number(options.allAgents === true) > 1) {
|
|
124
|
+
throw new Error("--self、--agent、--all-agents 不能同时使用。");
|
|
125
|
+
}
|
|
126
|
+
if (options.agent !== undefined) {
|
|
127
|
+
if (options.agent !== "codex")
|
|
128
|
+
throw new Error("当前仅支持更新 codex。");
|
|
129
|
+
return { kind: "agent", id: options.agent };
|
|
130
|
+
}
|
|
131
|
+
return options.allAgents ? { kind: "all-agents" } : { kind: "self" };
|
|
132
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { redact } from "../proxy/environment.js";
|
|
4
|
+
export function npmRunner(env) {
|
|
5
|
+
const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => key.toLowerCase() !== "npm_config_update_notifier"));
|
|
6
|
+
childEnv.npm_config_update_notifier = "false";
|
|
7
|
+
return (args, stream = false) => new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn("npm", args, { env: childEnv, stdio: ["inherit", "pipe", "pipe"], windowsHide: true });
|
|
9
|
+
let stdout = "";
|
|
10
|
+
let stderr = "";
|
|
11
|
+
let timedOut = false;
|
|
12
|
+
let oversized = false;
|
|
13
|
+
const forwardInterrupt = () => child.kill("SIGINT");
|
|
14
|
+
const forwardTerminate = () => child.kill("SIGTERM");
|
|
15
|
+
process.once("SIGINT", forwardInterrupt);
|
|
16
|
+
process.once("SIGTERM", forwardTerminate);
|
|
17
|
+
const timer = setTimeout(() => { timedOut = true; child.kill(); }, stream ? 600_000 : 60_000);
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
process.removeListener("SIGINT", forwardInterrupt);
|
|
21
|
+
process.removeListener("SIGTERM", forwardTerminate);
|
|
22
|
+
};
|
|
23
|
+
const wire = (readable, destination, output) => {
|
|
24
|
+
const decoder = new StringDecoder("utf8");
|
|
25
|
+
let pending = "";
|
|
26
|
+
readable.on("data", (chunk) => {
|
|
27
|
+
const text = decoder.write(chunk);
|
|
28
|
+
if (output === "stdout")
|
|
29
|
+
stdout += text;
|
|
30
|
+
else
|
|
31
|
+
stderr += text;
|
|
32
|
+
if (stdout.length + stderr.length > 4_000_000) {
|
|
33
|
+
oversized = true;
|
|
34
|
+
child.kill();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!stream)
|
|
38
|
+
return;
|
|
39
|
+
pending += text;
|
|
40
|
+
const end = pending.lastIndexOf("\n");
|
|
41
|
+
if (end >= 0) {
|
|
42
|
+
destination.write(redact(pending.slice(0, end + 1)));
|
|
43
|
+
pending = pending.slice(end + 1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
readable.on("end", () => { if (stream)
|
|
47
|
+
destination.write(redact(pending + decoder.end())); });
|
|
48
|
+
};
|
|
49
|
+
wire(child.stdout, process.stdout, "stdout");
|
|
50
|
+
wire(child.stderr, process.stderr, "stderr");
|
|
51
|
+
child.once("error", () => { cleanup(); reject(new Error("无法启动 npm,请确认 npm 已安装并位于 PATH 中。")); });
|
|
52
|
+
child.once("close", (code, signal) => {
|
|
53
|
+
cleanup();
|
|
54
|
+
if (timedOut)
|
|
55
|
+
reject(new Error("npm 操作超时。"));
|
|
56
|
+
else if (oversized)
|
|
57
|
+
reject(new Error("npm 输出超出限制。"));
|
|
58
|
+
else if (code !== 0)
|
|
59
|
+
reject(new Error(`npm 操作失败(${signal ?? code})。${stream ? "" : `\n${redact(stderr).trim()}`}`));
|
|
60
|
+
else
|
|
61
|
+
resolve(stdout.trim());
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { proxyUrlSchema } from "../config/schema.js";
|
|
2
|
+
export function installationEnvironment(config, override, base = process.env) {
|
|
3
|
+
const proxy = override ?? config.proxy;
|
|
4
|
+
const env = { ...base };
|
|
5
|
+
if (proxy === undefined)
|
|
6
|
+
return env;
|
|
7
|
+
proxyUrlSchema.parse(proxy);
|
|
8
|
+
const replaced = new Set(["http_proxy", "https_proxy", "all_proxy", "npm_config_proxy", "npm_config_https_proxy"]);
|
|
9
|
+
if (config.no_proxy.length)
|
|
10
|
+
for (const name of ["no_proxy", "npm_config_noproxy"])
|
|
11
|
+
replaced.add(name);
|
|
12
|
+
for (const name of Object.keys(env))
|
|
13
|
+
if (replaced.has(name.toLowerCase()))
|
|
14
|
+
delete env[name];
|
|
15
|
+
Object.assign(env, {
|
|
16
|
+
HTTP_PROXY: proxy, HTTPS_PROXY: proxy, http_proxy: proxy, https_proxy: proxy,
|
|
17
|
+
npm_config_proxy: proxy, npm_config_https_proxy: proxy,
|
|
18
|
+
});
|
|
19
|
+
if (config.no_proxy.length) {
|
|
20
|
+
const bypass = config.no_proxy.join(",");
|
|
21
|
+
Object.assign(env, { NO_PROXY: bypass, no_proxy: bypass, npm_config_noproxy: bypass });
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
export function redact(text) {
|
|
26
|
+
return text.replace(/https?:\/\/[^\s<>"']+/gi, (value) => {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
if (!url.username && !url.password)
|
|
30
|
+
return value;
|
|
31
|
+
return `${url.protocol}//REDACTED:REDACTED@${url.host}${url.pathname}${url.search}${url.hash}`;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return value.replace(/\/\/[^/]*@/, "//REDACTED@");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
package/docs/configuration.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 配置功能
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
从 `0.2.0` 起提供 TOML 配置读写、`config` 命令、Codex 安装/更新及工具自身更新。
|
|
4
4
|
|
|
5
5
|
## 文件位置与格式
|
|
6
6
|
|
|
@@ -47,10 +47,13 @@ const defaults = configSchema.parse({});
|
|
|
47
47
|
agent-helper config
|
|
48
48
|
|
|
49
49
|
# 设置默认安装代理
|
|
50
|
-
agent-helper config set proxy
|
|
50
|
+
agent-helper config set proxy=http://127.0.0.1:7890
|
|
51
51
|
|
|
52
|
-
#
|
|
53
|
-
agent-helper config set no_proxy
|
|
52
|
+
# 数组在命令中用逗号分隔,文件中保存为 TOML 数组
|
|
53
|
+
agent-helper config set no_proxy=localhost,127.0.0.1,::1
|
|
54
|
+
|
|
55
|
+
# 同时设置多个字段,全部验证通过后才写入
|
|
56
|
+
agent-helper config set proxy=http://127.0.0.1:7890 no_proxy=localhost,127.0.0.1
|
|
54
57
|
|
|
55
58
|
# 删除代理配置,恢复继承安装工具原有配置
|
|
56
59
|
agent-helper config unset proxy
|
|
@@ -59,7 +62,7 @@ agent-helper config unset proxy
|
|
|
59
62
|
agent-helper config unset no_proxy
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
可写配置键由 `configKeySchema` 限定为 `proxy` 和 `no_proxy
|
|
65
|
+
可写配置键由 `configKeySchema` 限定为 `proxy` 和 `no_proxy`。每个赋值只按第一个等号拆分;代理密码中的等号会保留。重复键、未知键、空值及无效类型会报错。`no_proxy` 要求至少一个条目,需要恢复默认空数组时使用 `unset`。写入前校验修改后的整份配置,而不只是当前字段。`config --help` 列出配置路径、键类型和示例。
|
|
63
66
|
|
|
64
67
|
`config` 展示配置文件中的值及内置默认值,不将安装工具或当前环境中的代理误标成已保存配置。代理认证信息必须脱敏,包括用户名与密码;`--json` 等将来新增的展示方式也遵循此规则。
|
|
65
68
|
|
|
@@ -73,16 +76,17 @@ agent-helper config unset no_proxy
|
|
|
73
76
|
|
|
74
77
|
前两项命中时,安装适配器为本次子进程设置对应代理参数及环境变量,不修改系统代理或安装工具的全局配置。此时,非空 `no_proxy` 明确替换工具继承的绕过规则;空数组不注入绕过规则,保留工具已有设置。代理都未显式指定时,不单独应用 helper 的 `no_proxy`。
|
|
75
78
|
|
|
76
|
-
`no_proxy`
|
|
79
|
+
当前安装器仅支持 npm:`no_proxy` 转换为逗号分隔的 `NO_PROXY` / `no_proxy` 和 `npm_config_noproxy`,具体规则的匹配行为由 npm 决定。模型验证非空、无逗号的条目,不宣称各种工具对通配符或 CIDR 的语义一致。首版代理 URL 仅支持 HTTP/HTTPS。
|
|
77
80
|
|
|
78
81
|
## 读写要求
|
|
79
82
|
|
|
80
83
|
- 文件不存在时返回内置默认值;只查看配置不会创建目录或文件。
|
|
81
84
|
- 第一次 `set` 才创建目录与配置文件,写入 `version = 1`。
|
|
82
85
|
- 文件存在但 TOML 语法或数据校验失败时,报告文件位置和字段错误,停止修改,不覆盖原文件。
|
|
83
|
-
-
|
|
84
|
-
- `set` / `unset`
|
|
85
|
-
-
|
|
86
|
+
- 错误信息和展示结果对代理 URL 中的账号与密码脱敏。文件保存真实 URL,初次创建使用当前用户私有权限(POSIX 为目录 0700、文件 0600;Windows 实际访问权限由系统 ACL 决定)。
|
|
87
|
+
- `set` / `unset` 根据 TOML AST 的范围修改目标值,保留注释、字段顺序和换行风格;多行数组内的注释也保留。`config` 输出的是经过脱敏并补齐默认值的 TOML,不复刻原文件排版。
|
|
88
|
+
- 使用独占 `.lock` 文件协调 helper 写入,在同目录写临时文件并原子替换;写入前检查原文是否变化。锁文件存在时停止操作;若进程异常退出留下锁,确认没有写入进程后可手工删除锁文件。外部编辑器不参与 helper 的锁,原文检查不能彻底消除所有外部写入竞态。
|
|
89
|
+
- 配置文件为符号链接时允许读取,但拒绝通过 helper 修改,提示直接编辑目标文件。
|
|
86
90
|
- 未知字段、未知版本的文件可以由用户手动维护,但当前版本拒绝修改,保留原文。
|
|
87
91
|
|
|
88
92
|
文件格式版本与 npm 包版本分开维护。后续确有需求时,再为安装器参数或 agent 专属配置添加 TOML 分组及迁移规则。
|
package/examples/config.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-agent-helper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"description": "Discover
|
|
8
|
+
"description": "Discover AI agent CLIs, manage TOML configuration, and install or update Codex through a proxy.",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/tool-helper/ai-agent-helper.git"
|
|
@@ -53,10 +53,16 @@
|
|
|
53
53
|
"typescript"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"cross-spawn": "^7.0.6",
|
|
57
|
+
"semver": "^7.8.6",
|
|
58
|
+
"smol-toml": "^1.9.1",
|
|
59
|
+
"toml-eslint-parser": "^0.10.1",
|
|
56
60
|
"zod": "^4.6.7"
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
63
|
+
"@types/cross-spawn": "^6.0.6",
|
|
59
64
|
"@types/node": "^22.20.5",
|
|
65
|
+
"@types/semver": "^7.8.0",
|
|
60
66
|
"tsx": "^4.23.15",
|
|
61
67
|
"typescript": "^7.0.2"
|
|
62
68
|
}
|