draftgo-cli 3.0.48 → 3.0.51

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # draftgo-cli
2
2
 
3
+ ## Workflow 2.0
4
+
5
+ `draftgo context` outputs schema `2.0` with separate `bundled_references`, `live_project`, `live_resources`, `live_apis`, `live_db_meta`, and `live_session` sections. It writes to stdout by default, uses current cloud data on every run, and does not create a persistent context cache. Frontend and custom-service tasks always retain complete live DB Meta.
6
+
7
+ Pages, navigations, documents, and custom services are long-content resources. Custom services use `draftgo checkout custom-services <id>` and a four-file worktree (`service.go`, `go.mod`, `go.sum`, `service.json`). `commit` saves the shared cloud draft, `test` validates or executes it in the DraftGo Runner, and `publish` explicitly promotes a validated draft without interrupting the current live version. Start later work with `refresh`; it stops when local edits exist. Cloud versions are the only history, while local storage keeps only current content and one base.
8
+
9
+ Small, single-point changes do not need Task overhead. Cross-module, cross-session, or multi-Agent work uses one authoritative `.draftgo/Task/[YYYY-MM-DD]<slug>/Task.md` and `draftgo task create|add|claim|start|complete|block|reopen|accept|finish`. The file keeps the user's original request, the clarified executable requirement, expected effects, scope, checklist, decisions, blockers, and final acceptance together. Legacy `task.yaml + plan.md + items/` tasks remain readable and migrate only through an explicit `draftgo task migrate`. CLI temporary output is registered in `.draftgo/runtime-manifest.json` under `.draftgo/tmp/` or typed `.draftgo/artifacts/`; `draftgo clean` only handles those registered files. UI smoke checks remain single viewport and use `--screenshot never` by default. Supply `on-failure` or `always` only for explicit strong visual verification.
10
+
3
11
  面向 AI 编码工具的 DraftGo 工作台 CLI:分发 DraftGo Skill,通过 MCP 发现和操作实时资源,并为页面、导航和文档提供可校验的 `checkout` / `commit` 工作流。
4
12
 
5
13
  DraftGo Next 壳层使用 React + Vite;数据库页面可按任务使用原生 Web 技术、Tailwind CSS 或平台内置组件库。CLI 仍提供 Docker 本地运行、项目检查和浏览器验证能力。
@@ -170,6 +178,7 @@ draftgo context --task frontend --output json
170
178
  draftgo checkout pages 42 43
171
179
  # 独立资源可并行编辑;同一资源只由一个 Agent 修改
172
180
  draftgo check
181
+ draftgo check --remote # 交付前按需对比远端版本/hash
173
182
  draftgo verify-ui http://localhost:5173 # UI 变更时
174
183
  draftgo diff pages 42
175
184
  draftgo diff pages 43
@@ -203,9 +212,10 @@ draftgo changelog add "完成文档管理系统及角色权限"
203
212
  | `draftgo context --task <task> --output json` | 精确读取任务 Reference 章节,并复用一个 MCP 会话并行查询项目、资源、API 和数据结构;保留原始结果及来源。 |
204
213
  | `draftgo map` | 并行读取 pages、navigations、docs/articles 元数据,再叠加本地 checkout 状态;`--type` 可限制一种类型。 |
205
214
  | `draftgo checkout <pages|nav|docs> <id...>` | 下载完整正文和 base,校验 hash 后原子落盘;`--force` 可显式覆盖已修改的本地正文。 |
206
- | `draftgo check` | 只检查 checkout manifest、worktree 文件、base/hash、扩展名、HTML 结构和已 checkout 导航引用。 |
215
+ | `draftgo check` | 默认只检查本地 checkout;`--remote` 额外对比 local/base/manifest/remote 的版本和 hash。 |
207
216
  | `draftgo diff <pages|nav|docs> <id>` | 显示 checkout base 与当前本地正文的差异。 |
208
217
  | `draftgo commit <pages|nav|docs> <id...>` | 本地校验后,通过专用 HTTP 上传完整正文并更新 base/manifest。 |
218
+ | `draftgo reconcile <pages|nav|docs> <id...>` | 仅当 local 与经校验的 remote 字节一致时,安全修复落后的 base/manifest 元数据。 |
209
219
  | `draftgo conflicts` | 列出未解决冲突;存在冲突时返回非零。`--all` 包括已解决记录。 |
210
220
  | `draftgo conflict show <type> <id>` | 显示冲突的 base/local/remote/worktree 路径。 |
211
221
  | `draftgo conflict resolve <type> <id>` | 确认 worktree 已合并,把保留的 remote 设为新 base;随后重新 check、diff、commit。 |
@@ -220,6 +230,7 @@ draftgo changelog add "完成文档管理系统及角色权限"
220
230
  | `draftgo api <query>` | 通过 MCP `api_search` 查询实时 API 契约。 |
221
231
  | `draftgo api search <query>` | `draftgo api <query>` 的显式搜索形式。 |
222
232
  | `draftgo api describe <operation_id>` | 通过 MCP `api_describe` 查看指定操作。 |
