draftgo-cli 3.0.51 → 3.0.52

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
@@ -6,7 +6,7 @@
6
6
 
7
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
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.
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`. `.draftgo/tmp/` is entirely disposable; user-facing evidence belongs in registered `.draftgo/artifacts/`, and `draftgo clean` removes all tmp plus registered artifacts. UI smoke checks remain single viewport and use `--screenshot never` by default. Supply `on-failure` or `always` only for explicit strong visual verification.
10
10
 
11
11
  面向 AI 编码工具的 DraftGo 工作台 CLI:分发 DraftGo Skill,通过 MCP 发现和操作实时资源,并为页面、导航和文档提供可校验的 `checkout` / `commit` 工作流。
12
12
 
@@ -80,7 +80,7 @@ draftgo connect codex --server https://draftgo.example --token "$DRAFTGO_SAT"
80
80
 
81
81
  `--server` 始终表示 DraftGo 基础地址。直接连接 MCP endpoint 时使用 `--mcp-url https://draftgo.example/mcp`;CLI 会显式保存 `mcp_url`,若未同时传 `--server`,则从标准 `/mcp` 后缀推导基础地址,避免猜测已有 base URL 的语义。
82
82
 
83
- `connect` 会先验证 SAT、MCP `initialize`、`tools/list` 和关键 `tools/call`,成功后写入项目私有配置 `.draftgo/config.json`,并尝试为指定或自动检测到的宿主执行 MCP setup。它不会 pull 页面、导航、文档或任何其他业务资源。
83
+ `connect` 会先验证 SAT、MCP `initialize`、`tools/list` 和关键 `tools/call`,成功后写入项目私有配置 `.draftgo/config.json`,并尝试为指定或自动检测到的宿主执行 MCP setup。`init` / `connect` 不会预建 Task、lessons、changelog、worktree、conflicts 等运行时内容;这些文件和目录只在对应能力首次使用时按需创建。它不会 pull 页面、导航、文档或任何其他业务资源。
84
84
 
85
85
  远端暂不可用时,只有显式传入 `--allow-offline` 才会保存配置;恢复后运行 `draftgo mcp test`。使用 `--no-mcp-setup` 可跳过宿主配置。
86
86
 
@@ -178,8 +178,7 @@ draftgo context --task frontend --output json
178
178
  draftgo checkout pages 42 43
179
179
  # 独立资源可并行编辑;同一资源只由一个 Agent 修改
180
180
  draftgo check
181
- draftgo check --remote # 交付前按需对比远端版本/hash
182
- draftgo verify-ui http://localhost:5173 # UI 变更时
181
+ draftgo verify pages 42 --remote --url http://localhost:5173/example
183
182
  draftgo diff pages 42
184
183
  draftgo diff pages 43
185
184
  draftgo commit pages 42 43
@@ -213,13 +212,14 @@ draftgo changelog add "完成文档管理系统及角色权限"
213
212
  | `draftgo map` | 并行读取 pages、navigations、docs/articles 元数据,再叠加本地 checkout 状态;`--type` 可限制一种类型。 |
214
213
  | `draftgo checkout <pages|nav|docs> <id...>` | 下载完整正文和 base,校验 hash 后原子落盘;`--force` 可显式覆盖已修改的本地正文。 |
215
214
  | `draftgo check` | 默认只检查本地 checkout;`--remote` 额外对比 local/base/manifest/remote 的版本和 hash。 |
215
+ | `draftgo verify [<type> <id...>]` | 统一执行静态检查;`--remote` 增加远端比较,`--url` 增加 iframe-aware 浏览器验证。 |
216
216
  | `draftgo diff <pages|nav|docs> <id>` | 显示 checkout base 与当前本地正文的差异。 |
217
217
  | `draftgo commit <pages|nav|docs> <id...>` | 本地校验后,通过专用 HTTP 上传完整正文并更新 base/manifest。 |
218
218
  | `draftgo reconcile <pages|nav|docs> <id...>` | 仅当 local 与经校验的 remote 字节一致时,安全修复落后的 base/manifest 元数据。 |
219
219
  | `draftgo conflicts` | 列出未解决冲突;存在冲突时返回非零。`--all` 包括已解决记录。 |
220
220
  | `draftgo conflict show <type> <id>` | 显示冲突的 base/local/remote/worktree 路径。 |
221
221
  | `draftgo conflict resolve <type> <id>` | 确认 worktree 已合并,把保留的 remote 设为新 base;随后重新 check、diff、commit。 |
222
- | `draftgo verify-ui <url>` | 使用 Playwright 执行确定性的浏览器 smoke check。 |
222
+ | `draftgo verify-ui <url>` | `verify --url <url> --ui always` 的弃用兼容入口。 |
223
223
 
224
224
  `pages`、`page`,`nav`、`navigation`、`navigations`,以及 `docs`、`doc`、`article`、`articles`、`docs/articles` 都会规范化到三个支持的正文类型。
225
225
 
@@ -316,7 +316,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
316
316
  1. `draftgo conflict show <type> <id>` 定位三份材料和 worktree 文件。
317
317
  2. 在 worktree 文件中人工或借助可靠的三方合并工具完成合并。
318
318
  3. `draftgo conflict resolve <type> <id>` 校验 worktree,并把冲突时保存的 remote 版本设为新 base。
319
- 4. 重新运行 `draftgo check`、必要时 `verify-ui`、`draftgo diff`,最后 `draftgo commit`。
319
+ 4. 重新运行 `draftgo check`、必要时 `draftgo verify`、`draftgo diff`,最后 `draftgo commit`。
320
320
 
321
321
  ## map 与 check 输出
322
322
 
@@ -332,7 +332,11 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
332
332
  }
333
333
  ```
334
334
 
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`。它始终不会读取旧全量资源索引。
335
+ `draftgo check --output json` 返回结构化的本地、远端和工作区检查结果。它以声明的 charset(默认 UTF-8)严格解码 HTML,并在内存中检查内联 JavaScript,错误直接映射到原 HTML 行列,不生成 `.draftgo/tmp/syntax/*.js`。`--strict` 会把 warnings 也视为失败。默认 `check` 不访问远端;显式使用 `--remote` 时读取实时 metadata,并把 `committed_unrecorded` 等可恢复状态指向 `draftgo reconcile`。
336
+
337
+ `draftgo verify` 是交付前统一入口。无参数时只运行快速本地 `check`;`--remote` 才联网,`--url` 才启动浏览器。浏览器检查默认使用 `--frame auto`,同时检查顶层页面和可见 iframe,因此 `--selector` 可匹配 `iframe.srcdoc` 中的业务 DOM。`--viewport mobile|desktop|both` 控制目标视口,截图默认关闭。
338
+
339
+ `.draftgo/tmp/` 是统一临时区,CLI 和 AI 产生的可丢弃中间文件必须位于 `.draftgo/tmp/<owner>/<run-id>/`;AI 自建文件使用 `.draftgo/tmp/ai/<run-id>/`。`draftgo clean --dry-run` 会列出全部 tmp 文件和已注册 artifacts,`draftgo clean --yes` 负责回收并删除空运行目录。需要保留供用户查看的截图或报告应进入 `.draftgo/artifacts/`,而不是 tmp。
336
340
 
337
341
  ## Skill 与领域约定
338
342
 
@@ -364,10 +368,12 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
364
368
  - `--mcp-url <url>`:为 connect 显式指定完整 MCP endpoint;标准 `/mcp` 可自动推导基础地址,自定义路径需同时传 `--server`。
365
369
  - `--allow-offline` / `--no-mcp-setup`:允许 connect 在 MCP 暂不可用时保存,或跳过宿主配置。
