@metagptx/deepflow-cli 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/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # DeepFlow CLI
2
+
3
+ Zero-dependency CLI wrapper for DeepFlow Web methods.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @metagptx/deepflow-cli
9
+ deepflow --help
10
+ deepflow -l -t <dfpat_token> --base-url https://deepflow.example.com
11
+ ```
12
+
13
+ `npx` is still useful for one-off execution, but it does not install
14
+ `deepflow` into your PATH for later shell sessions:
15
+
16
+ ```bash
17
+ npx @metagptx/deepflow-cli --help
18
+ npx --package @metagptx/deepflow-cli deepflow --help
19
+ ```
20
+
21
+ For local development from this repository:
22
+
23
+ ```bash
24
+ npm link
25
+ deepflow --help
26
+ node bin/deepflow.js --help
27
+ ```
28
+
29
+ ## Authenticate
30
+
31
+ ```bash
32
+ deepflow -l -t <dfpat_token>
33
+ ```
34
+
35
+ Equivalent forms:
36
+
37
+ ```bash
38
+ deepflow -login -t <dfpat_token>
39
+ deepflow --login -t <dfpat_token>
40
+ deepflow auth login -t <dfpat_token>
41
+ ```
42
+
43
+ Config is stored at:
44
+
45
+ ```text
46
+ ~/.deepflow/config.json
47
+ ```
48
+
49
+ ## Run Plan
50
+
51
+ ```bash
52
+ deepflow plan run \
53
+ --repo <repo_id_or_app_code> \
54
+ --name "Demo Iteration" \
55
+ --context "Plan this change" \
56
+ --prompt "Create an implementation plan only." \
57
+ --json
58
+ ```
59
+
60
+ `plan run --json` returns `statusUrl`, `messagesUrl`, and `stopUrl` so
61
+ external callers can poll or stop the created session without knowing
62
+ DeepFlow Web route details.
63
+
64
+ ## Checks
65
+
66
+ ```bash
67
+ npm run check
68
+ npm test
69
+ npm run smoke
70
+ npm run pack:dry-run
71
+ ```
72
+
73
+ Run the full Web/Core-backed smoke when a token and app are available:
74
+
75
+ ```bash
76
+ DEEPFLOW_CLI_E2E_TOKEN=<dfpat_token> \
77
+ DEEPFLOW_CLI_E2E_BASE_URL=http://localhost:3100 \
78
+ npm run smoke:e2e
79
+ ```
80
+
81
+ Useful environment variables:
82
+
83
+ ```bash
84
+ export DEEPFLOW_BASE_URL=http://localhost:3100
85
+ export DEEPFLOW_TOKEN=dfpat_xxx
86
+ ```
87
+
88
+ ## Publish
89
+
90
+ Validate the package contents before publishing:
91
+
92
+ ```bash
93
+ npm run check
94
+ npm test
95
+ npm run smoke
96
+ npm run pack:dry-run
97
+ ```
98
+
99
+ Publish to the configured npm registry:
100
+
101
+ ```bash
102
+ npm publish --access public
103
+ ```
104
+
105
+ For a private company registry, pass the registry explicitly:
106
+
107
+ ```bash
108
+ npm publish --registry <registry-url>
109
+ ```
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/cli.js";
4
+
5
+ const exitCode = await main(process.argv.slice(2));
6
+
7
+ process.exitCode = exitCode;
@@ -0,0 +1,365 @@
1
+ # DeepFlow CLI 设计文档
2
+
3
+ ## 背景
4
+
5
+ DeepFlow 需要提供一种面向外部系统的调用方式,但外部调用方不应直接耦合 Web/Core 的 HTTP API 细节。CLI 作为稳定入口,对外暴露“方法”语义:调用方执行 `deepflow <method>`,CLI 负责读取配置、鉴权、请求 DeepFlow Web API、处理响应和输出结构化结果。
6
+
7
+ 首版目标是支持外部创建一个迭代并启动 Plan,后续再逐步扩展查询、停止、继续执行、获取结果等能力。
8
+
9
+ ## 目标
10
+
11
+ 1. 提供 `deepflow` 命令行入口,供外部脚本、CI、平台服务调用。
12
+ 2. 支持全局配置文件读取 token 和服务地址。
13
+ 3. 支持通过 `deepflow -l/-login -t <token>` 完成 token 认证写入。
14
+ 4. 对外暴露稳定方法,不暴露底层 API 路径和请求体。
15
+ 5. 支持机器可读输出,便于外部系统解析执行结果。
16
+ 6. 所有调用默认携带 token,走 `Authorization: Bearer <token>`。
17
+
18
+ ## 非目标
19
+
20
+ 1. 首版不实现交互式登录 OAuth/OIDC。
21
+ 2. 首版不直接调用 DeepFlowCore 外部 API,统一走 DeepFlowWeb,复用用户级 token、权限和本地迭代能力。
22
+ 3. 首版不提供完整前端功能镜像,只封装外部自动化需要的核心方法。
23
+ 4. 首版不在 CLI 内保存 token 明文之外的服务端状态,token 生命周期仍由 Web token 管理页负责。
24
+
25
+ ## 命令入口
26
+
27
+ 命令名:
28
+
29
+ ```bash
30
+ deepflow
31
+ ```
32
+
33
+ 发布包名:
34
+
35
+ ```text
36
+ @metagptx/deepflow-cli
37
+ ```
38
+
39
+ 推荐外部调用方先全局安装,然后直接使用固定的 `deepflow` 命令:
40
+
41
+ ```bash
42
+ npm install -g @metagptx/deepflow-cli
43
+ deepflow --help
44
+ deepflow plan run --repo <repo_id_or_app_code> --json
45
+ ```
46
+
47
+ `npx` 只适合临时执行,不会把 `deepflow` 持久安装到 PATH。需要临时执行时可以使用:
48
+
49
+ ```bash
50
+ npx @metagptx/deepflow-cli --help
51
+ npx --package @metagptx/deepflow-cli deepflow --help
52
+ ```
53
+
54
+ 认证快捷入口:
55
+
56
+ ```bash
57
+ deepflow -l -t <token>
58
+ deepflow -login -t <token>
59
+ deepflow --login -t <token>
60
+ ```
61
+
62
+ 说明:
63
+
64
+ - `-l` 是短选项。
65
+ - `--login` 是标准长选项。
66
+ - `-login` 兼容用户明确提出的写法,内部等价于 `--login`。
67
+ - `-t, --token <token>` 指定 token。
68
+
69
+ 推荐也支持子命令形式,便于长期维护:
70
+
71
+ ```bash
72
+ deepflow auth login -t <token>
73
+ deepflow auth logout
74
+ deepflow auth status
75
+ ```
76
+
77
+ ## 全局配置
78
+
79
+ 配置文件路径:
80
+
81
+ ```text
82
+ ~/.deepflow/config.json
83
+ ```
84
+
85
+ 权限建议:
86
+
87
+ ```text
88
+ 0600
89
+ ```
90
+
91
+ 配置结构:
92
+
93
+ ```json
94
+ {
95
+ "baseUrl": "http://localhost:3100",
96
+ "token": "dfpat_xxx",
97
+ "profile": "default",
98
+ "profiles": {
99
+ "default": {
100
+ "baseUrl": "http://localhost:3100",
101
+ "token": "dfpat_xxx"
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ 首版可以只实现 `baseUrl` 和 `token`,同时预留 `profiles`,避免后续多环境改配置结构。
108
+
109
+ 配置读取优先级:
110
+
111
+ 1. 命令行参数,例如 `--token`、`--base-url`。
112
+ 2. 环境变量:
113
+ - `DEEPFLOW_TOKEN`
114
+ - `DEEPFLOW_BASE_URL`
115
+ 3. 全局配置 `~/.deepflow/config.json` 当前 profile。
116
+ 4. 默认值:
117
+ - `baseUrl = http://localhost:3100`
118
+
119
+ ## Token 认证设计
120
+
121
+ ### 登录写入
122
+
123
+ ```bash
124
+ deepflow -l -t dfpat_xxx
125
+ ```
126
+
127
+ 执行逻辑:
128
+
129
+ 1. 校验 token 格式,必须以 `dfpat_` 开头。
130
+ 2. 使用 token 调用 Web 的轻量校验接口:`GET /api/auth/token/verify`。
131
+ 3. 校验通过后写入 `~/.deepflow/config.json`。
132
+ 4. 设置配置文件权限为 `0600`。
133
+ 5. 输出当前认证状态。
134
+
135
+ 输出示例:
136
+
137
+ ```json
138
+ {
139
+ "ok": true,
140
+ "baseUrl": "http://localhost:3100",
141
+ "authenticated": true
142
+ }
143
+ ```
144
+
145
+ ### 登出
146
+
147
+ ```bash
148
+ deepflow auth logout
149
+ ```
150
+
151
+ 只删除本地 token,不删除服务端 token。服务端 token 删除仍通过 Web 设置页完成。
152
+
153
+ ### 状态检查
154
+
155
+ ```bash
156
+ deepflow auth status
157
+ ```
158
+
159
+ 输出:
160
+
161
+ ```json
162
+ {
163
+ "authenticated": true,
164
+ "baseUrl": "http://localhost:3100",
165
+ "tokenPrefix": "dfpat_OQ"
166
+ }
167
+ ```
168
+
169
+ ## 方法设计
170
+
171
+ CLI 对外提供方法式命令。每个方法负责把业务参数转换为 Web API 调用,并输出统一结构。
172
+
173
+ ### 创建迭代并运行 Plan
174
+
175
+ 命令:
176
+
177
+ ```bash
178
+ deepflow plan run --repo <repo_id_or_app_code> --name "Demo Iteration" --context "..."
179
+ ```
180
+
181
+ 可选参数:
182
+
183
+ ```bash
184
+ --base-branch <branch>
185
+ --target-branch <branch>
186
+ --runtime <claude|codex>
187
+ --prompt <text>
188
+ --prompt-file <path>
189
+ --json
190
+ ```
191
+
192
+ 执行链路:
193
+
194
+ 1. 读取 token 和 baseUrl。
195
+ 2. 创建本地迭代:`POST /api/local/iterations`。
196
+ 3. 创建 Plan session:`POST /api/local/sessions`。
197
+ 4. 发送 Plan prompt:`POST /api/local/sessions/{session_id}/messages`。
198
+ 5. 输出迭代、session、run 和 UI URL。
199
+
200
+ 输出示例:
201
+
202
+ ```json
203
+ {
204
+ "ok": true,
205
+ "method": "plan.run",
206
+ "iterationId": "local-iteration-id",
207
+ "backendIterationId": "backend-iteration-id",
208
+ "planSessionId": "session-id",
209
+ "runId": "run-id",
210
+ "uiUrl": "http://localhost:3100/iterations/backend-iteration-id/plan?sessionId=session-id",
211
+ "statusUrl": "http://localhost:3100/api/local/sessions/session-id?iterationId=local-iteration-id",
212
+ "messagesUrl": "http://localhost:3100/api/sessions/session-id/messages?iteration_id=backend-iteration-id",
213
+ "stopUrl": "http://localhost:3100/api/sessions/session-id/stop?iteration_id=backend-iteration-id"
214
+ }
215
+ ```
216
+
217
+ ### 查询 Plan 状态
218
+
219
+ 命令:
220
+
221
+ ```bash
222
+ deepflow plan status --iteration <iteration_id> --session <session_id>
223
+ ```
224
+
225
+ 用于外部系统轮询结果。
226
+
227
+ ### 获取消息/结果
228
+
229
+ 命令:
230
+
231
+ ```bash
232
+ deepflow session messages --iteration <iteration_id> --session <session_id>
233
+ ```
234
+
235
+ 用于拉取会话消息,后续可以增加 `--latest`、`--format markdown`。
236
+
237
+ ### 停止运行
238
+
239
+ 命令:
240
+
241
+ ```bash
242
+ deepflow session stop --iteration <iteration_id> --session <session_id>
243
+ ```
244
+
245
+ 用于外部系统主动中断 plan 或 execute。
246
+
247
+ ## 统一输出与退出码
248
+
249
+ 默认输出人类可读文本,带 `--json` 时输出 JSON。外部系统建议总是加 `--json`。
250
+
251
+ 退出码:
252
+
253
+ | 退出码 | 含义 |
254
+ | --- | --- |
255
+ | 0 | 成功 |
256
+ | 1 | 通用失败 |
257
+ | 2 | 参数错误 |
258
+ | 3 | 未认证或 token 无效 |
259
+ | 4 | 权限不足 |
260
+ | 5 | DeepFlow Web 不可达 |
261
+ | 6 | DeepFlow Core 透传失败 |
262
+
263
+ 错误输出示例:
264
+
265
+ ```json
266
+ {
267
+ "ok": false,
268
+ "errorCode": "unauthorized",
269
+ "message": "Authentication is required.",
270
+ "status": 401
271
+ }
272
+ ```
273
+
274
+ ## 安全设计
275
+
276
+ 1. Token 只写入用户级配置文件,不写入项目目录。
277
+ 2. 配置文件创建后强制 `0600`。
278
+ 3. 日志和错误信息不打印完整 token,只打印 token prefix。
279
+ 4. 命令行参数中的 token 会进入 shell history,文档中建议优先使用环境变量或 stdin 方式。
280
+ 5. 后续可支持:
281
+ - `deepflow auth login --token-stdin`
282
+ - macOS Keychain / Linux Secret Service / Windows Credential Manager。
283
+
284
+ ## 与 Web Token 权限的关系
285
+
286
+ CLI 使用 Web 个人 API Token。首版需要以下权限范围:
287
+
288
+ | CLI 方法 | 需要 scope |
289
+ | --- | --- |
290
+ | `auth status` | `deepflow:read` 或 `web:*` |
291
+ | `plan run` | `deepflow:read` + `deepflow:invoke` 或 `web:*` |
292
+ | `plan status` | `deepflow:read` 或 `web:*` |
293
+ | `session messages` | `deepflow:read` 或 `web:*` |
294
+ | `session stop` | `deepflow:invoke` 或 `web:*` |
295
+
296
+ ## 推荐技术方案
297
+
298
+ DeepFlowCli 作为独立仓库/包,建议使用 Node.js + TypeScript:
299
+
300
+ - 与 DeepFlowWeb 技术栈一致,方便复用类型和请求工具。
301
+ - 可通过 npm/pnpm 发布或本地 link。
302
+ - Node 18+ 原生 `fetch` 足够完成 HTTP 调用。
303
+
304
+ 候选依赖:
305
+
306
+ - `commander`:命令解析。
307
+ - `zod`:配置和响应校验。
308
+ - `conf` 或自定义轻量 config store:全局配置管理。
309
+ - `vitest`:单元测试。
310
+ - `tsx`:开发期运行 TypeScript。
311
+
312
+ 也可以先用纯 Node.js 实现 MVP,减少依赖;后续稳定后再切 TypeScript。
313
+
314
+ ## 项目结构建议
315
+
316
+ ```text
317
+ DeepFlowCli/
318
+ package.json
319
+ tsconfig.json
320
+ src/
321
+ cli.ts
322
+ config.ts
323
+ auth.ts
324
+ client.ts
325
+ methods/
326
+ plan-run.ts
327
+ plan-status.ts
328
+ session-messages.ts
329
+ session-stop.ts
330
+ output.ts
331
+ errors.ts
332
+ tests/
333
+ config.test.ts
334
+ auth.test.ts
335
+ plan-run.test.ts
336
+ docs/
337
+ cli-design.md
338
+ ```
339
+
340
+ ## MVP 实现步骤
341
+
342
+ 1. 初始化 Node/TypeScript CLI 工程,生成 `deepflow` bin。
343
+ 2. 实现全局配置读写:
344
+ - `~/.deepflow/config.json`
345
+ - `baseUrl`
346
+ - `token`
347
+ 3. 实现登录入口:
348
+ - `deepflow -l -t <token>`
349
+ - `deepflow -login -t <token>`
350
+ - `deepflow --login -t <token>`
351
+ - `deepflow auth login -t <token>`
352
+ 4. 实现 Web API client:
353
+ - 自动注入 `Authorization: Bearer <token>`
354
+ - 统一 timeout
355
+ - 统一错误转换
356
+ 5. 实现 `deepflow plan run`,返回 `statusUrl` / `messagesUrl` / `stopUrl`。
357
+ 6. 增加 `--json` 输出和退出码。
358
+ 7. 补单元测试、本地 smoke 和可选 Web/Core e2e smoke。
359
+
360
+ ## 待确认问题
361
+
362
+ 1. CLI 默认 `baseUrl` 是否固定为 `http://localhost:3100`,还是从部署环境统一配置。
363
+ 2. `plan run` 是否必须传 `--repo`,还是允许默认选择第一个应用。
364
+ 3. 是否需要支持多 profile,例如 `deepflow --profile prod plan run ...`。
365
+ 4. 后续是否需要把 `GET /api/auth/token/verify` 暴露在 API 文档中。
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@metagptx/deepflow-cli",
3
+ "version": "0.1.0",
4
+ "description": "Command line interface for DeepFlow external automation.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "deepflow": "bin/deepflow.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md",
14
+ "docs/cli-design.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.0.0"
21
+ },
22
+ "scripts": {
23
+ "check": "node --check bin/deepflow.js && node --check src/*.js",
24
+ "pack:dry-run": "npm pack --dry-run",
25
+ "smoke": "node scripts/smoke.mjs",
26
+ "smoke:e2e": "node scripts/e2e-smoke.mjs",
27
+ "test": "node --test"
28
+ }
29
+ }
package/src/args.js ADDED
@@ -0,0 +1,198 @@
1
+ import { CliError } from "./errors.js";
2
+
3
+ const VALUE_FLAGS = new Set([
4
+ "base-url",
5
+ "baseUrl",
6
+ "base-branch",
7
+ "backend-iteration",
8
+ "context",
9
+ "context-file",
10
+ "fast-timeout",
11
+ "iteration",
12
+ "iteration-type",
13
+ "local-iteration",
14
+ "name",
15
+ "profile",
16
+ "prompt",
17
+ "prompt-file",
18
+ "repo",
19
+ "runtime",
20
+ "session",
21
+ "target-branch",
22
+ "timeout",
23
+ "token",
24
+ ]);
25
+
26
+ const BOOLEAN_FLAGS = new Set([
27
+ "force-new",
28
+ "help",
29
+ "json",
30
+ "login",
31
+ "token-stdin",
32
+ "version",
33
+ ]);
34
+
35
+ const SHORT_FLAGS = new Map([
36
+ ["-h", "help"],
37
+ ["-j", "json"],
38
+ ["-l", "login"],
39
+ ["-t", "token"],
40
+ ]);
41
+
42
+ const LONG_ALIASES = new Map([
43
+ ["-login", "login"],
44
+ ["--login", "login"],
45
+ ["--base-url", "base-url"],
46
+ ["--baseUrl", "baseUrl"],
47
+ ["--base-branch", "base-branch"],
48
+ ["--backend-iteration", "backend-iteration"],
49
+ ["--context", "context"],
50
+ ["--context-file", "context-file"],
51
+ ["--fast-timeout", "fast-timeout"],
52
+ ["--force-new", "force-new"],
53
+ ["--help", "help"],
54
+ ["--iteration", "iteration"],
55
+ ["--iteration-type", "iteration-type"],
56
+ ["--json", "json"],
57
+ ["--local-iteration", "local-iteration"],
58
+ ["--name", "name"],
59
+ ["--profile", "profile"],
60
+ ["--prompt", "prompt"],
61
+ ["--prompt-file", "prompt-file"],
62
+ ["--repo", "repo"],
63
+ ["--runtime", "runtime"],
64
+ ["--session", "session"],
65
+ ["--target-branch", "target-branch"],
66
+ ["--timeout", "timeout"],
67
+ ["--token", "token"],
68
+ ["--token-stdin", "token-stdin"],
69
+ ["--version", "version"],
70
+ ]);
71
+
72
+ function toCamelCase(name) {
73
+ return name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
74
+ }
75
+
76
+ function setFlag(flags, rawName, value) {
77
+ flags[toCamelCase(rawName)] = value;
78
+ }
79
+
80
+ function resolveFlagName(arg) {
81
+ if (SHORT_FLAGS.has(arg)) {
82
+ return SHORT_FLAGS.get(arg);
83
+ }
84
+
85
+ if (LONG_ALIASES.has(arg)) {
86
+ return LONG_ALIASES.get(arg);
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ function parseLongAssignment(arg) {
93
+ const index = arg.indexOf("=");
94
+
95
+ if (index === -1) {
96
+ return null;
97
+ }
98
+
99
+ const namePart = arg.slice(0, index);
100
+ const value = arg.slice(index + 1);
101
+ const name = resolveFlagName(namePart);
102
+
103
+ if (!name) {
104
+ return null;
105
+ }
106
+
107
+ return { name, value };
108
+ }
109
+
110
+ export function parseArgs(argv) {
111
+ const flags = {};
112
+ const positionals = [];
113
+
114
+ for (let index = 0; index < argv.length; index += 1) {
115
+ const arg = argv[index];
116
+
117
+ if (arg === "--") {
118
+ positionals.push(...argv.slice(index + 1));
119
+ break;
120
+ }
121
+
122
+ if (arg.startsWith("--no-")) {
123
+ const name = arg.slice("--no-".length);
124
+
125
+ if (!BOOLEAN_FLAGS.has(name)) {
126
+ throw new CliError(`Unknown option: ${arg}`, {
127
+ code: "bad_option",
128
+ exitCode: 2,
129
+ });
130
+ }
131
+
132
+ setFlag(flags, name, false);
133
+ continue;
134
+ }
135
+
136
+ const assignment = parseLongAssignment(arg);
137
+
138
+ if (assignment) {
139
+ if (!VALUE_FLAGS.has(assignment.name)) {
140
+ throw new CliError(`Option does not take a value: ${arg}`, {
141
+ code: "bad_option",
142
+ exitCode: 2,
143
+ });
144
+ }
145
+
146
+ setFlag(flags, assignment.name, assignment.value);
147
+ continue;
148
+ }
149
+
150
+ const name = resolveFlagName(arg);
151
+
152
+ if (name) {
153
+ if (VALUE_FLAGS.has(name)) {
154
+ const value = argv[index + 1];
155
+
156
+ if (!value || value.startsWith("-")) {
157
+ throw new CliError(`Missing value for ${arg}`, {
158
+ code: "missing_option_value",
159
+ exitCode: 2,
160
+ });
161
+ }
162
+
163
+ setFlag(flags, name, value);
164
+ index += 1;
165
+ continue;
166
+ }
167
+
168
+ if (!BOOLEAN_FLAGS.has(name)) {
169
+ throw new CliError(`Unknown option: ${arg}`, {
170
+ code: "bad_option",
171
+ exitCode: 2,
172
+ });
173
+ }
174
+
175
+ setFlag(flags, name, true);
176
+ continue;
177
+ }
178
+
179
+ if (arg.startsWith("-")) {
180
+ throw new CliError(`Unknown option: ${arg}`, {
181
+ code: "bad_option",
182
+ exitCode: 2,
183
+ });
184
+ }
185
+
186
+ positionals.push(arg);
187
+ }
188
+
189
+ return { flags, positionals };
190
+ }
191
+
192
+ export function commandFromParsedArgs(parsed) {
193
+ if (parsed.flags.login) {
194
+ return ["auth", "login"];
195
+ }
196
+
197
+ return parsed.positionals;
198
+ }