233
+ | `draftgo api call <operation_id> --input <file>` | 描述后调用实时 operation;JSON 文件严格按 UTF-8 解码并输出状态码、服务端 code 和 request ID。 |
223
234
  | `draftgo delete <operation_id> [id]` | 描述并确认后调用实时 MCP `api_call`;支持 `--params JSON`、`--input file` 和 `--yes`。 |
224
235
  | `draftgo deploy [<type> <id...>]` | 先 `check`,再按 delivery 模式检查 diff 或 commit。仅 `--delivery local` 可省略 type/id。 |
225
236
  | `draftgo auto-push [<type> <id...>]` | `auto_push: true` 时检查并 commit 已 checkout 的变更;有冲突立即停止。 |
@@ -321,7 +332,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
321
332
  }
322
333
  ```
323
334
 
324
- `draftgo check --output json` 返回 `{ map, errors, warnings, warningDetails, legacy_cache_ignored }`。`--strict` 会把 warnings 也视为失败。`check` 不访问远端,也不会读取旧全量资源索引。
335
+ `draftgo check --output json` 返回 `{ map, errors, warnings, warningDetails, remote, remote_error, legacy_cache_ignored }`。`--strict` 会把 warnings 也视为失败。默认 `check` 不访问远端;显式使用 `--remote` 时读取每个 checkout 的实时 metadata,并把 `committed_unrecorded` 等可恢复状态指向 `draftgo reconcile`。它始终不会读取旧全量资源索引。
325
336
 
326
337
  ## Skill 与领域约定
327
338
 
@@ -343,6 +354,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
343
354
  - `--task <task>`:选择 context 路由;支持 frontend、data、custom-service、aihub、content、project。
344
355
  - `--type <type>`:限制 `map` 查询一种正文资源类型;支持 pages、nav/navigations、docs/articles 及其别名。
345
356
  - `--strict`:check/deploy/auto-push 将 warnings 视为失败。
357
+ - `--remote`:让 check 按需对比 checkout 的远端版本和 hash;默认仍为快速离线检查。
346
358
  - `--force`:init 覆盖已有 Skill;checkout 显式覆盖已修改的本地正文。update 始终刷新目标 Skill。
347
359
  - `--purge`:uninstall 时同时删除整个 `.draftgo/` 运行时目录。
348
360
  - `--skip-update-check`:update 不访问 npm,直接用当前 CLI 刷新 Skill。
@@ -353,6 +365,8 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
353
365
  - `--allow-offline` / `--no-mcp-setup`:允许 connect 在 MCP 暂不可用时保存,或跳过宿主配置。
354
366
  - `--yes`:跳过支持该选项的交互确认。
355
367
  - `--mobile-check auto|always|never`:控制 `verify-ui` 是否执行。
368
+ - `--resource <type:id>`:verify-ui 打开 URL 前对比本地 checkout 与远端已提交版本,并明确报告实际验证来源。
369
+ - `--browser-path <file>`:verify-ui 显式使用浏览器可执行文件;也可设置 `DRAFTGO_BROWSER_PATH`,否则自动查找系统浏览器和 Playwright 缓存。
356
370
  - `--token auto|never`:`verify-ui` 默认读取 `.draftgo/config.json`,对与 `server` 同源的地址自动附加 `token=<SAT>`;配置无 SAT、跨源地址或 `never` 模式均不附加。
357
371
  - `--screenshot on-failure|always|never`:控制 UI 截图。
358
372
  - `--delivery local|preview|deploy`:控制 deploy 行为。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.48",
3
+ "version": "3.0.51",
4
4
  "description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
5
5
  "bin": {
6
6
  "draftgo": "bin/draftgo.js"
@@ -45,9 +45,10 @@
45
45
  "lint": "node scripts/check-syntax.js",
46
46
  "validate:skill": "node scripts/validate-skill.js",
47
47
  "verify:package": "node scripts/verify-package.js",
48
- "test": "npm run lint && npm run validate:skill && npm run verify:package && npm run test:unit && npm run test:context && npm run test:changelog && npm run test:mcp && npm run test:worktree && npm run test:migration && npm run test:local && npm run test:e2e",
48
+ "test": "npm run lint && npm run validate:skill && npm run verify:package && npm run test:unit && npm run test:context && npm run test:workflow && npm run test:changelog && npm run test:mcp && npm run test:worktree && npm run test:migration && npm run test:local && npm run test:e2e",
49
49
  "test:unit": "node tests/unit.js",
50
50
  "test:context": "node tests/context.test.js",
51
+ "test:workflow": "node tests/workflow-upgrade.test.js",
51
52
  "test:changelog": "node tests/changelog.test.js",
52
53
  "test:mcp": "node tests/mcp.test.js",
53
54
  "test:worktree": "node tests/worktree.test.js",
@@ -57,11 +58,11 @@
57
58
  },
58
59
  "dependencies": {
59
60
  "@grpc/grpc-js": "1.14.1",
61
+ "adm-zip": "0.5.16",
60
62
  "parse5": "6.0.1",
61
63
  "playwright-core": "1.61.1"
62
64
  },
63
65
  "directories": {
64
66
  "test": "tests"
65
- },
66
- "devDependencies": {}
67
+ }
67
68
  }
@@ -3,6 +3,15 @@ name: draftgo
3
3
  description: Use this skill to inspect, develop, debug, or deliver a DraftGo application. Use draftgo context for task-specific references and live MCP discovery, checkout/commit for complete page, navigation, and document bodies, and write the changelog only after all verification and delivery succeed.
4
4
  ---
5
5
 
6
+ ## Workflow 2.0 authority
7
+
8
+ - Treat pages, navigations, docs/articles, and custom services as checkout resources. Never save complete MCP responses, duplicate source snapshots, temporary Go modules, SDK stubs, executables, or build directories by hand.
9
+ - `context` schema 2.0 separates `bundled_references` from `live_project`, `live_resources`, `live_apis`, and complete `live_db_meta`. It is stdout-only by default and has no persistent or delta cache.
10
+ - Custom-service flow: `context --task custom-service` -> `checkout custom-services` -> edit four files -> `commit` cloud draft -> `test` in the server Runner -> `check --remote` -> `publish` -> `refresh`. CLI and admin UI edit the same cloud draft.
11
+ - Do not create Task overhead for a small, single-point change. For cross-module, cross-session, or multi-Agent work, create or resume `.draftgo/Task/[YYYY-MM-DD]<slug>/Task.md`. It is the only task state: preserve both the user's original request and the clarified executable requirement, assign one owner per item/resource, and check an item only with verification evidence. Changelog remains the final delivery record, not a task tracker.
12
+ - Normal UI work uses lightweight single-viewport `verify-ui` with no screenshot. Only explicit visual acceptance or regression diagnosis may pass `--screenshot on-failure|always`.
13
+ - CLI temporary data belongs under registered `.draftgo/tmp/` or `.draftgo/artifacts/`; use `draftgo clean --dry-run` before `draftgo clean --yes`.
14
+
6
15
  # DraftGo 开发助手
7
16
 
8
17
  ## 核心边界
@@ -50,8 +59,8 @@ context 会校验本地 Reference,返回逐字章节、来源、实时项目/
50
59
 
51
60
  1. MCP 定位 pages/nav/docs 并读取 metadata。
52
61
  2. 批量 checkout;在 `.draftgo/worktree/` 按唯一 owner 编辑。
53
- 3. 主 Agent 回读全部修改,统一运行 `draftgo check`;页面布局或交互变化时按受众运行 `draftgo verify-ui`。
54
- 4. 用 `draftgo diff <type> <id>` 检查 base/local 差异,再按类型运行 `draftgo commit ...`。
62
+ 3. 主 Agent 回读全部修改,统一运行 `draftgo check`;交付前需要核对远端版本时运行 `draftgo check --remote`。
63
+ 4. 用 `draftgo diff <type> <id>` 检查 base/local 差异,再按类型运行 `draftgo commit ...`。页面布局或交互变化时在 commit 后用 `draftgo verify-ui <url> --resource <type:id>` 验证远端已提交版本。
55
64
  5. 409/412 时停止自动提交,保留 base/local/remote 冲突材料;不得 force、覆盖或自动合并。
56
65
 
57
66
  完整 manifest、流式传输、哈希和冲突规则见 `references/checkout.md`。
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.48",
5
+ "version": "3.0.51",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 context 聚合编排、Skill/reference 原文、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -4,6 +4,12 @@ read_when: 编辑 pages、navigation 或 docs 正文时 · 查看 checkout manif
4
4
 
5
5
  # Checkout / Commit
6
6
 
7
+ ## Workflow 2.0 scope
8
+
9
+ The checkout set includes `pages`, `navigations`, `docs/articles`, and `custom_services`. A custom-service checkout is a directory containing exactly `service.go`, `go.mod`, `go.sum`, and `service.json`, plus one complete `.base` directory. `draftgo commit custom-services <id>` saves a cloud draft; it does not publish. Use `draftgo test custom-services <id>` for server Runner validation/execution and `draftgo publish custom-services <id>` for explicit publication. DB Meta remains a live MCP/API resource and is never checked out.
10
+
11
+ `draftgo refresh <type> <id...>` is a safe checkout shortcut. It updates only a clean local worktree; local changes stop it. A successful commit keeps current local files and one base only. Cloud version storage is the sole history source.
12
+
7
13
  > 根 `SKILL.md` 在 Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明长正文的传输、版本和冲突规则,不能替代页面、前端、运行时或安全资料。
8
14
 
9
15
  ## 适用范围
@@ -23,6 +29,7 @@ db_meta、AIHub、system_config、roles、users、doc_categories 和普通配置
23
29
  ```bash