366
370
  - `--yes`:跳过支持该选项的交互确认。
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 缓存。
370
- - `--token auto|never`:`verify-ui` 默认读取 `.draftgo/config.json`,对与 `server` 同源的地址自动附加 `token=<SAT>`;配置无 SAT、跨源地址或 `never` 模式均不附加。
371
+ - `--ui auto|always|never`:控制 verify 的浏览器验证;默认 auto,只在提供 `--url` 时运行。
372
+ - `--viewport mobile|desktop|both`:控制 verify 的目标视口;不传时使用 `--width/--height` 390x844。
373
+ - `--frame auto|top|all|<iframe-selector>`:控制 verify 检查哪些 frame;默认检查顶层与可见 iframe。
374
+ - `--resource <type:id>`:兼容入口使用;推荐直接写 `draftgo verify pages 42 --url ...`。
375
+ - `--browser-path <file>`:verify 显式使用浏览器可执行文件;也可设置 `DRAFTGO_BROWSER_PATH`,否则自动查找系统浏览器和 Playwright 缓存。
376
+ - `--token auto|never`:verify 默认读取 `.draftgo/config.json`,对与 `server` 同源的地址自动附加 `token=<SAT>`;配置无 SAT、跨源地址或 `never` 模式均不附加。
371
377
  - `--screenshot on-failure|always|never`:控制 UI 截图。
372
378
  - `--delivery local|preview|deploy`:控制 deploy 行为。
373
379
  - `--timeout <ms>`:控制 connect、MCP test 或 context 的远端请求超时。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.51",
3
+ "version": "3.0.52",
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"
@@ -9,8 +9,8 @@ description: Use this skill to inspect, develop, debug, or deliver a DraftGo app
9
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
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
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`.
12
+ - Normal delivery uses `draftgo verify`; add `--remote` only when remote comparison is required and `--url` only for browser evidence. UI verification is iframe-aware and defaults to one viewport with no screenshot. Only explicit visual acceptance or regression diagnosis may pass `--viewport both` or `--screenshot on-failure|always`.
13
+ - `.draftgo/tmp/` is entirely disposable. CLI data uses `.draftgo/tmp/<command>/<run-id>/`; AI-created scratch files use `.draftgo/tmp/ai/<run-id>/`. User-facing evidence belongs under registered `.draftgo/artifacts/`. Use `draftgo clean --dry-run` before `draftgo clean --yes`.
14
14
 
15
15
  # DraftGo 开发助手
16
16
 
@@ -59,8 +59,8 @@ context 会校验本地 Reference,返回逐字章节、来源、实时项目/
59
59
 
60
60
  1. MCP 定位 pages/nav/docs 并读取 metadata。
61
61
  2. 批量 checkout;在 `.draftgo/worktree/` 按唯一 owner 编辑。
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>` 验证远端已提交版本。
62
+ 3. 主 Agent 回读全部修改,编辑过程中统一运行一次快速 `draftgo check`。交付前运行 `draftgo verify <type> <id...>`;需要远端证据时加 `--remote`,页面布局或交互变化时加对应 `--url`。
63
+ 4. 用 `draftgo diff <type> <id>` 检查 base/local 差异,再按类型运行 `draftgo commit ...`。commit 后需要验证远端页面时运行 `draftgo verify <type> <id> --remote --url <url>`。
64
64
  5. 409/412 时停止自动提交,保留 base/local/remote 冲突材料;不得 force、覆盖或自动合并。
65
65
 
66
66
  完整 manifest、流式传输、哈希和冲突规则见 `references/checkout.md`。
@@ -93,10 +93,10 @@ draftgo changelog add "<完成结果>"
93
93
  | 范围 | 最低证据 |
94
94
  |---|---|
95
95
  | MCP 连接 | 失败时用 `draftgo mcp test` 核对 initialize、tools/list 和关键工具;输出不含 SAT |
96
- | 页面、导航、文档 | checkout manifest、`draftgo check`、必要的 `verify-ui`、diff、commit 返回版本与哈希 |
96
+ | 页面、导航、文档 | checkout manifest、`draftgo check`、交付前 `draftgo verify`、diff、commit 返回版本与哈希 |
97
97
  | 结构化资源 | search/describe 契约、call 结果与回读的权限、字段、错误和副作用 |
98
98
  | 自定义服务 | 动态 Route describe、无副作用健康请求或目标行为结果,以及管理员调用审计 |
99
- | 冲突 | `.draftgo/conflicts/` 的 base/local/remote;解决后重新 check、必要的 verify-ui 和 commit |
99
+ | 冲突 | `.draftgo/conflicts/` 的 base/local/remote;解决后重新 check、必要的 verify 和 commit |
100
100
  | 本地底座 | `draftgo local status`、日志和相关 local 测试 |
101
101
 
102
102
  `draftgo deploy`、`auto-push` 和兼容 `push pages|nav|docs` 只能沿用当前 check/commit 语义,不能恢复旧的全量 push。主 Agent 最终明确区分 Skill 约束、MCP 实时状态和 checkout 正文证据。
@@ -21,6 +21,8 @@ description: Install or refresh the DraftGo skill, connect a project to a DraftG
21
21
 
22
22
  draftgo init 只负责 Skill 安装;draftgo connect 负责保存并验证 server/SAT;draftgo mcp setup 负责宿主配置。不要混用职责。
23
23
 
24
+ 初始化后 `.draftgo/` 只保留已经写入的 `config.json`。不要预建 Task、lessons、changelog、worktree、conflicts 或其他占位文件和目录;首次实际使用对应能力时再创建。
25
+
24
26
  ## SAT 与配置
25
27
 
26
28
  - 交互式连接应使用隐藏输入;不要把 SAT 写入宿主配置、Skill 文件、命令行示例或日志。
@@ -33,7 +35,7 @@ draftgo init 只负责 Skill 安装;draftgo connect 负责保存并验证 serv
33
35
  - 宿主配置和诊断:draftgo mcp setup/status/test,详见 ../references/mcp.md。
34
36
  - 开发上下文:先运行匹配领域的 `draftgo context --task <task> --output json`;context 后仍可按需直接调用 MCP。
35
37
  - 远端资源补充定位:MCP project_overview、resource_search、resource_list。
36
- - 正文编辑:自主设计并批量 checkout,不同资源按唯一 owner 并行修改,由主 Agent 统一运行 check/必要的 verify-ui、diff 和 commit。
38
+ - 正文编辑:自主设计并批量 checkout,不同资源按唯一 owner 并行修改,由主 Agent 统一运行 check、交付前 verify、diff 和 commit。
37
39
  - 结构化资源:直接使用 MCP 的 api_search、api_describe、api_call;不 checkout、不生成本地镜像。
38
40
  - 完成日志:只有整个任务统一验证且全部 commit/MCP 交付成功后,由主 Agent 执行一次 `draftgo changelog add "<完成结果>"`;验证、冲突或交付失败时不得写入。
39
41
  - 管理本地基座:draftgo local start|stop|logs|status;local-dev、local up/down 仍可用。
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.51",
5
+ "version": "3.0.52",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 context 聚合编排、Skill/reference 原文、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -93,12 +93,12 @@ base 已变化时必须拒绝覆盖。
93
93
  ## Commit 流程
94
94
 
95
95
  1. 读取 manifest 指向的 worktree 文件,计算当前字节数和 SHA-256;未变化时返回 `unchanged`。
96
- 2. 按 content_type 执行本地结构检查;页面布局或交互有变化时另外运行 `draftgo verify-ui`。
96
+ 2. 按 content_type 执行本地结构和内联脚本检查;页面布局或交互有变化时运行 `draftgo verify <type> <id> --url <url>`。
97
97
  3. 通过专用 HTTP 流式上传原始文件,携带 `If-Match`、base version/revision、content_type、长度和 SHA-256。
