draftgo-cli 3.0.35 → 3.0.39

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.
Files changed (64) hide show
  1. package/README.md +220 -272
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +5 -4
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +1 -1
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +47 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +326 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
@@ -267,9 +267,11 @@ await App.patch(`db/order/batch`, [
267
267
 
268
268
  ---
269
269
 
270
- ## db_meta 本地文件
270
+ ## db_meta 实时契约
271
271
 
272
- 开发前先读 `.draftgo/db_meta/index.json` 了解可用 type 和字段:
272
+ db_meta 是结构化远端资源,不 checkout,也不生成本地 `index.json`。开发前先通过 MCP
273
+ `draftgo_api_search` / `draftgo_api_describe` 确认接口,再用 `draftgo_api_call` 查询目标 type 和 schema。
274
+ 典型返回条目如下:
273
275
 
274
276
  ```json
275
277
  [
@@ -343,7 +343,7 @@ antd Modal:`width="min(90vw, 520px)"`;抽屉窄屏优先 `placement="bottom"
343
343
 
344
344
  ### 导航与系统页边界(强制)
345
345
 
346
- - 顶部导航已有内置资源时,通常先读 `.draftgo/navigations/index.json` 和对应 HTML;需要新导航时优先复制/复刻一份再改造,而不是直接改坏内置导航。
346
+ - 顶部导航已有内置资源时,先通过 MCP 查询导航元数据;需要分析或编辑完整 HTML 时执行 `draftgo checkout nav <id>`,读取 worktree 文件。需要新导航时优先基于现有结构创建或复刻,而不是直接改坏内置导航。
347
347
  - 导航栏通常要同时考虑未登录 / 已登录 / 管理员三种状态,以及收起 / 展开状态;普通用户不显示管理后台入口,管理员额外显示管理后台入口。
348
348
  - 管理端侧边栏通常基于现有内置侧边栏修改,新增业务管理路由优先追加或局部调整;删除系统内置页面入口前先确认影响。
349
349
  - 业务页面内可以做局部二级导航或侧边栏,但必须考虑外部导航已存在且可单独配置,避免重复渲染全局导航。
@@ -0,0 +1,101 @@
1
+ ---
2
+ read_when: 配置或诊断 DraftGo MCP 时 · 查询实时资源或 API 契约时 · 判断宿主支持范围时
3
+ ---
4
+
5
+ # DraftGo MCP
6
+
7
+ > 根 `SKILL.md` 在 Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明实时 MCP 的边界和调用顺序,不能替代页面、前端、运行时、安全或模块资料。
8
+
9
+ ## 边界
10
+
11
+ DraftGo MCP 用于实时发现、结构化查询和普通 API 操作。完整 pages、navigations、docs/articles 正文不通过
12
+ MCP tool result 或参数传输;需要全文时使用 `draftgo checkout`,提交时使用 `draftgo commit`。MCP 返回
13
+ `artifact` / `omitted` 时保留该语义,不要尝试重新展开长内容。
14
+
15
+ 标准发现顺序:
16
+
17
+ 1. `draftgo_project_overview`
18
+ 2. `draftgo_resource_search` 或 `draftgo_resource_list`
19
+ 3. 按需调用 `draftgo_resource_get_metadata` 或 `draftgo_resource_read_fragment`
20
+ 4. 需要实时 API 契约时调用 `draftgo_api_search`、`draftgo_api_describe`
21
+ 5. 结构化资源读写使用 `draftgo_api_call`
22
+
23
+ 结构化资源包括 db_meta、AIHub、system_config、roles、users、doc_categories 和自定义服务元数据。
24
+ 这些资源不 checkout,也不生成 `.draftgo/*/index.json` 镜像。
25
+
26
+ ## CLI
27
+
28
+ ```bash
29
+ draftgo mcp setup [target...]
30
+ draftgo mcp status [target...]
31
+ draftgo mcp test
32
+ draftgo mcp serve
33
+ ```
34
+
35
+ - `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
36
+ - `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
37
+ - `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list 和关键 tools/call。
38
+ - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。
39
+
40
+ `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;它不下载业务资源,
41
+ 也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
42
+ 不得回退到 `pull --all`。
43
+
44
+ ## 宿主配置
45
+
46
+ | 宿主 | 项目配置 | MCP setup |
47
+ |---|---|---|
48
+ | Codex | `.codex/config.toml` | 支持 |
49
+ | Claude Code | `.mcp.json` | 支持 |
50
+ | Cursor | `.cursor/mcp.json` | 支持 |
51
+ | Gemini CLI | `.gemini/settings.json` | 支持 |
52
+ | Kiro | `.kiro/settings/mcp.json` | 支持 |
53
+ | GitHub Copilot | `.vscode/mcp.json` | 支持 |
54
+ | Windsurf | 无可靠的项目级适配 | 不支持,CLI 必须明确提示 |
55
+ | Antigravity | 无可靠的项目级适配 | 不支持,CLI 必须明确提示 |
56
+
57
+ Codex 配置:
58
+
59
+ ```toml
60
+ [mcp_servers.draftgo]
61
+ command = "draftgo"
62
+ args = ["mcp", "serve"]
63
+ ```
64
+
65
+ Claude Code、Cursor、Gemini CLI 和 Kiro 使用各自文件中的 `mcpServers`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "draftgo": {
71
+ "command": "draftgo",
72
+ "args": ["mcp", "serve"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ GitHub Copilot 使用 `servers`,并声明 stdio:
79
+
80
+ ```json
81
+ {
82
+ "servers": {
83
+ "draftgo": {
84
+ "type": "stdio",
85
+ "command": "draftgo",
86
+ "args": ["mcp", "serve"]
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ 配置中不得出现 SAT、token、Authorization、headers、远端 `/mcp` URL、`env` 或带凭据的命令行。
93
+ bridge 必须从当前工作目录的 `.draftgo/config.json` 读取 server/SAT。
94
+
95
+ ## 协议与安全
96
+
97
+ - stdio 只输出 MCP JSON-RPC 帧;普通日志写入 stderr,且不得包含 SAT。
98
+ - 代理 initialize、tools/list、tools/call、通知、取消、错误和流式响应,不改写底座 schema 或结构化错误。
99
+ - HTTP 401/403、协议错误和工具缺失必须清晰失败;输出前对 SAT 和 Authorization 值做脱敏。
100
+ - `.draftgo/config.json`、`.draftgo/worktree/` 和 `.draftgo/conflicts/` 必须加入 `.gitignore`。
101
+ - MCP 不可用时运行 `draftgo mcp test` 收集证据;不要读取旧索引作为替代事实来源。
@@ -8,13 +8,13 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
8
8
 
9
9
  | 模块 | 开发方式 | 入口 |
10
10
  |---|---|---|
11
- | 页面 | 数据库 HTML(`page.value.html`) | `.draftgo/pages/` |
12
- | 导航栏 | 数据库 HTML(`navigation.html`) | `.draftgo/navigations/` |
13
- | 动态 DB | db_meta 定义 schema + `/api/db/{type}` 操作数据 | `.draftgo/db_meta/` |
14
- | 自定义服务 | Go `Register` 服务,支持 route/event/scheduled 混合注册 | `.draftgo/custom_scripts/` |
15
- | AIHub | 配置 AI Agent(模型/编排/能力/子智能体/记忆);页面对话使用 `<dg-chat>`,图片模式使用 `DraftGoAI.images`;见 `references/chat-sdk.md` 与 `references/aihub.md` | `.draftgo/aihub/` |
16
- | 文档中心 | Markdown 文章 + 分类树 | `.draftgo/docs/articles/` |
17
- | 系统配置 | KV 存储,含全局前端层槽位 | `.draftgo/system_config/` |
11
+ | 页面 | 数据库 HTML(`page.value.html`) | MCP 定位,`checkout pages` / `commit pages` 编辑正文 |
12
+ | 导航栏 | 数据库 HTML(`navigation.html`) | MCP 定位,`checkout nav` / `commit nav` 编辑正文 |
13
+ | 动态 DB | db_meta 定义 schema + `/api/db/{type}` 操作数据 | MCP `api_search` / `api_describe` / `api_call` |
14
+ | 自定义服务 | Go `Register` 服务,支持 route/event/scheduled 混合注册 | MCP 实时 API;不创建本地镜像 |
15
+ | AIHub | 配置 AI Agent(模型/编排/能力/子智能体/记忆);页面对话使用 `<dg-chat>`,图片模式使用 `DraftGoAI.images`;见 `references/chat-sdk.md` 与 `references/aihub.md` | MCP 实时 API;不创建本地镜像 |
16
+ | 文档中心 | Markdown/HTML 文章 + 分类树 | 正文用 `checkout docs` / `commit docs`;分类用 MCP |
17
+ | 系统配置 | KV 存储,含全局前端层槽位 | MCP 实时 API;不创建本地镜像 |
18
18
 
19
19
  ## 平台内置模块(开箱即用,不需实现)
20
20
 
@@ -56,7 +56,7 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
56
56
  ## 自定义服务边界
57
57
 
58
58
  - 新服务使用 Go `Register(app *sdk.App)` 自动注册路由、事件和定时任务;完整语言、`draftgo` 和 SDK 契约见 `references/custom-services.md`。
59
- - `route`:对外暴露 HTTP 端点,运行时路径为 `/api/x/{slug}/{path}`;同一 Go 服务可用多个 `app.Route(method, path, handler)` 注册多个端点,推送后要真实请求验证。
59
+ - `route`:对外暴露 HTTP 端点,运行时路径为 `/api/x/{slug}/{path}`;同一 Go 服务可用多个 `app.Route(method, path, handler)` 注册多个端点,保存或发布后要真实请求验证。
60
60
  - `event`:响应平台事件,如 `db.created` / `db.updated` / `user.registered`;用 `app.On(event, handler)` 注册,可为同一事件注册多个 handler。
61
61
  - `scheduled`:用 `app.Schedule("分 时 日 月 周", handler)` 注册 cron;同一服务可声明多个定时 handler。
62
62
  - 新 Go 服务的 `Register` 是唯一触发器事实来源;旧 `triggers` 字段不参与注册。
@@ -37,12 +37,15 @@ delivery_owner: main
37
37
  2. 将 `depends` 已满足且 `resource_lock` 不冲突的工作单元放入同一 wave。
38
38
  3. 并行执行当前 wave,汇总后回读所有变更资源。
39
39
  4. 处理接口接线和入口绑定,再启动依赖已满足的下一 wave。
40
- 5. 统一运行 `draftgo check`、必要的 API/UI 验证,并按交付意图执行批量推送。
40
+ 5. 统一运行 `draftgo check`、必要的 API/UI 验证;只对已 checkout 的 pages/nav/docs 执行 commit,结构化资源通过 MCP 实时 API 交付。
41
41
 
42
42
  某个工作单元失败时,其依赖项等待处理;其余独立分支继续汇总。发现资源锁冲突时,将冲突单元合并或移到后续 wave。
43
43
 
44
- 批量交付示例:
44
+ 已 checkout 长正文的批量交付示例:
45
45
 
46
46
  ```bash
47
- draftgo auto-push --batch pages 1,2,3 nav 4 custom_scripts 7
47
+ draftgo commit pages 1 2 3
48
+ draftgo commit nav 4
48
49
  ```
50
+
51
+ 不同类型分开提交。发生 409/412 时当前 wave 必须停止并保留冲突材料,不得继续自动提交后续依赖项。
@@ -113,16 +113,13 @@ const orderId = query.orderId; // "42"
113
113
 
114
114
  规则:**不能在业务页面内重复实现全局层能力;不允许新增槽位,只能编辑内置槽位。**
115
115
 
116
- ### 修改和推送方法
116
+ ### 修改方法
117
117
 
118
- 本地修改来自 `.draftgo/system_config/index.json`。修改前先执行 `/draftgo pull system_config` 获取最新值,改完后执行:
118
+ system_config 是结构化远端资源。修改前通过 MCP `draftgo_api_search` / `draftgo_api_describe` 获取实时契约,
119
+ 再用 `draftgo_api_call` 读取并更新目标键;不要 checkout、pull 或维护本地索引。
119
120
 
120
- ```bash
121
- python {{SKILL_SCRIPTS}}/draftgo_push.py system_config frontend_global_toast_opacity
122
- ```
123
-
124
- 也可以通过 skill 命令使用 `/draftgo push system_config frontend_global_toast_opacity`。
125
-
126
- 前端全局层是系统默认配置。通过 API 或脚本更新现有全局配置时,只发送 `config_value`;不要带 `description`、`category`、`value_type`、`status` 等元信息,否则旧基座或旧索引容易触发“系统默认字段不允许修改字段描述/分类/状态”。`draftgo_push.py system_config` 已对 `frontend_global_*` 自动只推值。
121
+ 前端全局层是系统默认配置。更新现有全局配置时只发送 `config_value`;不要带 `description`、`category`、
122
+ `value_type`、`status` 等元信息,避免触发“系统默认字段不允许修改字段描述/分类/状态”。
127
123
 
128
- 页面管理中 Toast 时长以秒输入,`sys_config` 中仍以毫秒存储。例如 UI 中 `错误 Toast 时长(s) = 4.5`,本地索引应写 `frontend_global_toast_duration_error.config_value = 4500`。
124
+ 页面管理中 Toast 时长以秒输入,`sys_config` 中仍以毫秒存储。例如 UI 中
125
+ `错误 Toast 时长(s) = 4.5` 对应 `frontend_global_toast_duration_error.config_value = 4500`。
@@ -0,0 +1,8 @@
1
+ # Skill scripts
2
+
3
+ DraftGo execution is provided by the installed `draftgo` CLI. Legacy Python pull, push, init, and delete helpers were
4
+ removed because they depended on full local mirrors and bypassed the MCP plus checkout/commit safety model.
5
+
6
+ Do not add a script that downloads `.draftgo/*/index.json`, embeds SAT credentials, sends full pages/nav/docs content
7
+ through MCP, or retries version conflicts with force. Reusable helpers added here must preserve the workflows in
8
+ `references/mcp.md` and `references/checkout.md`.
@@ -93,9 +93,9 @@ now:
93
93
  ### 路径选择(先判断再走)
94
94
 
95
95
  ```
96
- 扫描 .draftgo/ 下的数据,判断走哪条路径:
96
+ 通过 MCP 实时摘要和项目内可保留的决策记录判断走哪条路径:
97
97
 
98
- ├─ 存在用户自建内容(非 tag="系统" 的页面、db_meta、custom_scripts)→ 路径 A
98
+ ├─ MCP 显示存在用户自建内容(非 tag="系统" 的页面、db_meta、custom_scripts)→ 路径 A
99
99
  ├─ 用户提供了设计文档(PRD、原型图描述、需求文档等)→ 路径 A(以文档为分析素材)
100
100
  └─ 只有内置系统页面,无用户自建内容,无设计文档 → 路径 B(对话采集)
101
101
  ```
@@ -105,11 +105,11 @@ now:
105
105
  ```
106
106
  输出:[ Story 文件生成中...正在分析项目 ]
107
107
 
108
- 1. 扫描项目结构(以及用户提供的设计文档,如有):
109
- - .draftgo/pages/index.json现有页面(跳过 tag="系统" 的内置页)
110
- - .draftgo/navigations/index.json导航结构
111
- - .draftgo/db_meta/index.json数据模型
112
- - .draftgo/custom_scripts/index.json 自定义脚本
108
+ 1. 查询项目结构(以及读取用户提供的设计文档,如有):
109
+ - MCP project_overview/resource_list资源总览
110
+ - MCP resource_search/get_metadata页面与导航元数据(跳过 tag="系统" 的内置页)
111
+ - MCP api_search/api_describe/api_calldb_meta 与自定义服务结构
112
+ - 只有必须分析完整 pages/nav/docs 正文时才 checkout;不要要求 MCP 返回全文
113
113
  - .draftgo/changelog.md → 更新日志
114
114
  - .draftgo/lessons/ → 开发经验记录
115
115
  - .draftgo/Task/ → 历史任务文档
@@ -248,7 +248,7 @@ AI 在开发过程中检测到"方向性决策"时,主动提议沉淀:
248
248
  ### 构建 Story
249
249
 
250
250
  1. 检查 `.draftgo/story.yaml` 是否存在
251
- 2. 不存在 → 判断路径 A 还是 B(看 `.draftgo/pages/index.json` 是否有数据)
251
+ 2. 不存在 → 根据 MCP project_overview/resource_list 和用户提供的设计材料判断路径 A 还是 B
252
252
  3. 走对应构建流程
253
253
  4. 落盘 → 回到用户原始请求
254
254
 
package/src/cli.js CHANGED
@@ -20,6 +20,11 @@ const VALUE_FLAGS = new Set([
20
20
  'delivery',
21
21
  'assignee',
22
22
  'assignee-id',
23
+ 'timeout',
24
+ 'operation-id',
25
+ 'params',
26
+ 'input',
27
+ 'type',
23
28
  ]);
24
29
 
25
30
  function parse(argv) {
@@ -10,7 +10,13 @@ const commands = [
10
10
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
11
11
  { name: 'check', run: (dir, _args, flags) => require('./commands/check')(dir, flags) },
12
12
  { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
13
- { name: 'api', run: (_dir, args, flags) => require('./commands/api')(args, flags) },
13
+ { name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
14
+ { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
15
+ { name: 'commit', run: (dir, args, flags) => require('./commands/commit')(dir, args, flags) },
16
+ { name: 'diff', run: (dir, args, flags) => require('./commands/diff')(dir, args, flags) },
17
+ { name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
18
+ { name: 'conflict', run: (dir, args, flags) => require('./commands/conflict')(dir, args, flags) },
19
+ { name: 'api', run: (dir, args, flags) => require('./commands/api')(dir, args, flags) },
14
20
  { name: 'delete', aliases: ['del', 'rm'], run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
15
21
  { name: 'deploy', run: (dir, args, flags) => require('./commands/deploy')(dir, args, flags) },
16
22
  { name: 'auto-push', aliases: ['autopush'], run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
@@ -1,203 +1,40 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
3
  const log = require('../logger');
4
+ const { loadProjectConfig } = require('../projectConfig');
5
+ const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
6
6
 
7
- const API_FILE = path.resolve(__dirname, '..', '..', 'resources', 'skill', 'references', 'api.json');
8
- const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options']);
9
- const AUTH_HEADER_NAMES = new Set(['authorization', 'proxy-authorization', 'x-api-key']);
10
-
11
- function resolveRef(spec, ref) {
12
- if (!ref || !ref.startsWith('#/')) return null;
13
- return ref.slice(2).split('/').reduce((value, part) => value && value[part], spec);
14
- }
15
-
16
- function schemaType(schema, spec) {
17
- if (!schema || !Object.keys(schema).length) return 'unspecified';
18
- if (schema.$ref) {
19
- const target = resolveRef(spec, schema.$ref);
20
- return target && (target.title || schema.$ref.split('/').pop()) || schema.$ref;
21
- }
22
- if (schema.anyOf) return schema.anyOf.map((item) => schemaType(item, spec)).join(' | ');
23
- if (schema.oneOf) return schema.oneOf.map((item) => schemaType(item, spec)).join(' | ');
24
- if (schema.allOf) return schema.allOf.map((item) => schemaType(item, spec)).join(' & ');
25
- if (schema.type === 'array') return `${schemaType(schema.items, spec)}[]`;
26
- if (schema.type) return schema.format ? `${schema.type} (${schema.format})` : schema.type;
27
- if (schema.properties) return 'object';
28
- return 'unspecified';
29
- }
30
-
31
- function schemaNotes(schema) {
32
- const notes = [];
33
- if (schema.description) notes.push(schema.description);
34
- if (schema.default !== undefined) notes.push(`default: ${JSON.stringify(schema.default)}`);
35
- if (schema.enum) notes.push(`enum: ${schema.enum.map((item) => JSON.stringify(item)).join(', ')}`);
36
- return notes.length ? ` (${notes.join('; ')})` : '';
37
- }
38
-
39
- function printSchema(schema, spec, indent = ' ', seen = new Set()) {
40
- if (!schema || !Object.keys(schema).length) {
41
- console.log(`${indent}schema: unspecified`);
42
- return;
43
- }
44
-
45
- let resolved = schema;
46
- let refName = '';
47
- if (schema.$ref) {
48
- refName = schema.$ref.split('/').pop();
49
- if (seen.has(schema.$ref)) {
50
- console.log(`${indent}${refName} (recursive)`);
51
- return;
52
- }
53
- resolved = resolveRef(spec, schema.$ref) || schema;
54
- seen = new Set(seen).add(schema.$ref);
55
- }
56
-
57
- console.log(`${indent}schema: ${refName ? `${refName} (${schemaType(resolved, spec)})` : schemaType(resolved, spec)}${schemaNotes(resolved)}`);
58
- const variants = resolved.anyOf || resolved.oneOf || resolved.allOf;
59
- if (variants) {
60
- variants.forEach((variant, index) => {
61
- console.log(`${indent} variant ${index + 1}:`);
62
- printSchema(variant, spec, `${indent} `, seen);
63
- });
7
+ function printResult(title, result) {
8
+ log.title(title);
9
+ if (Array.isArray(result)) {
10
+ for (const item of result) log.plain(` ${JSON.stringify(item)}`);
64
11
  return;
65
12
  }
66
- const required = new Set(resolved.required || []);
67
- for (const [name, property] of Object.entries(resolved.properties || {})) {
68
- const fieldSchema = property.$ref ? resolveRef(spec, property.$ref) || property : property;
69
- const marker = required.has(name) ? ' required' : ' optional';
70
- console.log(`${indent} ${name}: ${schemaType(property, spec)}${marker}${schemaNotes(fieldSchema)}`);
71
- }
72
- if (resolved.additionalProperties === true) console.log(`${indent} additional properties: allowed`);
73
- if (resolved.items && !resolved.properties) printSchema(resolved.items, spec, `${indent} `, seen);
74
- }
75
-
76
- function authenticationFor(operation, spec) {
77
- const security = operation.security === undefined ? spec.security : operation.security;
78
- const schemes = (security || []).flatMap((requirement) => Object.keys(requirement).map((name) => {
79
- const definition = spec.components && spec.components.securitySchemes && spec.components.securitySchemes[name] || {};
80
- return { name, type: definition.type || 'unknown', scheme: definition.scheme, bearerFormat: definition.bearerFormat };
81
- }));
82
- const headers = (operation.parameters || []).filter((parameter) =>
83
- parameter.in === 'header' && AUTH_HEADER_NAMES.has(String(parameter.name).toLowerCase())
84
- );
85
- return { schemes, headers };
86
- }
87
-
88
- function printAuthentication(operation, spec) {
89
- const auth = authenticationFor(operation, spec);
90
- if (!auth.schemes.length && !auth.headers.length) {
91
- console.log(' Authentication: no requirement declared in the bundled OpenAPI.');
13
+ if (result && Array.isArray(result.items)) {
14
+ for (const item of result.items) log.plain(` ${JSON.stringify(item)}`);
92
15
  return;
93
16
  }
94
- console.log(' Authentication:');
95
- for (const scheme of auth.schemes) {
96
- const detail = [scheme.type, scheme.scheme, scheme.bearerFormat].filter(Boolean).join(', ');
97
- console.log(` security scheme ${scheme.name}${detail ? ` (${detail})` : ''}`);
98
- }
99
- for (const header of auth.headers) {
100
- console.log(` header ${header.name}: ${schemaType(header.schema, spec)}${header.required ? ' (required)' : ' (optional)'}${schemaNotes(header)}`);
101
- }
102
- }
103
-
104
- function printOperation(item, spec) {
105
- console.log(`\n${item.method} ${item.route}${item.summary ? ` ${item.summary}` : ''}`);
106
- if (item.operationId) console.log(` Operation: ${item.operationId}`);
107
- printAuthentication(item.operation, spec);
108
-
109
- const parameters = (item.operation.parameters || []).filter((parameter) =>
110
- !(parameter.in === 'header' && AUTH_HEADER_NAMES.has(String(parameter.name).toLowerCase()))
111
- );
112
- if (parameters.length) {
113
- console.log(' Parameters:');
114
- for (const parameter of parameters) {
115
- console.log(` ${parameter.in} ${parameter.name}: ${schemaType(parameter.schema, spec)}${parameter.required ? ' (required)' : ' (optional)'}${schemaNotes(parameter)}`);
116
- }
117
- }
118
-
119
- if (item.operation.requestBody) {
120
- console.log(` Request body: ${item.operation.requestBody.required ? 'required' : 'optional'}`);
121
- for (const [contentType, media] of Object.entries(item.operation.requestBody.content || {})) {
122
- console.log(` ${contentType}:`);
123
- printSchema(media.schema, spec, ' ');
124
- }
125
- }
126
-
127
- const responses = Object.entries(item.operation.responses || {});
128
- if (responses.length) {
129
- console.log(' Responses:');
130
- for (const [status, response] of responses) {
131
- console.log(` ${status}${response.description ? ` ${response.description}` : ''}`);
132
- for (const [contentType, media] of Object.entries(response.content || {})) {
133
- console.log(` ${contentType}:`);
134
- printSchema(media.schema, spec, ' ');
135
- }
136
- }
137
- }
17
+ console.log(JSON.stringify(result, null, 2));
138
18
  }
139
19
 
140
- function apiCommand(positional, flags = {}) {
141
- const keyword = String(positional.join(' ') || '').trim().toLowerCase();
142
- if (!keyword) {
143
- log.err('用法:draftgo api <keyword>');
144
- log.dim(' 示例:draftgo api pages batch');
20
+ async function apiCommand(projectDir, positional, flags = {}) {
21
+ const first = String(positional[0] || '').toLowerCase();
22
+ const describe = first === 'describe';
23
+ const queryParts = first === 'search' || describe ? positional.slice(1) : positional;
24
+ const query = queryParts.join(' ').trim();
25
+ if (!query) {
26
+ log.err('Usage: draftgo api <query> | draftgo api search <query> | draftgo api describe <operation_id>');
145
27
  return 1;
146
28
  }
147
29
 
148
- let spec;
149
- try {
150
- spec = JSON.parse(fs.readFileSync(API_FILE, 'utf8'));
151
- } catch (err) {
152
- log.err(`无法读取 OpenAPI:${err.message}`);
153
- return 1;
154
- }
155
-
156
- const words = keyword.split(/\s+/).filter(Boolean);
157
- const matches = [];
158
- for (const [route, methods] of Object.entries(spec.paths || {})) {
159
- for (const [method, operation] of Object.entries(methods || {})) {
160
- if (!HTTP_METHODS.has(method.toLowerCase())) continue;
161
- const haystack = [route, method, operation.summary, operation.operationId, ...(operation.tags || [])]
162
- .filter(Boolean).join(' ').toLowerCase();
163
- if (!words.every((word) => haystack.includes(word))) continue;
164
- matches.push({
165
- method: method.toUpperCase(),
166
- route,
167
- summary: operation.summary || '',
168
- operationId: operation.operationId || '',
169
- tags: operation.tags || [],
170
- operation,
171
- });
172
- }
173
- }
174
-
175
- if (flags.output === 'json') {
176
- console.log(JSON.stringify({
177
- source: {
178
- kind: 'DraftGo bundled base OpenAPI',
179
- openapi: spec.openapi || null,
180
- version: spec.info && spec.info.version || null,
181
- customServiceRoutes: {
182
- included: false,
183
- reason: 'Custom service routes are registered dynamically from local app.Route calls.',
184
- derive: '<METHOD> /api/x/<slug><route-path>',
185
- },
186
- },
187
- query: keyword,
188
- matches,
189
- }, null, 2));
190
- return matches.length ? 0 : 1;
191
- }
192
- if (!matches.length) {
193
- log.warn(`未找到包含“${keyword}”的 API。`);
194
- return 1;
195
- }
196
- log.title(`DraftGo base API — ${keyword}`);
197
- log.dim(`Source: bundled DraftGo base OpenAPI v${spec.info && spec.info.version || 'unknown'}; these are platform APIs, not project-defined APIs.`);
198
- log.dim('Custom service routes are not indexed here: derive them from local app.Route calls as <METHOD> /api/x/<slug><route-path>.');
199
- for (const item of matches.slice(0, 50)) printOperation(item, spec);
200
- if (matches.length > 50) log.dim(`\nOnly the first 50 of ${matches.length} matches are shown; use --output json for all contracts.`);
30
+ const config = loadProjectConfig(projectDir);
31
+ const expected = describe ? TOOL_NAMES.apiDescribe : TOOL_NAMES.apiSearch;
32
+ const session = await openToolSession(config, [expected]);
33
+ const result = await callStructured(session, expected, describe
34
+ ? { operation_id: query }
35
+ : { query });
36
+ if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
37
+ else printResult(describe ? `DraftGo API ${query}` : `DraftGo API search: ${query}`, result);
201
38
  return 0;
202
39
  }
203
40
 
@@ -1,41 +1,72 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const path = require('path');
5
4
  const log = require('../logger');
6
5
  const check = require('./check');
7
- const sync = require('./sync');
6
+ const { loadProjectConfig } = require('../projectConfig');
7
+ const { loadManifest, absolutePath } = require('../worktree/manifest');
8
+ const { hashFile } = require('../worktree/streams');
9
+ const { listConflicts, commitResources } = require('../worktree');
8
10
 
9
- function autoPush(projectDir, positional, flags = {}) {
10
- const configPath = path.join(projectDir, '.draftgo', 'config.json');
11
- if (!fs.existsSync(configPath)) {
12
- log.dim('自动推送未执行:未连接 DraftGo 服务器(缺少 .draftgo/config.json)。');
13
- return 0;
11
+ async function changedEntries(projectDir, manifest) {
12
+ const changed = [];
13
+ for (const entry of Object.values(manifest.entries)) {
14
+ const local = absolutePath(projectDir, entry.local_path);
15
+ if (!fs.existsSync(local)) continue;
16
+ if ((await hashFile(local)).hash !== entry.base_hash) changed.push(entry);
14
17
  }
18
+ return changed;
19
+ }
15
20
 
21
+ async function autoPush(projectDir, positional, flags = {}) {
16
22
  let config;
17
23
  try {
18
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
19
- } catch (err) {
20
- log.err(`自动推送未执行:config.json 解析失败:${err.message}`);
24
+ config = loadProjectConfig(projectDir);
25
+ } catch {
26
+ log.dim('auto-push skipped: this project is not connected to DraftGo.');
27
+ return 0;
28
+ }
29
+
30
+ if (config.auto_push !== true) {
31
+ log.dim('auto-push skipped: .draftgo/config.json does not set auto_push to true.');
32
+ return 0;
33
+ }
34
+ const conflicts = listConflicts(projectDir);
35
+ if (conflicts.length) {
36
+ log.err(`auto-push stopped: ${conflicts.length} unresolved DraftGo conflict(s).`);
21
37
  return 1;
22
38
  }
23
39
 
24
- log.step('auto-push: 先执行 check …');
25
40
  const checkCode = check(projectDir, { strict: flags.strict });
26
41
  if (checkCode !== 0) {
27
- log.err('自动推送终止:check 未通过,请修复后重试。');
42
+ log.err('auto-push stopped because local validation failed.');
28
43
  return checkCode;
29
44
  }
30
45
 
31
- if (config.auto_push !== true) {
32
- log.dim('自动推送已跳过:check 已通过,但 .draftgo/config.json 的 auto_push 不为 true。');
46
+ const groups = new Map();
47
+ if (positional.length) {
48
+ const [resourceType, ...ids] = positional;
49
+ if (!resourceType || !ids.length) {
50
+ log.err('Usage: draftgo auto-push [<pages|nav|docs> <id...>]');
51
+ return 1;
52
+ }
53
+ groups.set(resourceType, ids);
54
+ } else {
55
+ for (const entry of await changedEntries(projectDir, loadManifest(projectDir))) {
56
+ if (!groups.has(entry.resource_type)) groups.set(entry.resource_type, []);
57
+ groups.get(entry.resource_type).push(entry.resource_id);
58
+ }
59
+ }
60
+ if (!groups.size) {
61
+ log.dim('auto-push: no checked-out content changed.');
33
62
  return 0;
34
63
  }
35
64
 
36
- log.step('auto_push=true:开始 push …');
37
- const pushArgs = flags.batch ? ['--batch', ...positional] : positional;
38
- return sync(projectDir, 'push', pushArgs, flags);
65
+ for (const [resourceType, ids] of groups) {
66
+ await commitResources(projectDir, resourceType, ids, { config });
67
+ }
68
+ log.ok('auto-push committed all changed checked-out content.');
69
+ return 0;
39
70
  }
40
71
 
41
72
  module.exports = autoPush;