24
30
  draftgo checkout <pages|nav|docs> <id...>
25
31
  draftgo commit <pages|nav|docs> <id...>
32
+ draftgo reconcile <pages|nav|docs> <id...>
26
33
  draftgo diff <pages|nav|docs> <id>
27
34
  draftgo conflicts
28
35
  draftgo conflict show <pages|nav|docs> <id>
@@ -91,7 +98,7 @@ base 已变化时必须拒绝覆盖。
91
98
  4. 完整正文不得作为 MCP tool 参数发送。
92
99
  5. 底座确认 hash 和新版本后,CLI 原子更新 `.base` 与 manifest。返回 hash 不一致时不得推进基线。
93
100
 
94
- 推荐编辑顺序:主 Agent 执行一次 context -> MCP 按需补充定位 -> Agent 自主设计并批量 checkout -> 独立资源按唯一 owner 并行编辑 -> 主 Agent 汇总回读 -> 统一运行一次 `draftgo check` 和必要的 `draftgo verify-ui` -> `draftgo diff` -> commit
101
+ 推荐编辑顺序:主 Agent 执行一次 context -> MCP 按需补充定位 -> Agent 自主设计并批量 checkout -> 独立资源按唯一 owner 并行编辑 -> 主 Agent 汇总回读 -> 统一运行一次 `draftgo check` -> `draftgo diff` -> commit -> 页面变化时用 `draftgo verify-ui <url> --resource <type:id>` 验证远端已提交版本。遇到本地正文已经等于远端、但 base/manifest 落后时,先用 `draftgo check --remote` 确认 `committed_unrecorded`,再运行 `draftgo reconcile`;不要手改 manifest
95
102
 
