@olierjs/fugue-cli 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +49 -2
- package/THIRD_PARTY_NOTICES +416 -0
- package/bin/fugue-cli.mjs +23 -0
- package/dist/darwin-arm64/fugue-cli +0 -0
- package/dist/darwin-x64/fugue-cli +0 -0
- package/dist/linux-arm64/fugue-cli +0 -0
- package/dist/linux-x64/fugue-cli +0 -0
- package/dist/win32-arm64/fugue-cli.exe +0 -0
- package/dist/win32-x64/fugue-cli.exe +0 -0
- package/package.json +26 -4
- package/scripts/postinstall.mjs +13 -0
- package/scripts/skills.mjs +104 -0
- package/skills/fugue-stack/SKILL.md +10 -0
- package/skills/plan-with-remote/SKILL.md +60 -0
- package/skills/plan-with-remote/references/configuration.md +43 -0
- package/skills/plan-with-remote/scripts/client.mjs +61 -0
- package/skills/plan-with-remote/scripts/config.mjs +53 -0
- package/skills/plan-with-remote/scripts/planning.mjs +279 -0
- package/skills/plan-with-remote/scripts/protocol.mjs +147 -0
- package/skills/plan-with-remote/scripts/service.mjs +146 -0
- package/skills/plan-with-remote/scripts/storage.mjs +74 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
6
|
+
const supported = new Set(['darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64']);
|
|
7
|
+
|
|
8
|
+
if (!supported.has(platform)) {
|
|
9
|
+
console.error(`fugue-cli 不支持当前平台:${platform}`);
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
} else {
|
|
12
|
+
const filename = process.platform === 'win32' ? 'fugue-cli.exe' : 'fugue-cli';
|
|
13
|
+
const binary = fileURLToPath(new URL(`../dist/${platform}/${filename}`, import.meta.url));
|
|
14
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
15
|
+
if (result.error) {
|
|
16
|
+
console.error(`无法运行 fugue-cli,请重新安装 @olierjs/fugue-cli:${result.error.message}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
} else if (result.signal) {
|
|
19
|
+
process.kill(process.pid, result.signal);
|
|
20
|
+
} else {
|
|
21
|
+
process.exitCode = result.status ?? 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olierjs/fugue-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fugue 需求交付工作指南与 Agent 技能",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "fugue-cli": "bin/fugue-cli.mjs" },
|
|
7
|
+
"files": [
|
|
8
|
+
"bin/",
|
|
9
|
+
"dist/",
|
|
10
|
+
"scripts/postinstall.mjs",
|
|
11
|
+
"scripts/skills.mjs",
|
|
12
|
+
"skills/",
|
|
13
|
+
"THIRD_PARTY_NOTICES"
|
|
14
|
+
],
|
|
15
|
+
"engines": { "node": ">=22.19.0" },
|
|
16
|
+
"os": ["darwin", "linux", "win32"],
|
|
17
|
+
"cpu": ["x64", "arm64"],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"repository": { "type": "git", "url": "git+https://github.com/TbhT/fugue-stack.git" },
|
|
20
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test tests/*.test.mjs",
|
|
23
|
+
"test:go": "go test ./...",
|
|
24
|
+
"build": "node scripts/build.mjs",
|
|
25
|
+
"prepack": "node scripts/build.mjs",
|
|
26
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { syncSkills } from './skills.mjs';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
const result = await syncSkills({ sourceDirectory: fileURLToPath(new URL('../skills/', import.meta.url)), version });
|
|
8
|
+
console.log(`fugue-cli:技能已同步到 ${result.directory}`);
|
|
9
|
+
if (result.backupDirectory) console.log(`旧技能备份:${result.backupDirectory}`);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(`fugue-cli 技能同步失败:${error.message}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const owner = '@olierjs/fugue-cli';
|
|
7
|
+
const marker = '.fugue-cli.json';
|
|
8
|
+
|
|
9
|
+
async function exists(path) {
|
|
10
|
+
try { return await lstat(path); }
|
|
11
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function managed(path) {
|
|
15
|
+
if (!(await exists(path))?.isDirectory()) return false;
|
|
16
|
+
try { return JSON.parse(await readFile(join(path, marker), 'utf8')).owner === owner; }
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (['ENOENT', 'ENOTDIR'].includes(error.code) || error instanceof SyntaxError) return false;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function digest(path) {
|
|
24
|
+
const hash = createHash('sha256');
|
|
25
|
+
async function visit(current, relative) {
|
|
26
|
+
const info = await lstat(current);
|
|
27
|
+
hash.update(JSON.stringify([relative, info.mode & 0o777]));
|
|
28
|
+
if (info.isSymbolicLink()) hash.update(JSON.stringify(['link', await readlink(current)]));
|
|
29
|
+
else if (info.isDirectory()) {
|
|
30
|
+
hash.update('directory');
|
|
31
|
+
for (const name of (await readdir(current)).sort()) await visit(join(current, name), `${relative}/${name}`);
|
|
32
|
+
} else if (info.isFile()) {
|
|
33
|
+
hash.update('file');
|
|
34
|
+
hash.update(await readFile(current));
|
|
35
|
+
} else throw new Error(`技能包含不支持的文件类型:${current}`);
|
|
36
|
+
}
|
|
37
|
+
await visit(path, '');
|
|
38
|
+
return hash.digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function syncSkills({ sourceDirectory, version, homeDirectory = homedir() }) {
|
|
42
|
+
const directory = join(homeDirectory, '.agents', 'skills');
|
|
43
|
+
const storage = join(homeDirectory, '.local', 'share', 'fugue-cli');
|
|
44
|
+
const batch = randomUUID();
|
|
45
|
+
const staging = join(storage, `.staging-${batch}`);
|
|
46
|
+
const backupDirectory = join(storage, 'backups', batch);
|
|
47
|
+
const names = (await readdir(sourceDirectory, { withFileTypes: true })).filter(entry => entry.isDirectory()).map(entry => entry.name).sort();
|
|
48
|
+
if (!names.length) throw new Error('安装包没有可用技能。');
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || !(await exists(join(sourceDirectory, name, 'SKILL.md')))?.isFile()) {
|
|
51
|
+
throw new Error(`安装包技能无效:${name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await mkdir(directory, { recursive: true });
|
|
55
|
+
await mkdir(staging, { recursive: true });
|
|
56
|
+
const result = { directory, installed: [], removed: [], unchanged: [], backupDirectory: null };
|
|
57
|
+
const changed = [];
|
|
58
|
+
const stale = [];
|
|
59
|
+
const backedUp = [];
|
|
60
|
+
const installed = [];
|
|
61
|
+
try {
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const prepared = join(staging, name);
|
|
64
|
+
await cp(join(sourceDirectory, name), prepared, { recursive: true, dereference: false, verbatimSymlinks: true });
|
|
65
|
+
await writeFile(join(prepared, marker), JSON.stringify({ owner, version }) + '\n');
|
|
66
|
+
const target = join(directory, name);
|
|
67
|
+
if (await exists(target) && await digest(target) === await digest(prepared)) result.unchanged.push(name);
|
|
68
|
+
else changed.push(name);
|
|
69
|
+
}
|
|
70
|
+
for (const name of await readdir(directory)) {
|
|
71
|
+
if (!names.includes(name) && await managed(join(directory, name))) stale.push(name);
|
|
72
|
+
}
|
|
73
|
+
for (const name of [...changed, ...stale]) {
|
|
74
|
+
const target = join(directory, name);
|
|
75
|
+
if (await exists(target)) {
|
|
76
|
+
await mkdir(backupDirectory, { recursive: true });
|
|
77
|
+
await rename(target, join(backupDirectory, name));
|
|
78
|
+
backedUp.push(name);
|
|
79
|
+
result.backupDirectory = backupDirectory;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const name of changed) {
|
|
83
|
+
await rename(join(staging, name), join(directory, name));
|
|
84
|
+
installed.push(name);
|
|
85
|
+
}
|
|
86
|
+
result.installed = changed;
|
|
87
|
+
result.removed = stale;
|
|
88
|
+
return result;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const failures = [];
|
|
91
|
+
for (const name of installed.reverse()) {
|
|
92
|
+
try { await rm(join(directory, name), { recursive: true, force: true }); }
|
|
93
|
+
catch (rollbackError) { failures.push(rollbackError.message); }
|
|
94
|
+
}
|
|
95
|
+
for (const name of backedUp.reverse()) {
|
|
96
|
+
try { await rename(join(backupDirectory, name), join(directory, name)); }
|
|
97
|
+
catch (rollbackError) { failures.push(rollbackError.message); }
|
|
98
|
+
}
|
|
99
|
+
if (failures.length) throw new Error(`${error.message};恢复未完成,备份位于 ${backupDirectory}:${failures.join(';')}`, { cause: error });
|
|
100
|
+
throw error;
|
|
101
|
+
} finally {
|
|
102
|
+
await rm(staging, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fugue-stack
|
|
3
|
+
description: 开始或继续个人需求交付时,通过 fugue-cli 获取当前工作指南,选择并衔接规划、编码、代码审查与验证或实现解释;不用于纯闲聊和普通知识问答。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fugue Stack
|
|
7
|
+
|
|
8
|
+
每次开始或继续任务时,运行 `fugue-cli guide`,读取当前安装版本的总览,并遵循其中与本次用户目标对应的流程。
|
|
9
|
+
|
|
10
|
+
工作路由和具体要求由本次命令输出提供。按需读取其中指向的技能与材料,保留已有规划会话和产物,在本次授权范围内完成工作。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-with-remote
|
|
3
|
+
description: 当用户需要通过远端服务制定开发方案、生成执行和验收文档、查看规划进展、补充需求或审阅修改方案时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 远端规划
|
|
7
|
+
|
|
8
|
+
将用户需求交给规划服务,向用户呈现方案、待回答的问题和最终文档。规划交付不表示软件已经实现或验收。脚本由 Node 内置能力组成,不需要从 Reconcile 源码仓库构建或安装 npm 依赖。
|
|
9
|
+
|
|
10
|
+
## 开始
|
|
11
|
+
|
|
12
|
+
使用 Node 22.19.0 或更高版本及 Git。在目标 Git 仓库中调用:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
node <skill-root>/scripts/client.mjs --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
服务地址由 `RECONCILE_BASE_URL` 或 `--config` 指定;首次配置、切换服务或调整等待时间时阅读[配置说明](references/configuration.md)。仓库必须已在服务中登记;匹配失败时报告缺项。
|
|
19
|
+
|
|
20
|
+
先收集用户目标、范围、约束和需求来源。保留产品事实的原意;先读取来源正文,不把链接当成正文,也不把已有建议当成已批准决定。创建 `request.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"title": "修复切换对象后的状态显示",
|
|
25
|
+
"description": "切换到新对象后,旧请求的结果不能覆盖新对象的状态。保留正常加载行为。",
|
|
26
|
+
"sources": [{ "label": "用户补充", "content": "切换后应立即显示新对象的加载状态。" }]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`sources` 可省略。两种规划方式都生成执行文档与验收文档,并等待最终批准:
|
|
31
|
+
|
|
32
|
+
- `lightweight`:适合范围明确的局部改动,直接形成最终文档。
|
|
33
|
+
- `standard`:先审阅方案方向,用户认可后继续生成文档并进行独立评审。
|
|
34
|
+
|
|
35
|
+
按用户目标明确选择;不清楚是否需要先审方向时向用户确认。
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node <skill-root>/scripts/client.mjs plan --input request.json --mode lightweight
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 处理结果
|
|
42
|
+
|
|
43
|
+
脚本输出状态、文件路径、问题和下一步命令。保存返回的 `session` 和 `review` 路径,原样传回脚本;这些文件由脚本维护,无需打开、编辑或拼装其内容。
|
|
44
|
+
|
|
45
|
+
| 返回状态 | Agent 的下一步 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `running` | 使用 `read --session <路径>` 继续查看,不重复创建任务。 |
|
|
48
|
+
| `review_solution` | 阅读并呈现方案;用户认可后用 `accept --review <路径>`,有修改用 `feedback`。 |
|
|
49
|
+
| `review_documents` | 阅读执行文档、验收文档及返回的问题文件;用户明确批准后用 `approve --review <路径>`。 |
|
|
50
|
+
| `questions` | 向用户提出返回的问题,用 `answer --review <路径> --question <编号> --text <用户回答>` 提交。确认类问题在用户同意后使用 `--confirm`。 |
|
|
51
|
+
| `changes_needed` | 呈现待解决问题,按用户修改要求使用 `feedback`;不能批准。 |
|
|
52
|
+
| `submission_unconfirmed` | 用 `retry --session <路径>` 重试原请求,不另建任务。 |
|
|
53
|
+
| `failed` | 报告失败;需要重试时用 `retry --session <路径>`。 |
|
|
54
|
+
| `approved` / `stopped` | 交付返回的文档,或报告规划已停止。 |
|
|
55
|
+
|
|
56
|
+
反馈用 `feedback --review <路径> --scope <范围> --change <修改要求> --preserve <保留要求>`。范围选需求 `requirements`、方案 `solution` 或验收设计 `verification`;方案总览还支持仅改表达的 `wording`。具体可用范围见 `--help`。
|
|
57
|
+
|
|
58
|
+
决定来源通过配置或 `--actor` 提供。认可、批准、停止和确认重新规划必须有用户明确授权。已获得且仍适用于本次文档的授权不重复询问。版本变化时重新呈现,不将旧决定用于新文档。
|
|
59
|
+
|
|
60
|
+
阅读并解释返回的正式文档。计划中的实现、测试或效果保持为计划,不写成已完成事实。文档可以提交审阅,不代表用户已批准。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 配置
|
|
2
|
+
|
|
3
|
+
运行时无需安装 npm 依赖。需要 Node >=22.19.0、Git、可访问的规划服务,以及已登记到该服务的目标仓库。
|
|
4
|
+
|
|
5
|
+
配置优先级:命令行参数 > 环境变量 > `--config` 指定的 JSON 文件 > 默认值。也可用 `RECONCILE_CLIENT_CONFIG` 指定配置文件路径。不会自动读取或执行 `.env` 文件。
|
|
6
|
+
|
|
7
|
+
| JSON 字段 | 环境变量 | 命令行 | 默认值 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| `baseUrl` | `RECONCILE_BASE_URL` | `--base-url` | 必须配置,无内置 IP |
|
|
10
|
+
| `remote` | `RECONCILE_REMOTE` | `plan --remote` | `origin` |
|
|
11
|
+
| `agent` | `RECONCILE_AGENT` | `plan --agent` | 服务中配置的项目默认客户端 |
|
|
12
|
+
| `actor` | `RECONCILE_ACTOR` | 决策、反馈、回答命令的 `--actor` | 无;需明确提供 |
|
|
13
|
+
| `waitMs` | `RECONCILE_WAIT_MS` | `--wait-ms` | `60000` |
|
|
14
|
+
| `requestTimeoutMs` | `RECONCILE_REQUEST_TIMEOUT_MS` | `--request-timeout-ms` | `30000` |
|
|
15
|
+
|
|
16
|
+
例如将配置存入仓库外的 `reconcile.config.json`,把示例域名换成实际服务地址:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"baseUrl": "https://planning.example.com",
|
|
21
|
+
"remote": "origin",
|
|
22
|
+
"actor": "user:your-name",
|
|
23
|
+
"waitMs": 60000,
|
|
24
|
+
"requestTimeoutMs": 30000
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`actor` 只声明决定来源,不代表登录或身份认证,不要编造真实人员身份。`agent` 仅在明确需要覆盖项目默认值时配置,可选 `traex` 或 `grok-build`,创建后不可切换。
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
node <skill-root>/scripts/client.mjs plan \
|
|
32
|
+
--config /absolute/path/reconcile.config.json \
|
|
33
|
+
--input request.json --mode standard \
|
|
34
|
+
--output-dir /absolute/path/planning-output
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
默认输出目录为目标仓库的 `.plan-with-remote/<任务编号>/`。可用 `--output-dir` 指定空目录;不要把需求材料、输出或配置提交到公开仓库。后续命令直接使用返回的绝对路径。
|
|
38
|
+
|
|
39
|
+
任务会记住创建时的服务地址;继续任务不需要重复配置地址。显式配置了不同地址时命令会拒绝执行,不能把同一个任务切换到其他服务。
|
|
40
|
+
|
|
41
|
+
`waitMs` 限制本轮等待,结束后返回当前进展;`read` 可以继续等待,远端任务不会因此停止。每次 HTTP 请求另外受 `requestTimeoutMs` 限制。网络结果不明确时保留任务文件,用 `retry` 重试同一个请求。
|
|
42
|
+
|
|
43
|
+
只有 `plan --dry-run` 提供本地输入预览,不访问服务、不创建任务;它不证明服务可达或仓库已登记。其他命令拒绝 `--dry-run`。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { configuration } from './config.mjs';
|
|
6
|
+
import { start, useSession } from './planning.mjs';
|
|
7
|
+
|
|
8
|
+
const commandOptions = {
|
|
9
|
+
plan: ['input', 'mode', 'output-dir', 'remote', 'agent', 'dry-run'],
|
|
10
|
+
read: ['session'], retry: ['session'],
|
|
11
|
+
accept: ['review', 'actor'], approve: ['review', 'actor'], stop: ['review', 'actor'],
|
|
12
|
+
answer: ['review', 'question', 'text', 'confirm', 'actor'],
|
|
13
|
+
feedback: ['review', 'scope', 'change', 'preserve', 'actor'],
|
|
14
|
+
};
|
|
15
|
+
const globalOptions = ['config', 'base-url', 'wait-ms', 'request-timeout-ms', 'help'];
|
|
16
|
+
const booleanOptions = new Set(['help', 'dry-run', 'confirm']);
|
|
17
|
+
const help = `用法:node <skill-root>/scripts/client.mjs <命令> [参数]
|
|
18
|
+
|
|
19
|
+
plan --input request.json --mode lightweight|standard [--output-dir 目录] [--dry-run]
|
|
20
|
+
read --session <返回的任务文件>
|
|
21
|
+
retry --session <任务文件> 重试原提交,或重新规划失败任务
|
|
22
|
+
accept --review <返回的审阅凭据> 用户认可方案,继续生成最终文档
|
|
23
|
+
approve --review <审阅凭据> 用户批准最终文档
|
|
24
|
+
stop --review <审阅凭据> 用户停止规划
|
|
25
|
+
answer --review <审阅凭据> --question <编号> --text <用户回答>
|
|
26
|
+
确认类问题使用 --confirm,代替 --text
|
|
27
|
+
feedback --review <审阅凭据> --scope <范围> --change <修改要求> --preserve <保留要求>
|
|
28
|
+
方案范围:requirements|solution|wording;最终文档:requirements|solution|verification
|
|
29
|
+
|
|
30
|
+
决定、反馈和回答需要 --actor <决定来源>,也可通过配置提供。
|
|
31
|
+
plan 支持 --remote <Git remote>、--agent traex|grok-build;通常使用项目默认客户端。
|
|
32
|
+
通用配置:--config <JSON 文件>、--base-url <URL>、--wait-ms <毫秒>、--request-timeout-ms <毫秒>。
|
|
33
|
+
配置优先级:命令行 > 环境变量 > 配置文件 > 内置默认值。配置说明见 references/configuration.md。
|
|
34
|
+
只有 plan 支持 --dry-run,且不会访问网络。其他命令拒绝此参数。
|
|
35
|
+
输出为 JSON:状态、文件路径、待回答问题和下一步命令。无需打开任务文件或审阅凭据。
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
39
|
+
const [command, ...args] = argv;
|
|
40
|
+
if (!command || command === '--help' || command === '-h') return { help };
|
|
41
|
+
if (!Object.hasOwn(commandOptions, command)) throw new Error('未知命令,请使用 --help。');
|
|
42
|
+
const options = Object.fromEntries([...globalOptions, ...commandOptions[command]].map(key => [key, { type: booleanOptions.has(key) ? 'boolean' : 'string' }]));
|
|
43
|
+
const { values } = parseArgs({ args, options, strict: true, allowPositionals: false });
|
|
44
|
+
if (values.help) return { help };
|
|
45
|
+
const config = await configuration(values, env);
|
|
46
|
+
return command === 'plan' ? start(values, config) : useSession(command, values, config);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
50
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
51
|
+
if (major < 22 || (major === 22 && minor < 19)) {
|
|
52
|
+
console.error('请使用 Node 22.19.0 或更高版本。');
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
} else {
|
|
55
|
+
main().then(result => console.log(result.help ?? JSON.stringify(result, null, 2))).catch(error => {
|
|
56
|
+
const message = error.message?.match(/[\u3400-\u9fff]/) ? error.message : '命令未完成。请检查参数或配置;已提交的任务请保留任务文件后继续。';
|
|
57
|
+
console.error(JSON.stringify({ status: 'error', message, ...(error.session ? { session: error.session } : {}), ...(error.diagnostic ? { diagnostic: error.diagnostic } : {}), ...(!error.session ? { detail: error.message } : {}) }, null, 2));
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const settings = {
|
|
5
|
+
baseUrl: ['base-url', 'RECONCILE_BASE_URL'],
|
|
6
|
+
remote: ['remote', 'RECONCILE_REMOTE', 'origin'],
|
|
7
|
+
agent: ['agent', 'RECONCILE_AGENT'],
|
|
8
|
+
actor: ['actor', 'RECONCILE_ACTOR'],
|
|
9
|
+
waitMs: ['wait-ms', 'RECONCILE_WAIT_MS', 60000],
|
|
10
|
+
requestTimeoutMs: ['request-timeout-ms', 'RECONCILE_REQUEST_TIMEOUT_MS', 30000],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function configuration(flags, env = process.env) {
|
|
14
|
+
const path = flags.config ?? env.RECONCILE_CLIENT_CONFIG;
|
|
15
|
+
const file = path ? JSON.parse(await readFile(resolve(path), 'utf8')) : {};
|
|
16
|
+
if (!file || typeof file !== 'object' || Array.isArray(file)) throw new Error('配置文件必须是 JSON 对象。');
|
|
17
|
+
for (const key of Object.keys(file)) {
|
|
18
|
+
if (!(key in settings)) throw new Error(`未知配置:${key}`);
|
|
19
|
+
}
|
|
20
|
+
const config = {};
|
|
21
|
+
for (const [key, [flag, variable, fallback]] of Object.entries(settings)) {
|
|
22
|
+
config[key] = flags[flag] ?? env[variable] ?? file[key] ?? fallback;
|
|
23
|
+
}
|
|
24
|
+
if (config.baseUrl !== undefined) {
|
|
25
|
+
const url = new URL(config.baseUrl);
|
|
26
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
27
|
+
throw new Error('服务地址必须是没有凭证、查询参数或片段的 HTTP(S) 地址。');
|
|
28
|
+
}
|
|
29
|
+
config.baseUrl = url.href.replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
for (const key of ['waitMs', 'requestTimeoutMs']) {
|
|
32
|
+
config[key] = Number(config[key]);
|
|
33
|
+
if (!Number.isSafeInteger(config[key]) || config[key] <= 0) throw new Error(`${key} 必须是正整数毫秒。`);
|
|
34
|
+
}
|
|
35
|
+
for (const key of ['remote', 'actor', 'agent']) {
|
|
36
|
+
if (config[key] !== undefined && (typeof config[key] !== 'string' || !config[key].trim())) {
|
|
37
|
+
throw new Error(`${key} 不能为空。`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (config.agent !== undefined && !['traex', 'grok-build'].includes(config.agent)) {
|
|
41
|
+
throw new Error('agent 只能是 traex 或 grok-build。');
|
|
42
|
+
}
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serviceAddress(config, session) {
|
|
47
|
+
if (session && config.baseUrl && config.baseUrl !== session.baseUrl) {
|
|
48
|
+
throw new Error('当前配置指向另一服务。继续原任务时请使用创建任务的服务地址。');
|
|
49
|
+
}
|
|
50
|
+
const url = session?.baseUrl ?? config.baseUrl;
|
|
51
|
+
if (!url) throw new Error('请设置 RECONCILE_BASE_URL,或通过 --config / --base-url 配置服务地址。');
|
|
52
|
+
return url;
|
|
53
|
+
}
|