98
98
  4. 完整正文不得作为 MCP tool 参数发送。
99
99
  5. 底座确认 hash 和新版本后,CLI 原子更新 `.base` 与 manifest。返回 hash 不一致时不得推进基线。
100
100
 
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。
101
+ 推荐编辑顺序:主 Agent 执行一次 context -> MCP 按需补充定位 -> Agent 自主设计并批量 checkout -> 独立资源按唯一 owner 并行编辑 -> 主 Agent 汇总回读 -> 统一运行一次 `draftgo check` -> `draftgo diff` -> commit -> 页面变化时用 `draftgo verify <type> <id> --remote --url <url>` 验证远端已提交版本。遇到本地正文已经等于远端、但 base/manifest 落后时,先用 `draftgo check --remote` 确认 `committed_unrecorded`,再运行 `draftgo reconcile`;不要手改 manifest。
102
102
 
103
103
  单个 commit 成功不自动写 changelog。只有整个任务统一验证且全部 commit/MCP 交付成功后,主 Agent 才执行一次 `draftgo changelog add "<完成结果>"`。任何检查失败、409/412 或交付失败都不得写入。
104
104
 
@@ -117,7 +117,7 @@ base 已变化时必须拒绝覆盖。
117
117
  - `base` 是 checkout 时的内容;`local` 是发生冲突时的本地快照;`remote` 是重新下载并校验的当前远端内容。
118
118
  - `conflict.json` 记录三份路径、版本、ETag 和哈希,不嵌入完整正文。
119
119
  - Agent 或用户在 worktree local 文件中完成合并;不要手写 HTML 自动合并器,也不要改动保存的三份证据。
120
- - 合并完成后运行 `draftgo check`、必要时 `draftgo verify-ui`,再执行 `draftgo conflict resolve <type> <id>`。
120
+ - 合并完成后运行 `draftgo check`、必要时 `draftgo verify`,再执行 `draftgo conflict resolve <type> <id>`。
121
121
  - resolve 校验 worktree 与 remote,采用 remote 版本作为新的 base,但保留合并后的 worktree;随后运行
122
122
  `draftgo diff` 并 `draftgo commit`。
123
123
 
@@ -68,7 +68,7 @@ version: 2.0.0
68
68
  - 表格、代码和其他超宽内容应在合理边界内可访问,不能让核心操作因溢出而不可达。
69
69
  - 弹窗和抽屉在目标视口内完整可用,长内容有明确滚动边界。
70
70
 
71
- 需要浏览器证据时按页面受众和现有断点运行 `draftgo verify-ui <url> --mobile-check auto`;不要机械遍历与任务无关的视口。
71
+ 需要浏览器证据时按页面受众和现有断点运行 `draftgo verify <type> <id> --url <url>`;只有验收确实覆盖多端时加 `--viewport both`,不要机械遍历与任务无关的视口。业务 HTML 位于 iframe 时默认 `--frame auto` 会检查顶层和可见 iframe,`--selector` 可直接定位业务 DOM。
72
72
 
73
73
  ### 导航与系统页边界(强制)
74
74
 