96
103
  单个 commit 成功不自动写 changelog。只有整个任务统一验证且全部 commit/MCP 交付成功后,主 Agent 才执行一次 `draftgo changelog add "<完成结果>"`。任何检查失败、409/412 或交付失败都不得写入。
97
104
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+
2
3
  read_when: 编写、修改、调试或评审自定义服务时;使用服务 SDK、路由、事件、定时任务或服务依赖时
3
4
  ---
4
5
 
package/src/cli.js CHANGED
@@ -12,6 +12,8 @@ const VALUE_FLAGS = new Set([
12
12
  'output',
13
13
  'url',
14
14
  'browser',
15
+ 'browser-path',
16
+ 'resource',
15
17
  'mobile-check',
16
18
  'screenshot',
17
19
  'selector',
@@ -27,6 +29,30 @@ const VALUE_FLAGS = new Set([
27
29
  'input',
28
30
  'type',
29
31
  'task',
32
+ 'title',
33
+ 'description',
34
+ 'item-id',
35
+ 'id',
36
+ 'owner',
37
+ 'original',
38
+ 'original-requirement',
39
+ 'clarified',
40
+ 'clarified-requirement',
41
+ 'expected-effect',
42
+ 'effect',
43
+ 'scope',
44
+ 'out-of-scope',
45
+ 'content',
46
+ 'acceptance',
47
+ 'module-logic',
48
+ 'user-journey',
49
+ 'item',
50
+ 'date',
51
+ 'handler',
52
+ 'headers',
53
+ 'user',
54
+ 'side-effect-policy',
55
+ 'evidence',
30
56
  ]);
31
57
 
32
58
  function parse(argv) {
@@ -34,24 +60,24 @@ function parse(argv) {
34
60
  const out = { command: null, positional: [], flags: {}, errors: [] };
35
61
  while (args.length) {
36
62
  const a = args.shift();
37
- if (a === '--') { out.positional.push(...args); break; }
63
+ if (a === '--') {
64
+ out.positional.push(...args);
65
+ break;
66
+ }
38
67
  if (a.startsWith('--')) {
39
68
  const raw = a.slice(2);
40
69
  const eq = raw.indexOf('=');
41
70
  if (eq >= 0) {
42
71
  const key = raw.slice(0, eq);
43
72
  const value = raw.slice(eq + 1);
44
- if (VALUE_FLAGS.has(key) && !value) {
45
- out.errors.push(`选项 --${key} 需要一个值。`);
46
- } else {
47
- out.flags[key] = value;
48
- }
73
+ if (VALUE_FLAGS.has(key) && !value) out.errors.push(`Option --${key} requires a value.`);
74
+ else out.flags[key] = value;
49
75
  } else {
50
76
  const next = args[0];
51
77
  if (VALUE_FLAGS.has(raw)) {
52
78
  // A declared value flag must never silently become boolean true.
53
79
  if (next === undefined || next === '--' || next.startsWith('--')) {
54
- out.errors.push(`选项 --${raw} 需要一个值。`);
80
+ out.errors.push(`Option --${raw} requires a value.`);
55
81
  } else {
56
82
  out.flags[raw] = next;
57
83
  args.shift();
@@ -61,8 +87,7 @@ function parse(argv) {
61
87
  }
62
88
  }
63
89
  } else if (a.startsWith('-') && a.length > 1) {
64
- const key = a.slice(1);
65
- out.flags[key] = true;
90
+ out.flags[a.slice(1)] = true;
66
91
  } else if (!out.command) {
67
92
  out.command = a;
68
93
  } else {
@@ -9,12 +9,19 @@ const commands = [
9
9
  { name: 'status', run: (dir) => require('./commands/status')(dir) },
10
10
  { name: 'context', run: (dir, args, flags) => require('./commands/context')(dir, args, flags) },
11
11
  { name: 'changelog', run: (dir, args, flags) => require('./commands/changelog')(dir, args, flags) },
12
+ { name: 'task', run: (dir, args, flags) => require('./commands/task')(dir, args, flags) },
13
+ { name: 'clean', run: (dir, args, flags) => require('./commands/clean')(dir, args, flags) },
12
14
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
13
- { name: 'check', run: (dir, _args, flags) => require('./commands/check')(dir, flags) },
15
+ { name: 'check', run: (dir, args, flags) => require('./commands/check')(dir, args, flags) },
14
16
  { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
15
17
  { name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
16
18
  { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
19
+ { name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
17
20
  { name: 'commit', run: (dir, args, flags) => require('./commands/commit')(dir, args, flags) },
21
+ { name: 'validate', run: (dir, args, flags) => require('./commands/customService').validate(dir, args, flags) },
22
+ { name: 'test', run: (dir, args, flags) => require('./commands/customService').test(dir, args, flags) },
23
+ { name: 'publish', run: (dir, args, flags) => require('./commands/customService').publish(dir, args, flags) },
24
+ { name: 'reconcile', run: (dir, args, flags) => require('./commands/reconcile')(dir, args, flags) },
18
25
  { name: 'diff', run: (dir, args, flags) => require('./commands/diff')(dir, args, flags) },
19
26
  { name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
20
27
  { name: 'conflict', run: (dir, args, flags) => require('./commands/conflict')(dir, args, flags) },
@@ -1,9 +1,105 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
3
5
  const log = require('../logger');
6
+ const { confirm } = require('../prompt');
4
7
  const { loadProjectConfig } = require('../projectConfig');
5
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
6
9
 
10
+ function readCallInput(projectDir, flags) {
11
+ const raw = flags.input
12
+ ? fs.readFileSync(path.resolve(projectDir, String(flags.input)))
13
+ : flags.params ? Buffer.from(String(flags.params), 'utf8') : Buffer.from('{}', 'utf8');
14
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
15
+ const value = JSON.parse(text);
16
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
17
+ throw new Error('API call input must be a UTF-8 JSON object.');
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function diagnosticResult(operationId, result) {
23
+ const source = result && typeof result === 'object' ? result : { data: result };
24
+ const statusCode = source.status_code ?? source.statusCode ?? source.status ?? null;
25
+ const headerRequestId = source.headers
26
+ && (source.headers['x-request-id'] || source.headers['X-Request-Id']);
27
+ const requestId = source.request_id ?? source.requestId ?? headerRequestId ?? null;
28
+ const error = source.error && typeof source.error === 'object' ? source.error : null;
29
+ return {
30
+ operation_id: operationId,
31
+ status_code: statusCode,
32
+ server_code: error && error.code || source.code || null,
33
+ request_id: requestId,
34
+ result,
35
+ };
36
+ }
37
+
38
+ function diagnosticError(operationId, error) {
39
+ const source = error && (error.details || error.data) || {};
40
+ const nested = source.error && typeof source.error === 'object' ? source.error : {};
41
+ return {
42
+ operation_id: operationId,
43
+ status_code: (error && error.status) ?? source.status_code ?? source.status ?? null,
44
+ server_code: nested.code || source.code || error && error.code || null,
45
+ request_id: source.request_id || source.requestId || nested.request_id || null,
46
+ error: { message: error && error.message || String(error) },
47
+ };
48
+ }
49
+
50
+ function printCallError(operationId, error, flags) {
51
+ const diagnostic = diagnosticError(operationId, error);
52
+ if (flags.output === 'json') console.log(JSON.stringify(diagnostic, null, 2));
53
+ else {
54
+ log.err(`DraftGo operation failed: ${operationId}`);
55
+ log.info(`status=${diagnostic.status_code ?? '-'} code=${diagnostic.server_code ?? '-'} request_id=${diagnostic.request_id ?? '-'}`);
56
+ log.err(diagnostic.error.message);
57
+ }
58
+ return 1;
59
+ }
60
+
61
+ async function callOperation(projectDir, operationId, flags = {}) {
62
+ if (!operationId) {
63
+ log.err('Usage: draftgo api call <operation_id> --input <json-file>');
64
+ return 1;
65
+ }
66
+ let session;
67
+ let description;
68
+ let input;
69
+ try {
70
+ const config = loadProjectConfig(projectDir);
71
+ session = await openToolSession(config, [TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
72
+ description = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
73
+ input = readCallInput(projectDir, flags);
74
+ } catch (error) {
75
+ return printCallError(operationId, error, flags);
76
+ }
77
+ const destructive = description && (description.destructive === true
78
+ || description.operation && description.operation.destructive === true);
79
+ if (destructive && !flags.yes && !flags.y) {
80
+ const approved = await confirm(`Call destructive DraftGo operation ${operationId}?`, { default: false });
81
+ if (!approved) return 0;
82
+ }
83
+ let result;
84
+ try {
85
+ result = await callStructured(session, TOOL_NAMES.apiCall, {
86
+ ...input,
87
+ operation_id: operationId,
88
+ ...(destructive ? { confirm: true } : {}),
89
+ });
90
+ } catch (error) {
91
+ return printCallError(operationId, error, flags);
92
+ }
93
+ const diagnostic = diagnosticResult(operationId, result);
94
+ if (flags.output === 'json') console.log(JSON.stringify(diagnostic, null, 2));
95
+ else {
96
+ log.ok(`DraftGo operation completed: ${operationId}`);
97
+ log.info(`status=${diagnostic.status_code ?? '-'} code=${diagnostic.server_code ?? '-'} request_id=${diagnostic.request_id ?? '-'}`);
98
+ console.log(JSON.stringify(result, null, 2));
99
+ }
100
+ return 0;
101
+ }
102
+
7
103
  function printResult(title, result) {
8
104
  log.title(title);
9
105
  if (Array.isArray(result)) {
@@ -19,6 +115,7 @@ function printResult(title, result) {
19
115
 
20
116
  async function apiCommand(projectDir, positional, flags = {}) {
21
117
  const first = String(positional[0] || '').toLowerCase();
118
+ if (first === 'call') return callOperation(projectDir, String(positional[1] || ''), flags);
22
119
  const describe = first === 'describe';
23
120
  const queryParts = first === 'search' || describe ? positional.slice(1) : positional;
24
121
  const query = queryParts.join(' ').trim();
@@ -39,3 +136,8 @@ async function apiCommand(projectDir, positional, flags = {}) {
39
136
  }
40
137
 
41
138
  module.exports = apiCommand;
139
+ module.exports.readCallInput = readCallInput;
140
+ module.exports.diagnosticResult = diagnosticResult;
141
+ module.exports.diagnosticError = diagnosticError;
142
+ module.exports.printCallError = printCallError;
143
+ module.exports.callOperation = callOperation;
@@ -37,7 +37,7 @@ async function autoPush(projectDir, positional, flags = {}) {
37
37
  return 1;
38
38
  }
39
39
 
40
- const checkCode = check(projectDir, { strict: flags.strict });
40
+ const checkCode = await check(projectDir, { strict: flags.strict });
41
41
  if (checkCode !== 0) {
42
42
  log.err('auto-push stopped because local validation failed.');
43
43
  return checkCode;
@@ -4,16 +4,85 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const log = require('../logger');
6
6
  const { analyzeProject } = require('../projectMap');
7
+ const { inspectRemoteCheckouts } = require('../worktree/status');
8
+ const customServices = require('../customServices');
9
+ const { workspaceHealth } = require('../workspaceHealth');
7
10
 
8
11
  function legacyCacheDetected(projectDir) {
9
12
  return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
10
13
  .some((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
11
14
  }
12
15
 
13
- function check(projectDir, flags = {}) {
16
+ async function check(projectDir, positional = [], flags = {}) {
17
+ if (!Array.isArray(positional)) {
18
+ flags = positional || {};
19
+ positional = [];
20
+ }
21
+ const [requestedType, ...requestedIds] = positional;
22
+ if (requestedType && !customServicesCommand().isServiceType(requestedType)) {
23
+ log.err('Usage: draftgo check [custom-services <id...>] [--remote]');
24
+ return 1;
25
+ }
26
+ if (requestedType && !requestedIds.length) {
27
+ log.err('Usage: draftgo check custom-services <id...> [--remote]');
28
+ return 1;
29
+ }
14
30
  const result = analyzeProject(projectDir);
31
+ const hygiene = workspaceHealth(projectDir);
32
+ hygiene.warnings.forEach((warning) => result.warningDetails.push({ code: warning.code, confidence: 'high', message: `${warning.path}${warning.size ? ` (${warning.size} bytes)` : ''}` }));
33
+ hygiene.warnings.forEach((warning) => result.warnings.push(`${warning.code}: ${warning.path}`));
34
+ const serviceManifest = customServices.loadManifest(projectDir);
35
+ const customServiceChecks = [];
36
+ const requested = new Set(requestedIds.map(String));
37
+ const serviceEntries = Object.values(serviceManifest.entries)
38
+ .filter((entry) => !requestedType || requested.has(String(entry.resource_id)));
39
+ if (requestedType) {
40
+ for (const id of requested) {
41
+ if (!serviceEntries.some((entry) => String(entry.resource_id) === id)) {
42
+ result.errors.push(`custom_services ${id}: resource is not checked out`);
43
+ }
44
+ }
45
+ }
46
+ for (const entry of serviceEntries) {
47
+ try {
48
+ const state = customServices.diff(projectDir, entry.resource_id);
49
+ customServiceChecks.push({ resource_type: 'custom_services', resource_id: entry.resource_id, state: state.changed ? 'local_modified' : 'clean_local', files: state.files });
50
+ } catch (error) {
51
+ result.errors.push(`custom_services ${entry.resource_id}: ${error.message}`);
52
+ }
53
+ }
54
+ let remote = null;
55
+ let remoteError = null;
56
+ if (flags.remote) {
57
+ try {
58
+ const [contentRemote, serviceRemote] = await Promise.all([
59
+ inspectRemoteCheckouts(projectDir),
60
+ customServices.inspectRemote(projectDir, requestedType ? { ids: [...requested] } : {}),
61
+ ]);
62
+ remote = requestedType
63
+ ? serviceRemote.filter((entry) => requested.has(String(entry.resource_id)))
64
+ : [...contentRemote, ...serviceRemote];
65
+ for (const entry of remote) {
66
+ if (!['clean', 'clean_local', 'local_modified'].includes(entry.state)) {
67
+ const detail = `${entry.resource_type} ${entry.resource_id}: ${entry.state}`
68
+ + ` (local=${entry.local_hash || '-'}, base=${entry.manifest_hash || '-'}, `
69
+ + `remote=${entry.remote_hash || '-'}, remote_version=${entry.remote_version || '-'})`
70
+ + (entry.recommendation ? `; run ${entry.recommendation}` : '');
71
+ result.errors.push(detail);
72
+ }
73
+ }
74
+ } catch (error) {
75
+ remoteError = { code: error.code || 'REMOTE_CHECK_FAILED', message: error.message };
76
+ result.errors.push(`Remote checkout check failed: ${error.message}`);
77
+ }
78
+ }
79
+ const output = {
80
+ content_validation: { map: result.map, errors: result.errors, warnings: result.warnings, warning_details: result.warningDetails, custom_services: customServiceChecks },
81
+ remote_validation: { resources: remote, error: remoteError },
82
+ workspace_hygiene: hygiene,
83
+ };
15
84
  if (flags.output === 'json') {
16
- console.log(JSON.stringify({ ...result, legacy_cache_ignored: legacyCacheDetected(projectDir) }, null, 2));
85
+ console.log(JSON.stringify({ ...output, legacy_cache_ignored: legacyCacheDetected(projectDir) }, null, 2));
17
86
  return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
18
87
  }
19
88
 
@@ -21,6 +90,10 @@ function check(projectDir, flags = {}) {
21
90
  log.info(`Checked-out pages: ${result.map.pages.length}`);
22
91
  log.info(`Checked-out navigations: ${result.map.navigations.length}`);
23
92
  log.info(`Checked-out docs: ${result.map.docs.length}`);
93
+ log.info(`Checked-out custom services: ${customServiceChecks.length}`);
94
+ log.info(`Workspace: ${hygiene.managed_files} managed, ${hygiene.unknown_files} unknown, ${hygiene.temporary_files} temporary, ${hygiene.artifact_files} artifact file(s)`);
95
+ log.info(`Workspace size: ${hygiene.total_bytes} bytes; reclaimable: ${hygiene.reclaimable_bytes} bytes`);
96
+ if (remote) log.info(`Remote checkout comparison: ${remote.length} resource(s)`);
24
97
  for (const message of result.errors) log.err(message);
25
98
  for (const detail of result.warningDetails) log.warn(`[${detail.code}/${detail.confidence}] ${detail.message}`);
26
99
  if (legacyCacheDetected(projectDir)) log.dim(' Legacy .draftgo index caches were detected and ignored.');
@@ -30,4 +103,8 @@ function check(projectDir, flags = {}) {
30
103
  return 0;
31
104
  }
32
105
 
106
+ function customServicesCommand() {
107
+ return require('./customService');
108
+ }
109
+
33
110
  module.exports = check;
@@ -9,6 +9,9 @@ async function checkout(projectDir, positional, flags = {}) {
9
9
  log.err('Usage: draftgo checkout <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
+ if (require('./customService').isServiceType(resourceType)) {
13
+ return require('./customService').checkout(projectDir, ids, flags);
14
+ }
12
15
  const results = await checkoutResources(projectDir, resourceType, ids, { force: Boolean(flags.force) });
13
16
  if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
14
17
  else for (const entry of results) log.ok(`Checked out ${entry.resource_type} ${entry.resource_id} -> ${entry.local_path}`);
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const log = require('../logger');
6
+ const runtime = require('../runtimeFiles');
7
+
8
+ const MANAGED_PREFIXES = ['tmp/', 'artifacts/ui/', 'artifacts/api/', 'artifacts/custom-services/'];
9
+ function fileSize(file) { try { const stat = fs.statSync(file); return stat.isFile() ? stat.size : 0; } catch { return 0; } }
10
+ function olderThan(value) {
11
+ if (value == null || value === '') return 0;
12
+ const match = String(value).match(/^(\d+)([dhm])$/i); if (!match) throw new Error('--older-than must use <number>d, <number>h, or <number>m.');
13
+ const unit = { d: 86400000, h: 3600000, m: 60000 }[match[2].toLowerCase()]; return Number(match[1]) * unit;
14
+ }
15
+ function plan(projectDir, flags = {}) {
16
+ const root = runtime.draftgoRoot(projectDir); const manifest = runtime.load(projectDir); const age = olderThan(flags['older-than']); const now = Date.now();
17
+ const types = new Set(String(flags.type || '').split(',').map((item) => item.trim()).filter(Boolean)); const candidates = [];
18
+ for (const entry of manifest.entries) {
19
+ if (!entry.cleanable || (types.size && !types.has(entry.type))) continue;
20
+ const relative = String(entry.path).replace(/\\/g, '/');
21
+ if (!MANAGED_PREFIXES.some((prefix) => relative.startsWith(prefix))) continue;
22
+ const absolute = path.resolve(root, relative); if (!runtime.inside(root, absolute) || !fs.existsSync(absolute)) continue;
23
+ const stat = fs.statSync(absolute); if (age && now - stat.mtimeMs < age) continue;
24
+ candidates.push({ ...entry, absolute, size: stat.isFile() ? stat.size : 0, reason: 'runtime_manifest' });
25
+ }
26
+ if (flags['all-artifacts']) {
27
+ for (const prefix of MANAGED_PREFIXES.filter((item) => item.startsWith('artifacts/'))) {
28
+ const directory = path.resolve(root, prefix); if (!runtime.inside(root, directory) || !fs.existsSync(directory)) continue;
29
+ const visit = (current) => { for (const child of fs.readdirSync(current, { withFileTypes: true })) { const absolute = path.join(current, child.name); if (child.isDirectory()) visit(absolute); else if (child.isFile() && !candidates.some((entry) => entry.absolute === absolute)) candidates.push({ path: path.relative(root, absolute).replace(/\\/g, '/'), absolute, type: 'artifact', size: fileSize(absolute), reason: 'all_artifacts' }); } };
30
+ visit(directory);
31
+ }
32
+ }
33
+ return { entries: candidates, count: candidates.length, bytes: candidates.reduce((sum, entry) => sum + entry.size, 0) };
34
+ }
35
+ async function clean(projectDir, _positional, flags = {}) {
36
+ const result = plan(projectDir, flags); const output = { count: result.count, bytes: result.bytes, entries: result.entries.map(({ absolute, ...entry }) => entry) };
37
+ if (flags.output === 'json') console.log(JSON.stringify({ ...output, deleted: Boolean(flags.yes && !flags['dry-run']) }, null, 2));
38
+ else { log.title('draftgo clean'); result.entries.forEach((entry) => log.info(`${entry.path} (${entry.size} bytes)`)); log.info(`Reclaimable: ${result.bytes} bytes in ${result.count} file(s).`); }
39
+ if (!flags.yes || flags['dry-run']) { if (!flags.output) log.dim('Run with --yes to delete this plan.'); return 0; }
40
+ const manifest = runtime.load(projectDir); const removed = new Set();
41
+ for (const entry of result.entries) { if (!runtime.inside(runtime.draftgoRoot(projectDir), entry.absolute)) throw new Error('Clean target escaped .draftgo.'); fs.rmSync(entry.absolute, { force: true }); removed.add(entry.path); }
42
+ manifest.entries = manifest.entries.filter((entry) => !removed.has(entry.path)); runtime.save(projectDir, manifest);
43
+ if (!flags.output) log.ok(`Deleted ${result.count} managed file(s).`); return 0;
44
+ }
45
+ clean.plan = plan;
46
+ module.exports = clean;
@@ -9,11 +9,40 @@ async function commit(projectDir, positional, flags = {}) {
9
9
  log.err('Usage: draftgo commit <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
- const results = await commitResources(projectDir, resourceType, ids);
13
- if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
14
- else for (const result of results) {
15
- if (result.status === 'unchanged') log.dim(`${result.resource_type} ${result.resource_id}: unchanged`);
16
- else log.ok(`Committed ${result.resource_type} ${result.resource_id}`);
12
+ if (require('./customService').isServiceType(resourceType)) {
13
+ return require('./customService').commit(projectDir, ids, flags);
14
+ }
15
+ const streamed = [];
16
+ const printStatus = (result) => {
17
+ streamed.push(result);
18
+ if (flags.output === 'json') return;
19
+ if (result.status === 'committed') log.ok(`${result.resource_type} ${result.resource_id}: committed`);
20
+ else if (result.status === 'unchanged') log.dim(`${result.resource_type} ${result.resource_id}: unchanged`);
21
+ else if (result.status === 'failed') log.err(`${result.resource_type} ${result.resource_id}: failed (${result.code}) - ${result.message}`
22
+ + (result.remote_change_possible ? ' Remote change is possible; run draftgo check --remote.' : ''));
23
+ else log.warn(`${result.resource_type} ${result.resource_id}: not started`);
24
+ };
25
+ try {
26
+ const results = await commitResources(projectDir, resourceType, ids, { onStatus: printStatus });
27
+ if (flags.output === 'json') console.log(JSON.stringify({
28
+ completed: results.filter((item) => ['committed', 'unchanged'].includes(item.status)),
29
+ failed: [],
30
+ not_started: [],
31
+ }, null, 2));
32
+ } catch (error) {
33
+ const batch = error.details && (error.details.batch || error.details);
34
+ const summary = batch && Array.isArray(batch.completed) ? batch : {
35
+ completed: streamed.filter((item) => ['committed', 'unchanged'].includes(item.status)),
36
+ failed: streamed.filter((item) => item.status === 'failed'),
37
+ not_started: streamed.filter((item) => item.status === 'not_started'),
38
+ };
39
+ if (flags.output === 'json') console.log(JSON.stringify({ error: {
40
+ code: error.code || 'COMMIT_FAILED', message: error.message,
41
+ }, ...summary }, null, 2));
42
+ else log.err(`Commit batch stopped: ${summary.completed.length} completed, `
43
+ + `${summary.failed.length} failed, ${summary.not_started.length} not started. `
44
+ + `${summary.completed.length ? 'Remote changes already occurred for completed resources.' : 'No remote content was changed.'}`);
45
+ return 1;
17
46
  }
18
47
  return 0;
19
48
  }