package/src/cli.js CHANGED
@@ -15,6 +15,9 @@ const VALUE_FLAGS = new Set([
15
15
  'browser-path',
16
16
  'resource',
17
17
  'mobile-check',
18
+ 'ui',
19
+ 'frame',
20
+ 'viewport',
18
21
  'screenshot',
19
22
  'selector',
20
23
  'width',
@@ -13,7 +13,8 @@ const commands = [
13
13
  { name: 'clean', run: (dir, args, flags) => require('./commands/clean')(dir, args, flags) },
14
14
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
15
15
  { name: 'check', run: (dir, args, flags) => require('./commands/check')(dir, args, flags) },
16
- { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
16
+ { name: 'verify', run: (dir, args, flags) => require('./commands/verify')(dir, args, flags) },
17
+ { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUiCompat')(dir, args, flags) },
17
18
  { name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
18
19
  { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
19
20
  { name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
@@ -27,7 +27,15 @@ async function check(projectDir, positional = [], flags = {}) {
27
27
  log.err('Usage: draftgo check custom-services <id...> [--remote]');
28
28
  return 1;
29
29
  }
30
- const result = analyzeProject(projectDir);
30
+ const resourceKeys = flags.resourceKeys || null;
31
+ const result = analyzeProject(projectDir, { resourceKeys });
32
+ if (resourceKeys) {
33
+ const manifestKeys = new Set(Object.keys(result.map.checkouts.reduce((entries, entry) => {
34
+ entries[`${entry.resource_type}:${entry.resource_id}`] = true;
35
+ return entries;
36
+ }, {})));
37
+ for (const key of resourceKeys) if (!manifestKeys.has(key)) result.errors.push(`${key}: resource is not checked out`);
38
+ }
31
39
  const hygiene = workspaceHealth(projectDir);
32
40
  hygiene.warnings.forEach((warning) => result.warningDetails.push({ code: warning.code, confidence: 'high', message: `${warning.path}${warning.size ? ` (${warning.size} bytes)` : ''}` }));
33
41
  hygiene.warnings.forEach((warning) => result.warnings.push(`${warning.code}: ${warning.path}`));
@@ -35,7 +43,7 @@ async function check(projectDir, positional = [], flags = {}) {
35
43
  const customServiceChecks = [];
36
44
  const requested = new Set(requestedIds.map(String));
37
45
  const serviceEntries = Object.values(serviceManifest.entries)
38
- .filter((entry) => !requestedType || requested.has(String(entry.resource_id)));
46
+ .filter((entry) => !resourceKeys && (!requestedType || requested.has(String(entry.resource_id))));
39
47
  if (requestedType) {
40
48
  for (const id of requested) {
41
49
  if (!serviceEntries.some((entry) => String(entry.resource_id) === id)) {
@@ -56,8 +64,12 @@ async function check(projectDir, positional = [], flags = {}) {
56
64
  if (flags.remote) {
57
65
  try {
58
66
  const [contentRemote, serviceRemote] = await Promise.all([
59
- inspectRemoteCheckouts(projectDir),
60
- customServices.inspectRemote(projectDir, requestedType ? { ids: [...requested] } : {}),
67
+ inspectRemoteCheckouts(projectDir, resourceKeys ? {
68
+ entries: result.map.checkouts.filter((entry) => resourceKeys.includes(`${entry.resource_type}:${entry.resource_id}`)),
69
+ } : {}),
70
+ resourceKeys
71
+ ? Promise.resolve([])
72
+ : customServices.inspectRemote(projectDir, requestedType ? { ids: [...requested] } : {}),
61
73
  ]);
62
74
  remote = requestedType
63
75
  ? serviceRemote.filter((entry) => requested.has(String(entry.resource_id)))
@@ -87,9 +99,12 @@ async function check(projectDir, positional = [], flags = {}) {
87
99
  }
88
100
 
89
101
  log.title('draftgo check');
90
- log.info(`Checked-out pages: ${result.map.pages.length}`);
91
- log.info(`Checked-out navigations: ${result.map.navigations.length}`);
92
- log.info(`Checked-out docs: ${result.map.docs.length}`);
102
+ const selectedCount = (type, entries) => resourceKeys
103
+ ? entries.filter((entry) => resourceKeys.includes(`${type}:${entry.resource_id}`)).length
104
+ : entries.length;
105
+ log.info(`Checked-out pages: ${selectedCount('pages', result.map.pages)}`);
106
+ log.info(`Checked-out navigations: ${selectedCount('navigations', result.map.navigations)}`);
107
+ log.info(`Checked-out docs: ${selectedCount('docs', result.map.docs)}`);
93
108
  log.info(`Checked-out custom services: ${customServiceChecks.length}`);
94
109
  log.info(`Workspace: ${hygiene.managed_files} managed, ${hygiene.unknown_files} unknown, ${hygiene.temporary_files} temporary, ${hygiene.artifact_files} artifact file(s)`);
95
110
  log.info(`Workspace size: ${hygiene.total_bytes} bytes; reclaimable: ${hygiene.reclaimable_bytes} bytes`);
@@ -23,6 +23,24 @@ function plan(projectDir, flags = {}) {
23
23
  const stat = fs.statSync(absolute); if (age && now - stat.mtimeMs < age) continue;
24
24
  candidates.push({ ...entry, absolute, size: stat.isFile() ? stat.size : 0, reason: 'runtime_manifest' });
25
25
  }
26
+ const tmp = path.resolve(root, 'tmp');
27
+ if (fs.existsSync(tmp) && runtime.inside(root, tmp) && (!types.size || types.has('tmp') || types.has('ai'))) {
28
+ const visitTmp = (current) => {
29
+ for (const child of fs.readdirSync(current, { withFileTypes: true })) {
30
+ const absolute = path.join(current, child.name);
31
+ if (child.isDirectory()) visitTmp(absolute);
32
+ else if (child.isFile() && !candidates.some((entry) => entry.absolute === absolute)) {
33
+ const stat = fs.statSync(absolute);
34
+ if (!age || now - stat.mtimeMs >= age) candidates.push({
35
+ path: path.relative(root, absolute).replace(/\\/g, '/'), absolute,
36
+ type: path.relative(tmp, absolute).replace(/\\/g, '/').startsWith('ai/') ? 'ai' : 'tmp',
37
+ size: stat.size, reason: 'temporary_area',
38
+ });
39
+ }
40
+ }
41
+ };
42
+ visitTmp(tmp);
43
+ }
26
44
  if (flags['all-artifacts']) {
27
45
  for (const prefix of MANAGED_PREFIXES.filter((item) => item.startsWith('artifacts/'))) {
28
46
  const directory = path.resolve(root, prefix); if (!runtime.inside(root, directory) || !fs.existsSync(directory)) continue;
@@ -39,6 +57,14 @@ async function clean(projectDir, _positional, flags = {}) {
39
57
  if (!flags.yes || flags['dry-run']) { if (!flags.output) log.dim('Run with --yes to delete this plan.'); return 0; }
40
58
  const manifest = runtime.load(projectDir); const removed = new Set();
41
59
  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); }
60
+ const tmp = path.join(runtime.draftgoRoot(projectDir), 'tmp');
61
+ if (fs.existsSync(tmp)) {
62
+ const removeEmpty = (directory) => {
63
+ for (const child of fs.readdirSync(directory, { withFileTypes: true })) if (child.isDirectory()) removeEmpty(path.join(directory, child.name));
64
+ if (directory !== tmp && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory);
65
+ };
66
+ removeEmpty(tmp);
67
+ }
42
68
  manifest.entries = manifest.entries.filter((entry) => !removed.has(entry.path)); runtime.save(projectDir, manifest);
43
69
  if (!flags.output) log.ok(`Deleted ${result.count} managed file(s).`); return 0;
44
70
  }
@@ -47,6 +47,8 @@ Usage:
47
47
  draftgo check [custom-services <id...>]
48
48
  Validate all checkouts or selected custom services.
49
49
  --remote also compares remote hash/version.
50
+ draftgo verify [<type> <id...>] Run unified static verification; --remote adds
51
+ remote comparison and --url enables UI checks.
50
52
  draftgo diff <type> <id> Show checkout base versus local body.
51
53
  draftgo commit <type> <id...> Validate and upload complete checked-out bodies.
52
54
  draftgo refresh <type> <id...> Safely refresh a clean checkout from the cloud.
@@ -63,9 +65,8 @@ Usage:
63
65
  draftgo conflict resolve <type> <id>
64
66
  Mark a merged worktree file ready against the
65
67
  preserved remote base; then check/diff/commit.
66
- draftgo verify-ui <url> Run a deterministic browser smoke check.
67
- Screenshots default to never.
68
- draftgo clean [--dry-run|--yes] Plan or remove registered tmp/artifacts only.
68
+ draftgo verify-ui <url> Deprecated alias for verify --url <url> --ui always.
69
+ draftgo clean [--dry-run|--yes] Plan or remove all tmp and registered artifacts.
69
70
  draftgo task <operation> [...] Track sustained work in one authoritative Task.md.
70
71
  Operations: create, list, show, add, claim,
71
72
  start, complete, block, reopen, accept, finish,
@@ -137,16 +138,20 @@ Important flags:
137
138
  (custom-service test) deny | mock | live; default deny.
138
139
  --delivery <mode> (deploy) local | preview | deploy.
139
140
  --dry-run (push) Show diffs without committing.
140
- --mobile-check <mode> (verify-ui) auto | always | never.
141
- --token <mode> (verify-ui) auto | never; auto appends the configured
141
+ --ui <mode> (verify) auto | always | never; auto runs with --url.
142
+ --remote (check/verify) Compare checkout hashes with remote.
143
+ --viewport <mode> (verify) mobile | desktop | both.
144
+ --frame <mode> (verify) auto | top | all | <iframe-selector>.
145
+ --mobile-check <mode> (verify-ui compatibility) auto | always | never.
146
+ --token <mode> (verify) auto | never; auto appends the configured
142
147
  SAT to same-origin URLs as the token query parameter.
143
- --screenshot <mode> (verify-ui) on-failure | always | never; default never.
144
- --browser <name> (verify-ui) chromium | chrome | msedge.
145
- --browser-path <file> (verify-ui) Explicit browser executable; environment
148
+ --screenshot <mode> (verify) on-failure | always | never; default never.
149
+ --browser <name> (verify) chromium | chrome | msedge.
150
+ --browser-path <file> (verify) Explicit browser executable; environment
146
151
  fallback: DRAFTGO_BROWSER_PATH.
147
- --resource <type:id> (verify-ui) Compare local and remote content first.
148
- --selector <css> (verify-ui) Require a visible key element.
149
- --width/--height <px> (verify-ui) Override the default 390x844 viewport.
152
+ --resource <type:id> (verify compatibility) Assert one remote UI resource.
153
+ --selector <css> (verify) Require a visible element in top or iframe DOM.
154
+ --width/--height <px> (verify) Override the default 390x844 viewport.
150
155
 
151
156
  Security:
152
157
  .draftgo/config.json stores the server and SAT and is gitignored. Host MCP
@@ -166,6 +171,7 @@ Examples:
166
171
  draftgo map --output json
167
172
  draftgo checkout pages 42
168
173
  draftgo check --strict
174
+ draftgo verify pages 42 --remote --url http://localhost:5173/example
169
175
  draftgo diff pages 42
170
176
  draftgo commit pages 42
171
177
  draftgo changelog add "Complete document management and role permissions"
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const log = require('../logger');
5
5
  const { detectTargets } = require('../detect');
6
6
  const { resolveTargets, all } = require('../installers');
7
- const { ensureRuntime, writeInstalledVersion, getPackageVersion } = require('../skill');
7
+ const { ensureRuntime, getPackageVersion } = require('../skill');
8
8
  const { exists } = require('../fsx');
9
9
  const { ask } = require('../prompt');
10
10
 
@@ -65,8 +65,7 @@ async function init(projectDir, positional, flags) {
65
65
  log.err(`安装未完成,失败目标:${failures.join(', ')}`);
66
66
  return 1;
67
67
  }
68
- if (installedCount > 0 && skippedCount === 0) writeInstalledVersion(projectDir);
69
- if (skippedCount > 0) log.dim(' 存在未覆盖目标,未更新全局 skill 版本标记;运行 draftgo update 可统一刷新。');
68
+ if (skippedCount > 0) log.dim(' 存在未覆盖目标;运行 draftgo update 可统一刷新。');
70
69
  log.dim(` CLI 版本:${getPackageVersion()}`);
71
70
 
72
71
  // 3) Environment check (advisory).
@@ -75,7 +75,7 @@ function lines(value, fallback = '- 暂无。') {
75
75
 
76
76
  function template(task) {
77
77
  return `# ${task.title}\n\n`+
78
- `- 状态:active\n- 创建时间:${task.created_at}\n- 更新时间:${task.updated_at}\n- 任务负责人:${task.owner}\n\n`+
78
+ `- 状态:active\n- 创建时间:${task.created_at}\n- 更新时间:${task.updated_at}\n\n`+
79
79
  `## 用户原始需求\n\n${task.original_requirement}\n\n`+
80
80
  `## 明确后的需求\n\n${task.clarified_requirement}\n\n`+
81
81
  `## 预计效果\n\n${lines(task.expected_effect)}\n\n`+
@@ -134,7 +134,7 @@ function parseMarkdown(file, id) {
134
134
  const acceptance = parseChecklist(section(body, '最终验收'), true);
135
135
  return {
136
136
  schema_version: 2, format: 'markdown', id, title, status: metadata(body, '状态') || 'active',
137
- created_at: metadata(body, '创建时间'), updated_at: metadata(body, '更新时间'), owner: metadata(body, '任务负责人'),
137
+ created_at: metadata(body, '创建时间'), updated_at: metadata(body, '更新时间'),
138
138
  original_requirement: section(body, '用户原始需求'), clarified_requirement: section(body, '明确后的需求'),
139
139
  expected_effect: section(body, '预计效果'), scope: section(body, '本次范围'), out_of_scope: section(body, '不在本次范围'),
140
140
  decisions: section(body, '关键决策'), blockers: section(body, '阻塞问题'), items, acceptance,
@@ -191,12 +191,6 @@ function print(flags, value) {
191
191
  if (flags.output === 'json') console.log(JSON.stringify(Array.isArray(value) ? value.map(publicTask) : publicTask(value), null, 2));
192
192
  }
193
193
  function taskActor(flags) { return String(flags.owner || flags.assignee || process.env.DRAFTGO_AGENT_ID || '').trim(); }
194
- function requireOwner(task, flags, operation) {
195
- if (!task.owner) return;
196
- const actor = taskActor(flags);
197
- if (!actor) throw new Error(`Task ${task.id} ${operation} requires owner identity ${task.owner}.`);
198
- if (actor !== task.owner) throw new Error(`Task ${task.id} ${operation} can only be performed by owner ${task.owner}.`);
199
- }
200
194
  function assertModern(task, operation) {
201
195
  if (task.format === 'legacy') throw new Error(`Legacy Task ${task.id} is read-only; run task migrate ${task.id} before ${operation}.`);
202
196
  }
@@ -245,7 +239,7 @@ function create(projectDir, positional, flags) {
245
239
  if (!clarified) throw new Error('task create requires --clarified with the executable requirement.');
246
240
  if (!expected) throw new Error('task create requires --expected-effect.');
247
241
  const id = uniqueTaskId(projectDir, title, flags); const now = new Date().toISOString();
248
- const task = { id, title, created_at: now, updated_at: now, owner: taskActor(flags), original_requirement: original, clarified_requirement: clarified,
242
+ const task = { id, title, created_at: now, updated_at: now, original_requirement: original, clarified_requirement: clarified,
249
243
  expected_effect: expected, scope: field(flags, ['scope']), out_of_scope: field(flags, ['out-of-scope']) };
250
244
  fs.mkdirSync(taskDir(projectDir, id), { recursive: true }); atomicWrite(markdownFile(projectDir, id), template(task));
251
245
  const loaded = loadTask(projectDir, id); print(flags, loaded); if (flags.output !== 'json') log.ok(`Created task ${id}`); return 0;
@@ -279,7 +273,7 @@ function nextItemId(items) {
279
273
  function add(projectDir, taskId, positional, flags) {
280
274
  const resolved = resolveTaskId(projectDir, taskId);
281
275
  return withLock(projectDir, resolved, (before) => {
282
- const task = loadTask(projectDir, resolved); assertModern(task, 'add'); requireOwner(task, flags, 'planning');
276
+ const task = loadTask(projectDir, resolved); assertModern(task, 'add');
283
277
  const title = field(flags, ['title'], positional.join(' ')); const content = field(flags, ['content']); const acceptance = field(flags, ['acceptance']);
284
278
  const owner = field(flags, ['assignee'], taskActor(flags));
285
279
  if (!title) throw new Error('task add requires a title or --title.');
@@ -343,7 +337,7 @@ function accept(projectDir, taskId, positional, flags) {
343
337
  const resolved = resolveTaskId(projectDir, taskId); const title = field(flags, ['item'], positional.join(' '));
344
338
  if (!title) throw new Error('task accept requires a final acceptance item title.');
345
339
  return withLock(projectDir, resolved, (before) => {
346
- const task = loadTask(projectDir, resolved); assertModern(task, 'accept'); requireOwner(task, flags, 'acceptance');
340
+ const task = loadTask(projectDir, resolved); assertModern(task, 'accept');
347
341
  const evidence = field(flags, ['evidence']); if (!evidence) throw new Error('task accept requires --evidence.');
348
342
  const body = touch(task, updateAcceptanceBody(task.body, title, true, evidence), 'active'); saveBody(task, body, before);
349
343
  const updated = loadTask(projectDir, resolved); print(flags, updated.acceptance.find((entry) => entry.title === title));
@@ -354,7 +348,7 @@ function accept(projectDir, taskId, positional, flags) {
354
348
  function finish(projectDir, taskId, flags) {
355
349
  const resolved = resolveTaskId(projectDir, taskId);
356
350
  return withLock(projectDir, resolved, (before) => {
357
- const task = loadTask(projectDir, resolved); assertModern(task, 'finish'); requireOwner(task, flags, 'finish');
351
+ const task = loadTask(projectDir, resolved); assertModern(task, 'finish');
358
352
  if (task.items.some((item) => !item.completed)) throw new Error(`Cannot finish ${task.id}; development items remain incomplete.`);
359
353
  const pending = task.acceptance.filter((item) => !item.completed);
360
354
  if (pending.length) throw new Error(`Cannot finish ${task.id}; final acceptance remains incomplete: ${pending.map((item) => item.title).join(', ')}.`);
@@ -379,11 +373,12 @@ function migrate(projectDir, taskId, flags) {
379
373
  const items = legacyItemDetails(projectDir, task); const now = new Date().toISOString();
380
374
  const original = field(flags, ['original'], task.title || resolved); const clarified = field(flags, ['clarified'], original);
381
375
  const expected = field(flags, ['expected-effect'], '保留历史任务的目标、进度和验收证据,并使用单文件继续维护。');
382
- const modern = { title: task.title || resolved, created_at: task.created_at || now, updated_at: now, owner: task.owner || taskActor(flags), original_requirement: original,
376
+ const defaultOwner = task.owner || taskActor(flags);
377
+ const modern = { title: task.title || resolved, created_at: task.created_at || now, updated_at: now, original_requirement: original,
383
378
  clarified_requirement: clarified, expected_effect: expected, scope: '', out_of_scope: '' };
384
379
  let body = template(modern); const blocks = items.map((item, index) => {
385
380
  const id = /^DG-\d{3}$/.test(item.id) ? item.id : `DG-${String(index + 1).padStart(3, '0')}`;
386
- const owner = item.owner || modern.owner || 'unassigned'; const evidence = item.evidence.join('; ') || 'Legacy task state migrated.';
381
+ const owner = item.owner || defaultOwner || 'unassigned'; const evidence = item.evidence.join('; ') || 'Legacy task state migrated.';
387
382
  return `- [${item.status === 'completed' ? 'x' : ' '}] ${id} ${item.title}\n - 内容:从历史 Task 迁移。\n - 负责人:${owner}\n - 验收:保留历史工作项状态与证据。\n - 状态:${item.status || 'pending'}${item.status === 'completed' ? `\n - 完成结果:${evidence}` : ''}`;
388
383
  });
389
384
  body = replaceSection(body, '开发清单', blocks.length ? blocks.join('\n\n') : '暂无。');
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const check = require('./check');
5
+ const verifyUi = require('./verifyUi');
6
+ const { canonicalResourceType } = require('../worktree/types');
7
+
8
+ function mode(value, fallback, flag) {
9
+ const normalized = String(value == null ? fallback : value).toLowerCase();
10
+ if (!['auto', 'always', 'never'].includes(normalized)) throw new Error(`${flag} must be auto, always, or never.`);
11
+ return normalized;
12
+ }
13
+
14
+ function resources(positional) {
15
+ if (!positional.length) return [];
16
+ const [rawType, ...ids] = positional;
17
+ if (!ids.length) throw new Error('Usage: draftgo verify [<pages|nav|docs> <id...>] [--url <url>]');
18
+ const resourceType = canonicalResourceType(rawType);
19
+ return ids.map((resourceId) => ({ resourceType, resourceId: String(resourceId) }));
20
+ }
21
+
22
+ function viewports(flags) {
23
+ const value = String(flags.viewport || '').toLowerCase();
24
+ if (!value) return [{ name: 'custom', width: flags.width, height: flags.height }];
25
+ if (value === 'mobile') return [{ name: 'mobile', width: 390, height: 844 }];
26
+ if (value === 'desktop') return [{ name: 'desktop', width: 1440, height: 900 }];
27
+ if (value === 'both') return [
28
+ { name: 'mobile', width: 390, height: 844 },
29
+ { name: 'desktop', width: 1440, height: 900 },
30
+ ];
31
+ throw new Error('--viewport must be mobile, desktop, or both.');
32
+ }
33
+
34
+ async function verify(projectDir, positional = [], flags = {}) {
35
+ let selected;
36
+ let uiMode;
37
+ let remoteMode;
38
+ try {
39
+ selected = resources(positional);
40
+ uiMode = mode(flags.ui, 'auto', '--ui');
41
+ remoteMode = flags.remote ? 'always' : 'never';
42
+ } catch (error) {
43
+ log.err(error.message);
44
+ return 1;
45
+ }
46
+
47
+ log.title('draftgo verify');
48
+ const checkCode = await check(projectDir, [], {
49
+ strict: flags.strict,
50
+ remote: remoteMode === 'always',
51
+ resourceKeys: selected.length
52
+ ? selected.map((entry) => `${entry.resourceType}:${entry.resourceId}`)
53
+ : null,
54
+ });
55
+ if (checkCode !== 0) return checkCode;
56
+
57
+ const url = String(flags.url || '').trim();
58
+ const runUi = uiMode === 'always' || (uiMode === 'auto' && Boolean(url));
59
+ if (!runUi) {
60
+ if (uiMode === 'always' && !url) {
61
+ log.err('--ui always requires --url <http://localhost:port/path>.');
62
+ return 1;
63
+ }
64
+ log.ok('Unified verification passed (UI skipped).');
65
+ return 0;
66
+ }
67
+ if (selected.length > 1) {
68
+ log.err('One --url can assert at most one resource. Run verify once per route for UI validation.');
69
+ return 1;
70
+ }
71
+
72
+ let targets;
73
+ try { targets = viewports(flags); }
74
+ catch (error) { log.err(error.message); return 1; }
75
+ for (const viewport of targets) {
76
+ const uiFlags = { ...flags, url, width: viewport.width, height: viewport.height, 'mobile-check': 'always' };
77
+ if (remoteMode === 'always' && selected.length === 1 && !uiFlags.resource) {
78
+ uiFlags.resource = `${selected[0].resourceType}:${selected[0].resourceId}`;
79
+ }
80
+ const code = await verifyUi(projectDir, [url], uiFlags);
81
+ if (code !== 0) return code;
82
+ }
83
+ log.ok(`Unified verification passed (${targets.map((item) => item.name).join(', ')}).`);
84
+ return 0;
85
+ }
86
+
87
+ module.exports = verify;
88
+ module.exports.mode = mode;
89
+ module.exports.resources = resources;
90
+ module.exports.viewports = viewports;
@@ -163,6 +163,51 @@ function parseResourceSpec(value) {
163
163
  return { resourceType: canonicalResourceType(raw.slice(0, separator)), resourceId: raw.slice(separator + 1) };
164
164
  }
165
165
 
166
+ async function selectFrames(page, mode = 'auto') {
167
+ const top = page.mainFrame ? page.mainFrame() : page;
168
+ const selected = [{ frame: top, label: 'top' }];
169
+ if (mode === 'top') return selected;
170
+ if (!['auto', 'all'].includes(mode)) {
171
+ const handle = await page.locator(mode).first().elementHandle().catch(() => null);
172
+ const frame = handle && await handle.contentFrame().catch(() => null);
173
+ if (!frame) throw new Error(`Frame not found: ${mode}`);
174
+ return [{ frame, label: `selector:${mode}` }];
175
+ }
176
+ const frames = page.frames ? page.frames() : [];
177
+ for (let index = 0; index < frames.length; index += 1) {
178
+ const frame = frames[index];
179
+ if (frame === top) continue;
180
+ let visible = true;
181
+ if (mode === 'auto') {
182
+ const element = await frame.frameElement().catch(() => null);
183
+ visible = Boolean(element && await element.isVisible().catch(() => false));
184
+ }
185
+ if (visible) selected.push({
186
+ frame,
187
+ label: `frame:${frame.name && frame.name() ? frame.name() : index}`,
188
+ });
189
+ }
190
+ return selected;
191
+ }
192
+
193
+ async function inspectFrame(frame) {
194
+ return frame.evaluate(() => {
195
+ const body = document.body;
196
+ const root = document.documentElement;
197
+ return {
198
+ bodyTextLength: body ? (body.innerText || '').trim().length : 0,
199
+ bodyHeight: body ? body.getBoundingClientRect().height : 0,
200
+ hasVisibleMedia: body ? Array.from(body.querySelectorAll('img,svg,canvas,video,iframe,input,button')).some((element) => {
201
+ const rect = element.getBoundingClientRect();
202
+ return rect.width > 1 && rect.height > 1;
203
+ }) : false,
204
+ horizontalOverflow: root.scrollWidth > window.innerWidth + 1,
205
+ scrollWidth: root.scrollWidth,
206
+ viewportWidth: window.innerWidth,
207
+ };
208
+ });
209
+ }
210
+
166
211
  async function verifyRemoteResource(projectDir, spec) {
167
212
  const { resourceType, resourceId } = parseResourceSpec(spec);
168
213
  const entry = getEntry(loadManifest(projectDir), resourceType, resourceId);
@@ -257,32 +302,28 @@ async function verifyUi(projectDir, positional, flags = {}) {
257
302
 
258
303
  const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
259
304
  if (waitMs) await page.waitForTimeout(waitMs);
260
- const state = await page.evaluate(() => {
261
- const body = document.body;
262
- const root = document.documentElement;
263
- return {
264
- title: document.title,
265
- bodyTextLength: body ? (body.innerText || '').trim().length : 0,
266
- bodyHeight: body ? body.getBoundingClientRect().height : 0,
267
- hasVisibleMedia: body ? Array.from(body.querySelectorAll('img,svg,canvas,video,iframe,input,button')).some((element) => {
268
- const rect = element.getBoundingClientRect();
269
- return rect.width > 1 && rect.height > 1;
270
- }) : false,
271
- horizontalOverflow: root.scrollWidth > window.innerWidth + 1,
272
- scrollWidth: root.scrollWidth,
273
- viewportWidth: window.innerWidth,
274
- };
275
- });
305
+ const frames = await selectFrames(page, String(flags.frame || 'auto'));
306
+ const states = [];
307
+ for (const item of frames) states.push({ ...item, state: await inspectFrame(item.frame) });
276
308
 
277
309
  const issues = [];
278
310
  if (response && response.status() >= 400) issues.push(`页面返回 HTTP ${response.status()}`);
279
- if (!state.bodyHeight || (!state.bodyTextLength && !state.hasVisibleMedia)) issues.push('页面疑似空白');
280
- if (state.horizontalOverflow) issues.push(`页面横向溢出:scrollWidth=${state.scrollWidth}, viewport=${state.viewportWidth}`);
311
+ if (!states.some(({ state }) => state.bodyHeight && (state.bodyTextLength || state.hasVisibleMedia))) issues.push('页面疑似空白');
312
+ for (const { label, state } of states) {
313
+ if (state.horizontalOverflow) issues.push(`${label} 横向溢出:scrollWidth=${state.scrollWidth}, viewport=${state.viewportWidth}`);
314
+ }
281
315
  if (consoleErrors.length) issues.push(`console error ${consoleErrors.length} 条`);
282
316
  if (pageErrors.length) issues.push(`page error ${pageErrors.length} 条`);
283
317
  if (flags.selector) {
284
- const visible = await page.locator(String(flags.selector)).first().isVisible().catch(() => false);
285
- if (!visible) issues.push(`关键元素不可见:${flags.selector}`);
318
+ let match = null;
319
+ for (const item of frames) {
320
+ if (await item.frame.locator(String(flags.selector)).first().isVisible().catch(() => false)) {
321
+ match = item.label;
322
+ break;
323
+ }
324
+ }
325
+ if (!match) issues.push(`关键元素在已检查 frame 中不可见:${flags.selector}`);
326
+ else log.info(`Selector ${flags.selector}: visible in ${match}`);
286
327
  }
287
328
 
288
329
  const shouldScreenshot = screenshotMode === 'always' || (screenshotMode === 'on-failure' && issues.length > 0);
@@ -303,7 +344,7 @@ async function verifyUi(projectDir, positional, flags = {}) {
303
344
  return 1;
304
345
  }
305
346
 
306
- log.ok(`UI smoke check 通过:${width}x${height},${decision.reason}`);
347
+ log.ok(`UI smoke check 通过:${width}x${height},${frames.length} frame(s),${decision.reason}`);
307
348
  if (screenshotPath) log.info(`截图:${screenshotPath}`);
308
349
  return 0;
309
350
  } catch (err) {
@@ -324,3 +365,5 @@ module.exports.executableCandidates = executableCandidates;
324
365
  module.exports.playwrightCacheCandidates = playwrightCacheCandidates;
325
366
  module.exports.parseResourceSpec = parseResourceSpec;
326
367
  module.exports.verifyRemoteResource = verifyRemoteResource;
368
+ module.exports.selectFrames = selectFrames;
369
+ module.exports.inspectFrame = inspectFrame;
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const verify = require('./verify');
5
+
6
+ async function verifyUiCompat(projectDir, positional = [], flags = {}) {
7
+ const url = String(flags.url || positional[0] || '').trim();
8
+ log.warn('`draftgo verify-ui` is deprecated; use `draftgo verify --url <url> --ui always`.');
9
+ return verify(projectDir, [], {
10
+ ...flags,
11
+ url,
12
+ ui: 'always',
13
+ });
14
+ }
15
+
16
+ module.exports = verifyUiCompat;
package/src/projectMap.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const parse5 = require('parse5');
7
7
  const { loadManifest, absolutePath } = require('./worktree/manifest');
8
8
  const { mediaType } = require('./worktree/types');
9
+ const { validateInlineScripts } = require('./worktree/inlineScripts');
9
10
 
10
11
  function normalizeRoute(route) {
11
12
  if (!route || typeof route !== 'string') return '';
@@ -121,7 +122,7 @@ function worktreeFiles(projectDir, directory) {
121
122
  .map((entry) => path.join(root, entry.name));
122
123
  }
123
124
 
124
- function analyzeProject(projectDir) {
125
+ function analyzeProject(projectDir, options = {}) {
125
126
  const map = buildProjectMap(projectDir);
126
127
  const errors = [];
127
128
  const warnings = [];
@@ -132,7 +133,12 @@ function analyzeProject(projectDir) {
132
133
  };
133
134
  const registered = new Set();
134
135
 
135
- for (const entry of map.checkouts) {
136
+ const resourceKeys = options.resourceKeys ? new Set(options.resourceKeys) : null;
137
+ const selectedCheckouts = resourceKeys
138
+ ? map.checkouts.filter((entry) => resourceKeys.has(`${entry.resource_type}:${entry.resource_id}`))
139
+ : map.checkouts;
140
+
141
+ for (const entry of selectedCheckouts) {
136
142
  let local;
137
143
  let base;
138
144
  try {
@@ -176,18 +182,24 @@ function analyzeProject(projectDir) {
176
182
  const position = issue.line ? ` at ${issue.line}:${issue.column || 1}` : '';
177
183
  addWarning('DG-HTML-001', 'high', `${entry.resource_type} ${entry.resource_id}: ${issue.code}${position}.`);
178
184
  }
185
+ for (const issue of validateInlineScripts(source).slice(0, 20)) {
186
+ errors.push(`${entry.resource_type} ${entry.resource_id}: inline script #${issue.script} `
187
+ + `(${issue.script_type}) ${issue.message} at ${entry.local_path}:${issue.line}:${issue.column}.`);
188
+ }
179
189
  }
180
190
  }
181
191
 
182
- for (const directory of ['pages', 'navigations', 'docs']) {
183
- for (const file of worktreeFiles(projectDir, directory)) {
184
- if (!registered.has(path.resolve(file).toLowerCase())) {
185
- const relative = path.relative(projectDir, file).replace(/\\/g, '/');
186
- addWarning('DG-WORKTREE-ORPHAN', 'high', `Unregistered worktree file: ${relative}.`);
192
+ if (!resourceKeys) {
193
+ for (const directory of ['pages', 'navigations', 'docs']) {
194
+ for (const file of worktreeFiles(projectDir, directory)) {
195
+ if (!registered.has(path.resolve(file).toLowerCase())) {
196
+ const relative = path.relative(projectDir, file).replace(/\\/g, '/');
197
+ addWarning('DG-WORKTREE-ORPHAN', 'high', `Unregistered worktree file: ${relative}.`);
198
+ }
187
199
  }
188
200
  }
189
201
  }
190
- for (const page of map.pages) {
202
+ for (const page of map.pages.filter((entry) => !resourceKeys || resourceKeys.has(`pages:${entry.resource_id}`))) {
191
203
  if (!page.route || page.route === '/' || page.area === 'system') continue;
192
204
  const sources = map.routeRefs[normalizeRoute(page.route)] || [];
193
205
  if (!sources.some((source) => source.startsWith('navigations:'))) {
package/src/skill.js CHANGED
@@ -187,15 +187,6 @@ function validateRenderedSkill(assetDir) {
187
187
  }
188
188
 
189
189
  function ensureRuntime(projectDir) {
190
- const dg = paths.dgDir(projectDir);
191
- ensureDir(dg);
192
- for (const sub of ['Task', 'lessons']) {
193
- ensureDir(path.join(dg, sub));
194
- }
195
- const changelog = path.join(dg, 'changelog.md');
196
- if (!exists(changelog)) {
197
- writeText(changelog, '');
198
- }
199
190
  appendGitignoreLine(projectDir, '.draftgo/config.json');
200
191
  appendGitignoreLine(projectDir, '.draftgo/token');
201
192
  appendGitignoreLine(projectDir, '.draftgo/worktree/');
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const runtime = require('./runtimeFiles');
6
6
 
7
- const PROTECTED_PREFIXES = ['worktree/', 'conflicts/', 'Task/', 'lessons/', 'config.json', 'changelog.md', 'story.yaml', '.version', 'runtime-manifest.json'];
7
+ const PROTECTED_PREFIXES = ['worktree/', 'conflicts/', 'Task/', 'lessons/', 'tmp/', 'config.json', 'changelog.md', 'story.yaml', '.version', 'runtime-manifest.json'];
8
8
  function workspaceHealth(projectDir) {
9
9
  const root = runtime.draftgoRoot(projectDir); const registered = new Set(runtime.load(projectDir).entries.map((entry) => String(entry.path).replace(/\\/g, '/')));
10
10
  const summary = { managed_files: 0, unknown_files: 0, temporary_files: 0, artifact_files: 0, total_bytes: 0, reclaimable_bytes: 0, warnings: [] };
@@ -22,7 +22,7 @@ function workspaceHealth(projectDir) {
22
22
  if (managed) summary.managed_files += 1; else summary.unknown_files += 1;
23
23
  if (relative.startsWith('tmp/')) summary.temporary_files += 1;
24
24
  if (relative.startsWith('artifacts/')) summary.artifact_files += 1;
25
- if (registered.has(relative)) summary.reclaimable_bytes += size;
25
+ if (registered.has(relative) || relative.startsWith('tmp/')) summary.reclaimable_bytes += size;
26
26
  if (!managed && (/\.exe$/i.test(relative) || (/^[^/]+\.(go|json)$/i.test(relative) && !['config.json', 'runtime-manifest.json'].includes(relative)))) summary.warnings.push({ code: 'DG-WORKSPACE-UNKNOWN-FILE', path: relative, size });
27
27
  if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-WORKSPACE-UNREGISTERED-ARTIFACT', path: relative, size });
28
28
  }
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ const vm = require('vm');
4
+ const { spawnSync } = require('child_process');
5
+ const parse5 = require('parse5');
6
+
7
+ const DATA_SCRIPT_TYPES = new Set([
8
+ 'application/json',
9
+ 'application/ld+json',
10
+ 'importmap',
11
+ 'speculationrules',
12
+ ]);
13
+
14
+ function attribute(node, name) {
15
+ const item = (node.attrs || []).find((candidate) => candidate.name.toLowerCase() === name);
16
+ return item ? item.value : '';
17
+ }
18
+
19
+ function scriptSource(node) {
20
+ return (node.childNodes || [])
21
+ .filter((child) => child.nodeName === '#text')
22
+ .map((child) => child.value || '')
23
+ .join('');
24
+ }
25
+
26
+ function collectInlineScripts(html) {
27
+ const document = parse5.parse(String(html || ''), { sourceCodeLocationInfo: true });
28
+ const scripts = [];
29
+ const visit = (node) => {
30
+ if (node.tagName === 'script' && !attribute(node, 'src')) {
31
+ const type = attribute(node, 'type').trim().toLowerCase();
32
+ if (!DATA_SCRIPT_TYPES.has(type) && (!type || type === 'module' || /(?:java|ecma)script/.test(type))) {
33
+ const location = node.sourceCodeLocation || {};
34
+ const startTag = location.startTag || {};
35
+ scripts.push({
36
+ index: scripts.length + 1,
37
+ type: type === 'module' ? 'module' : 'classic',
38
+ source: scriptSource(node),
39
+ startLine: startTag.endLine || location.startLine || 1,
40
+ startColumn: startTag.endCol || 1,
41
+ });
42
+ }
43
+ }
44
+ for (const child of node.childNodes || []) visit(child);
45
+ if (node.content) visit(node.content);
46
+ };
47
+ visit(document);
48
+ return scripts;
49
+ }
50
+
51
+ function syntaxPosition(message) {
52
+ const line = String(message || '').match(/(?:\[stdin\]|inline-script):(\d+)(?::(\d+))?/);
53
+ if (line) return { line: Number(line[1]), column: Number(line[2] || 1) };
54
+ const caretLines = String(message || '').split(/\r?\n/);
55
+ const caretIndex = caretLines.findIndex((value) => /^\s*\^/.test(value));
56
+ return { line: null, column: caretIndex > 0 ? caretLines[caretIndex].indexOf('^') + 1 : null };
57
+ }
58
+
59
+ function conciseMessage(error) {
60
+ const lines = String(error && (error.stderr || error.message) || error || '').split(/\r?\n/);
61
+ return lines.find((line) => /^SyntaxError:/.test(line))
62
+ || lines.find((line) => line.trim() && !/^\s*at\s/.test(line))
63
+ || 'Invalid JavaScript syntax';
64
+ }
65
+
66
+ function validateInlineScripts(html) {
67
+ const issues = [];
68
+ for (const script of collectInlineScripts(html)) {
69
+ if (!script.source.trim()) continue;
70
+ let failure = null;
71
+ if (script.type === 'module') {
72
+ const result = spawnSync(process.execPath, ['--check', '--input-type=module'], {
73
+ input: script.source,
74
+ encoding: 'utf8',
75
+ windowsHide: true,
76
+ });
77
+ if (result.error || result.status !== 0) failure = { message: result.stderr || result.stdout || result.error.message };
78
+ } else {
79
+ try { new vm.Script(script.source, { filename: 'inline-script' }); }
80
+ catch (error) { failure = error; }
81
+ }
82
+ if (!failure) continue;
83
+ const diagnostic = failure.stack || failure.message;
84
+ const position = syntaxPosition(diagnostic);
85
+ issues.push({
86
+ code: 'DG-JS-001',
87
+ script: script.index,
88
+ script_type: script.type,
89
+ line: position.line ? script.startLine + position.line - 1 : script.startLine,
90
+ column: position.column
91
+ ? position.column + (position.line === 1 ? script.startColumn - 1 : 0)
92
+ : script.startColumn,
93
+ message: conciseMessage(failure),
94
+ });
95
+ }
96
+ return issues;
97
+ }
98
+
99
+ module.exports = { DATA_SCRIPT_TYPES, collectInlineScripts, validateInlineScripts };
@@ -101,7 +101,7 @@ async function inspectRemoteCheckouts(projectDir, options = {}) {
101
101
  const session = options.backend && typeof options.backend.resolveMetadata === 'function'
102
102
  ? { client: options.client || {}, tools: options.tools || [] }
103
103
  : await openMetadataSession(config, options);
104
- const entries = Object.values(manifest.entries);
104
+ const entries = options.entries || Object.values(manifest.entries);
105
105
  return Promise.all(entries.map(async (entry) => {
106
106
  const remote = await backend.resolveMetadata(config, entry.resource_type, entry.resource_id, {
107
107
  ...options,