draftgo-cli 3.0.55 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +112 -316
  2. package/package.json +5 -5
  3. package/resources/skill/SKILL.md +25 -24
  4. package/resources/skill/init/SKILL.md +5 -10
  5. package/resources/skill/manifest.json +2 -2
  6. package/resources/skill/references/aihub.md +10 -5
  7. package/resources/skill/references/chat-sdk.md +10 -0
  8. package/resources/skill/references/checkout.md +4 -4
  9. package/resources/skill/references/custom-services.md +65 -226
  10. package/resources/skill/references/data.md +3 -2
  11. package/resources/skill/references/frontend.md +96 -490
  12. package/resources/skill/references/mcp.md +39 -103
  13. package/resources/skill/references/runtime.md +3 -2
  14. package/resources/skill/story/SKILL.md +1 -2
  15. package/src/apiContractCache.js +112 -0
  16. package/src/cli.js +1 -21
  17. package/src/commandRegistry.js +6 -11
  18. package/src/commands/api.js +28 -8
  19. package/src/commands/check.js +1 -10
  20. package/src/commands/customService.js +2 -4
  21. package/src/commands/delete.js +23 -46
  22. package/src/commands/deploy.js +1 -1
  23. package/src/commands/help.js +16 -31
  24. package/src/commands/init.js +4 -10
  25. package/src/commands/listTargets.js +1 -1
  26. package/src/commands/local.js +2 -6
  27. package/src/commands/map.js +0 -11
  28. package/src/commands/status.js +1 -1
  29. package/src/commands/uninstall.js +3 -3
  30. package/src/commands/update.js +1 -1
  31. package/src/commands/verify.js +43 -21
  32. package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
  33. package/src/commands/worklog.js +86 -0
  34. package/src/customServices.js +150 -33
  35. package/src/{localdev → localRuntime}/detect.js +1 -1
  36. package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
  37. package/src/{localdev → localRuntime}/services.js +1 -1
  38. package/src/projectConfig.js +2 -0
  39. package/src/{installers/index.js → targets.js} +3 -5
  40. package/src/worklog.js +274 -0
  41. package/src/workspaceHealth.js +1 -1
  42. package/src/worktree/index.js +81 -51
  43. package/src/changelog.js +0 -276
  44. package/src/commands/changelog.js +0 -24
  45. package/src/commands/localDev.js +0 -9
  46. package/src/commands/sync.js +0 -46
  47. package/src/commands/task.js +0 -408
  48. package/src/commands/verifyUiCompat.js +0 -16
  49. /package/src/{localdev → localRuntime}/compose.js +0 -0
  50. /package/src/{localdev → localRuntime}/index.js +0 -0
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { all } = require('../installers');
3
+ const { all } = require('../targets');
4
4
  const { HOSTS } = require('../mcp/hosts');
5
5
  const { getPackageVersion } = require('../skill');
6
6
 
@@ -43,8 +43,8 @@ Usage:
43
43
  draftgo check [custom-services <id...>]
44
44
  Validate all checkouts or selected custom services.
45
45
  --remote also compares remote hash/version.
46
- draftgo verify [<type> <id...>] Run unified static verification; --remote adds
47
- remote comparison and --url enables UI checks.
46
+ draftgo verify [<type> <id...>] Run the default delivery verification.
47
+ Browser options are explicit and optional.
48
48
  draftgo diff <type> <id> Show checkout base versus local body.
49
49
  draftgo commit <type> <id...> Validate and upload complete checked-out bodies.
50
50
  draftgo refresh <type> <id...> Safely refresh a clean checkout from the cloud.
@@ -61,12 +61,12 @@ Usage:
61
61
  draftgo conflict resolve <type> <id>
62
62
  Mark a merged worktree file ready against the
63
63
  preserved remote base; then check/diff/commit.
64
- draftgo verify-ui <url> Deprecated alias for verify --url <url> --ui always.
65
64
  draftgo clean [--dry-run|--yes] Plan or remove all tmp and registered artifacts.
66
- draftgo task <operation> [...] Track sustained work in one authoritative Task.md.
67
- Operations: create, list, show, add, claim,
68
- start, complete, block, reopen, accept, finish,
69
- and explicit legacy migrate.
65
+ draftgo work start <item> Add an active item to .draftgo/worklog.md.
66
+ draftgo work add <item> Add a pending item.
67
+ draftgo work start-item <ref> Mark an item active; ref is number or date#number.
68
+ draftgo work complete <ref> Mark an item complete; --note appends evidence.
69
+ draftgo work show|list Show the worklog or its structured item list.
70
70
 
71
71
  draftgo api <query> Search the live API contract through MCP.
72
72
  draftgo api describe <operation_id>
@@ -75,23 +75,14 @@ Usage:
75
75
  Call one described operation with UTF-8 JSON.
76
76
  draftgo delete <operation_id> [id]
77
77
  Confirm and call a live delete operation.
78
- Legacy <type> <id> works only when unambiguous.
79
78
  draftgo deploy [<type> <id...>] Check, then diff or commit checked-out content.
80
79
  --delivery local needs no IDs; preview/deploy
81
80
  require an explicit type and IDs.
82
81
  draftgo auto-push [<type> <id...>]
83
82
  With config.auto_push=true, check and commit
84
83
  changed checkouts. Any conflict stops the run.
85
- draftgo changelog add <result> Append one completed result after the task has
86
- passed unified verification and delivery.
87
- draftgo pull Migration notice only; always downloads nothing.
88
- draftgo push <type> <id...> Deprecated alias to commit for pages/nav/docs.
89
- Push-all and structured-resource push are removed.
90
-
91
84
  draftgo local setup|start|stop|logs|status
92
- Manage the existing local Docker stack. up/down
93
- remain aliases for start/stop; status also has ps.
94
- draftgo local-dev Compatibility alias for \`draftgo local setup\`.
85
+ Manage the project-local Docker stack.
95
86
  draftgo list-targets List Skill installation targets.
96
87
  draftgo -v | --version Print CLI version.
97
88
  draftgo -h | --help Show this help.
@@ -114,7 +105,6 @@ Important flags:
114
105
  --purge (uninstall) Also remove the .draftgo/ directory.
115
106
  --skip-update-check (update) Refresh Skills without contacting npm.
116
107
  --connect (init) Continue into DraftGo server connection.
117
- --local-dev (init) Continue into local Docker setup.
118
108
  --no-setup (init) Install the Skill without either setup flow.
119
109
  --output json Print machine-readable output where supported.
120
110
  --type <type> (map) pages | nav/navigations | docs/articles.
@@ -131,21 +121,19 @@ Important flags:
131
121
  --side-effect-policy <mode>
132
122
  (custom-service test) deny | mock | live; default deny.
133
123
  --delivery <mode> (deploy) local | preview | deploy.
134
- --dry-run (push) Show diffs without committing.
135
- --ui <mode> (verify) auto | always | never; auto runs with --url.
124
+ --dry-run Preview supported cleanup or delivery operations.
125
+ --ui <mode> (verify) always | never; default never.
136
126
  --remote (check/verify) Compare checkout hashes with remote.
137
127
  --viewport <mode> (verify) mobile | desktop | both.
138
128
  --frame <mode> (verify) auto | top | all | <iframe-selector>.
139
- --mobile-check <mode> (verify-ui compatibility) auto | always | never.
140
129
  --token <mode> (verify) auto | never; auto appends the configured
141
130
  SAT to same-origin URLs as the token query parameter.
142
- --screenshot <mode> (verify) on-failure | always | never; default never.
131
+ --screenshot <mode> (verify) always | never; always alone captures a screenshot.
143
132
  --browser <name> (verify) chromium | chrome | msedge.
144
133
  --browser-path <file> (verify) Explicit browser executable; environment
145
134
  fallback: DRAFTGO_BROWSER_PATH.
146
- --resource <type:id> (verify compatibility) Assert one remote UI resource.
147
135
  --selector <css> (verify) Require a visible element in top or iframe DOM.
148
- --width/--height <px> (verify) Override the default 390x844 viewport.
136
+ --width/--height <px> (verify) Override the default 1440x900 desktop viewport.
149
137
 
150
138
  Security:
151
139
  .draftgo/config.json stores the server and SAT and is gitignored. Host MCP
@@ -163,14 +151,11 @@ Examples:
163
151
  draftgo mcp test
164
152
  draftgo map --output json
165
153
  draftgo checkout pages 42
166
- draftgo check --strict
167
- draftgo verify pages 42 --remote --url http://localhost:5173/example
154
+ draftgo verify pages 42
168
155
  draftgo diff pages 42
169
156
  draftgo commit pages 42
170
- draftgo changelog add "Complete document management and role permissions"
171
- draftgo task create "Personal growth" --original "Build a Notion-like system" \
172
- --clarified "Build goals, habits, notes, and reviews" \
173
- --expected-effect "Users manage personal growth in one workspace"
157
+ draftgo work start "Build document management and role permissions"
158
+ draftgo work complete 1 --note "Verification and delivery passed"
174
159
  draftgo deploy docs 7 --delivery preview
175
160
  `);
176
161
  }
@@ -3,7 +3,7 @@
3
3
  const path = require('path');
4
4
  const log = require('../logger');
5
5
  const { detectTargets } = require('../detect');
6
- const { resolveTargets, all } = require('../installers');
6
+ const { resolveTargets, all } = require('../targets');
7
7
  const { ensureRuntime, getPackageVersion } = require('../skill');
8
8
  const { exists } = require('../fsx');
9
9
  const { ask } = require('../prompt');
@@ -76,23 +76,17 @@ async function init(projectDir, positional, flags) {
76
76
  const cfgPath = path.join(projectDir, '.draftgo', 'config.json');
77
77
  const hasConfig = exists(cfgPath);
78
78
  const flagConnect = flags.connect === true;
79
- const flagLocalDev = flags['local-dev'] === true;
80
- const flagSkip = flags['no-local-dev'] === true
81
- || flags['skip-local-dev'] === true
82
- || flags['no-setup'] === true;
79
+ const flagSkip = flags['no-setup'] === true;
83
80
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
84
81
 
85
82
  if (!hasConfig && !flagSkip) {
86
83
  // 非交互场景下,如果同时给了 --server 和 --token,直接走 connect,不再阻塞在 TTY 提示上。
87
- if (flags.server && flags.token && !flagConnect && !flagLocalDev) {
84
+ if (flags.server && flags.token && !flagConnect) {
88
85
  return await require('./connect')(projectDir, [], flags);
89
86
  }
90
87
  if (flagConnect) {
91
88
  return await require('./connect')(projectDir, [], flags);
92
89
  }
93
- if (flagLocalDev) {
94
- return await require('./localDev')(projectDir, [], flags);
95
- }
96
90
  if (interactive) {
97
91
  log.title('选择 DraftGo 基座处理方式');
98
92
  log.plain(` ${log.c.cyan('[A]')} 连接基座服务`);
@@ -106,7 +100,7 @@ async function init(projectDir, positional, flags) {
106
100
  return await require('./connect')(projectDir, [], flags);
107
101
  }
108
102
  if (choice === 'B') {
109
- return await require('./localDev')(projectDir, [], { ...flags, yes: true });
103
+ return await require('../localRuntime').runWizard(projectDir, { yes: true });
110
104
  }
111
105
  if (choice !== 'C') {
112
106
  log.warn('无效选择,已跳过基座处理。');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { all } = require('../installers');
3
+ const { all } = require('../targets');
4
4
 
5
5
  function listTargets() {
6
6
  console.log('draftgo 支持的 AI 工具(target):');
@@ -4,8 +4,8 @@ const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
5
  const log = require('../logger');
6
6
  const { exists } = require('../fsx');
7
- const { detectDocker } = require('../localdev/detect');
8
- const { runWizard } = require('../localdev');
7
+ const { detectDocker } = require('../localRuntime/detect');
8
+ const { runWizard } = require('../localRuntime');
9
9
 
10
10
  function composeFile(projectDir) {
11
11
  return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
@@ -58,21 +58,17 @@ function local(projectDir, positional, flags = {}) {
58
58
  switch (action) {
59
59
  case 'setup':
60
60
  return runWizard(projectDir, { yes: !!(flags.yes || flags.y) });
61
- case 'up':
62
61
  case 'start':
63
62
  return runCompose(projectDir, ['up', '-d']);
64
- case 'down':
65
63
  case 'stop':
66
64
  return runCompose(projectDir, ['down']);
67
65
  case 'logs':
68
66
  return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
69
67
  case 'status':
70
- case 'ps':
71
68
  return runCompose(projectDir, ['ps']);
72
69
  default:
73
70
  log.err(`未知 local 子命令:${action}`);
74
71
  log.dim(' 可用:draftgo local setup | start | stop | logs | status');
75
- log.dim(' 兼容别名:up | down');
76
72
  return 1;
77
73
  }
78
74
  }
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
3
  const log = require('../logger');
6
4
  const { loadProjectConfig } = require('../projectConfig');
7
5
  const { canonicalResourceType } = require('../worktree/types');
@@ -84,11 +82,6 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
84
82
  return groups.flat();
85
83
  }
86
84
 
87
- function legacyCaches(projectDir) {
88
- return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
89
- .filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
90
- }
91
-
92
85
  function checkoutChanged(entry) {
93
86
  if (entry.resource_type === 'custom_services') {
94
87
  return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
@@ -126,7 +119,6 @@ async function mapCommand(projectDir, flags = {}) {
126
119
  overview,
127
120
  resources,
128
121
  checkouts,
129
- legacy_cache: { ignored: true, detected: legacyCaches(projectDir) },
130
122
  };
131
123
 
132
124
  if (flags.output === 'json') {
@@ -149,9 +141,6 @@ async function mapCommand(projectDir, flags = {}) {
149
141
  + `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
150
142
  + `(${entry.local_path})`);
151
143
  }
152
- if (result.legacy_cache.detected.length) {
153
- log.warn(`Ignored legacy cache indexes: ${result.legacy_cache.detected.join(', ')}`);
154
- }
155
144
  return 0;
156
145
  }
157
146
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const log = require('../logger');
4
- const { all } = require('../installers');
4
+ const { all } = require('../targets');
5
5
  const { readInstalledVersion, getPackageVersion } = require('../skill');
6
6
 
7
7
  function status(projectDir) {
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const log = require('../logger');
4
- const { all, resolveTargets } = require('../installers');
4
+ const { all, resolveTargets } = require('../targets');
5
5
  const { removePath } = require('../fsx');
6
6
  const { dgDir } = require('../paths');
7
7
 
@@ -38,10 +38,10 @@ async function uninstall(projectDir, positional, flags) {
38
38
 
39
39
  // Each AI-tool skill directory is self-contained, so per-target uninstall
40
40
  // already wiped its content above. Only --purge nukes runtime data under
41
- // .draftgo/ (config/changelog/Task/lessons/cache etc.).
41
+ // .draftgo/ (config/worklog/lessons/cache etc.).
42
42
  if (flags.purge) {
43
43
  if (removePath(dgDir(projectDir))) {
44
- log.warn('已使用 --purge:.draftgo/(含 config/changelog/lessons/Task 等运行时数据)已删除。');
44
+ log.warn('已使用 --purge:.draftgo/(含 config/worklog/lessons 等运行时数据)已删除。');
45
45
  }
46
46
  }
47
47
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
4
  const log = require('../logger');
5
- const { all, byName, resolveTargets } = require('../installers');
5
+ const { all, byName, resolveTargets } = require('../targets');
6
6
  const { ensureRuntime, writeInstalledVersion, readInstalledVersion, getPackageVersion } = require('../skill');
7
7
  const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
8
8
  const { detectTargets } = require('../detect');
@@ -2,12 +2,12 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const check = require('./check');
5
- const verifyUi = require('./verifyUi');
5
+ const visualVerify = require('./visualVerify');
6
6
  const { canonicalResourceType } = require('../worktree/types');
7
7
 
8
8
  function mode(value, fallback, flag) {
9
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.`);
10
+ if (!['always', 'never'].includes(normalized)) throw new Error(`${flag} must be always or never.`);
11
11
  return normalized;
12
12
  }
13
13
 
@@ -21,7 +21,11 @@ function resources(positional) {
21
21
 
22
22
  function viewports(flags) {
23
23
  const value = String(flags.viewport || '').toLowerCase();
24
- if (!value) return [{ name: 'custom', width: flags.width, height: flags.height }];
24
+ if (!value) return [{
25
+ name: flags.width || flags.height ? 'custom' : 'desktop',
26
+ width: flags.width || 1440,
27
+ height: flags.height || 900,
28
+ }];
25
29
  if (value === 'mobile') return [{ name: 'mobile', width: 390, height: 844 }];
26
30
  if (value === 'desktop') return [{ name: 'desktop', width: 1440, height: 900 }];
27
31
  if (value === 'both') return [
@@ -31,13 +35,29 @@ function viewports(flags) {
31
35
  throw new Error('--viewport must be mobile, desktop, or both.');
32
36
  }
33
37
 
38
+ function visualRequest(flags, url) {
39
+ const uiMode = mode(flags.ui, 'never', '--ui');
40
+ const screenshotMode = String(flags.screenshot || 'never').toLowerCase();
41
+ if (!['always', 'never'].includes(screenshotMode)) {
42
+ throw new Error('--screenshot must be always or never.');
43
+ }
44
+ const screenshotRequested = screenshotMode !== 'never';
45
+ if ((uiMode === 'always' || screenshotRequested) && !url) {
46
+ throw new Error('Visual verification requires --url <http://localhost:port/path>.');
47
+ }
48
+ return {
49
+ uiMode,
50
+ screenshotMode,
51
+ captureOnly: uiMode === 'never' && screenshotMode === 'always',
52
+ run: Boolean(url) && (uiMode === 'always' || screenshotRequested),
53
+ };
54
+ }
55
+
34
56
  async function verify(projectDir, positional = [], flags = {}) {
35
57
  let selected;
36
- let uiMode;
37
58
  let remoteMode;
38
59
  try {
39
60
  selected = resources(positional);
40
- uiMode = mode(flags.ui, 'auto', '--ui');
41
61
  remoteMode = flags.remote ? 'always' : 'never';
42
62
  } catch (error) {
43
63
  log.err(error.message);
@@ -55,31 +75,32 @@ async function verify(projectDir, positional = [], flags = {}) {
55
75
  if (checkCode !== 0) return checkCode;
56
76
 
57
77
  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
- }
78
+ let visual;
79
+ try { visual = visualRequest(flags, url); }
80
+ catch (error) { log.err(error.message); return 1; }
81
+ if (!visual.run) {
82
+ if (url) log.warn('--url was provided without --screenshot always or --ui always; visual verification was skipped.');
64
83
  log.ok('Unified verification passed (UI skipped).');
65
84
  return 0;
66
85
  }
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
86
  let targets;
73
87
  try { targets = viewports(flags); }
74
88
  catch (error) { log.err(error.message); return 1; }
75
89
  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);
90
+ const uiFlags = {
91
+ ...flags,
92
+ url,
93
+ width: viewport.width,
94
+ height: viewport.height,
95
+ 'capture-only': visual.captureOnly,
96
+ };
97
+ const code = await visualVerify(projectDir, [url], uiFlags);
81
98
  if (code !== 0) return code;
82
99
  }
100
+ if (visual.captureOnly) {
101
+ log.ok(`Visual capture completed (${targets.map((item) => item.name).join(', ')}).`);
102
+ return 0;
103
+ }
83
104
  log.ok(`Unified verification passed (${targets.map((item) => item.name).join(', ')}).`);
84
105
  return 0;
85
106
  }
@@ -88,3 +109,4 @@ module.exports = verify;
88
109
  module.exports.mode = mode;
89
110
  module.exports.resources = resources;
90
111
  module.exports.viewports = viewports;
112
+ module.exports.visualRequest = visualRequest;
@@ -3,53 +3,8 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const runtimeFiles = require('../runtimeFiles');
6
- const { spawnSync } = require('child_process');
7
6
  const log = require('../logger');
8
7
  const { configPath, loadProjectConfig } = require('../projectConfig');
9
- const { loadManifest, getEntry } = require('../worktree/manifest');
10
- const { canonicalResourceType } = require('../worktree/types');
11
- const { resolveMetadata } = require('../worktree/backend');
12
- const { inspectEntry, openMetadataSession } = require('../worktree/status');
13
-
14
- const UI_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte']);
15
-
16
- function isUiFile(file) {
17
- const normalized = String(file || '').replace(/\\/g, '/').toLowerCase();
18
- const ext = path.extname(normalized);
19
- if (UI_EXTENSIONS.has(ext)) return true;
20
- if (!['.js', '.ts'].includes(ext)) return false;
21
- return /(^|\/)(frontend|web|ui|components|pages|views|client)(\/|$)/.test(normalized)
22
- || /(^|\/)(app|main|client)\.(js|ts)$/.test(normalized)
23
- || normalized.startsWith('.draftgo/worktree/pages/')
24
- || normalized.startsWith('.draftgo/worktree/navigations/')
25
- || normalized.startsWith('.draftgo/worktree/docs/');
26
- }
27
-
28
- function gitChangedFiles(projectDir) {
29
- const r = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
30
- cwd: projectDir,
31
- encoding: 'utf8',
32
- shell: false,
33
- });
34
- if (r.error || r.status !== 0) return null;
35
- return String(r.stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
36
- const raw = line.slice(3).trim();
37
- const renamed = raw.includes(' -> ') ? raw.split(' -> ').pop() : raw;
38
- return renamed.replace(/^"|"$/g, '');
39
- });
40
- }
41
-
42
- function decideMobileCheck(projectDir, mode) {
43
- if (mode === 'always') return { run: true, reason: 'mobile-check=always' };
44
- if (mode === 'never') return { run: false, reason: 'mobile-check=never' };
45
- const changed = gitChangedFiles(projectDir);
46
- if (changed === null || changed.length === 0) {
47
- return { run: true, reason: '无法从 Git diff 确认影响范围,执行单视口 smoke check' };
48
- }
49
- const uiFiles = changed.filter(isUiFile);
50
- if (!uiFiles.length) return { run: false, reason: '本次变更未涉及页面、样式或前端组件' };
51
- return { run: true, reason: `检测到 ${uiFiles.length} 个 UI 文件变更` };
52
- }
53
8
 
54
9
  function numberFlag(value, fallback, min, max) {
55
10
  const n = Number(value);
@@ -154,15 +109,6 @@ async function launchBrowser(chromium, requested, requestedPath) {
154
109
  + (lastError ? `;最后错误:${lastError.message.split('\n')[0]}` : ''));
155
110
  }
156
111
 
157
- function parseResourceSpec(value) {
158
- const raw = String(value || '');
159
- const separator = raw.indexOf(':');
160
- if (separator <= 0 || separator === raw.length - 1) {
161
- throw new Error('--resource 格式必须是 <pages|nav|docs>:<id>。');
162
- }
163
- return { resourceType: canonicalResourceType(raw.slice(0, separator)), resourceId: raw.slice(separator + 1) };
164
- }
165
-
166
112
  async function selectFrames(page, mode = 'auto') {
167
113
  const top = page.mainFrame ? page.mainFrame() : page;
168
114
  const selected = [{ frame: top, label: 'top' }];
@@ -208,50 +154,33 @@ async function inspectFrame(frame) {
208
154
  });
209
155
  }
210
156
 
211
- async function verifyRemoteResource(projectDir, spec) {
212
- const { resourceType, resourceId } = parseResourceSpec(spec);
213
- const entry = getEntry(loadManifest(projectDir), resourceType, resourceId);
214
- if (!entry) throw new Error(`${resourceType} ${resourceId} 未 checkout。`);
215
- const config = loadProjectConfig(projectDir);
216
- const session = await openMetadataSession(config);
217
- const remote = await resolveMetadata(config, resourceType, resourceId, {
218
- ...session,
219
- clientInitialized: true,
220
- });
221
- return inspectEntry(projectDir, entry, remote);
157
+ async function captureScreenshot(projectDir, page) {
158
+ const dir = path.join(projectDir, '.draftgo', 'artifacts', 'ui');
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ const screenshotPath = path.join(dir, `ui-check-${process.pid}-${Date.now()}.png`);
161
+ await page.screenshot({ path: screenshotPath, fullPage: true });
162
+ runtimeFiles.register(projectDir, screenshotPath, 'ui-artifact', 'verify');
163
+ return screenshotPath;
222
164
  }
223
165
 
224
- async function verifyUi(projectDir, positional, flags = {}) {
166
+ async function visualVerify(projectDir, positional, flags = {}) {
225
167
  const rawUrl = String(flags.url || positional[0] || '').trim();
226
168
  if (!/^https?:\/\//i.test(rawUrl)) {
227
- log.err('用法:draftgo verify-ui <http://localhost:port/path>');
169
+ log.err('Usage: draftgo verify --url <http://localhost:port/path> --screenshot always|--ui always');
228
170
  return 1;
229
171
  }
230
172
 
231
173
  const tokenMode = String(flags.token || 'auto').toLowerCase();
232
174
  if (!['auto', 'never'].includes(tokenMode)) {
233
- log.err('--token verify-ui 中只支持 autonever');
175
+ log.err('--token in visual verification accepts auto or never.');
234
176
  return 1;
235
177
  }
236
178
 
237
- const mode = String(flags['mobile-check'] || 'auto').toLowerCase();
238
- if (!['auto', 'always', 'never'].includes(mode)) {
239
- log.err('--mobile-check 只支持 auto、always、never。');
240
- return 1;
241
- }
242
- const decision = flags.resource
243
- ? { run: true, reason: `resource=${flags.resource}` }
244
- : decideMobileCheck(projectDir, mode);
245
- if (!decision.run) {
246
- log.ok(`跳过 UI smoke check:${decision.reason}`);
247
- return 0;
248
- }
249
-
250
179
  let url;
251
180
  try {
252
181
  url = configuredUiUrl(projectDir, rawUrl, tokenMode);
253
182
  } catch (err) {
254
- log.err(`无法读取 verify-ui 的项目配置:${err.message}`);
183
+ log.err(`Unable to read visual verification config: ${err.message}`);
255
184
  return 1;
256
185
  }
257
186
 
@@ -263,30 +192,18 @@ async function verifyUi(projectDir, positional, flags = {}) {
263
192
  return 1;
264
193
  }
265
194
 
266
- const width = numberFlag(flags.width, 390, 240, 3840);
267
- const height = numberFlag(flags.height, 844, 320, 2160);
195
+ const width = numberFlag(flags.width, 1440, 240, 3840);
196
+ const height = numberFlag(flags.height, 900, 320, 2160);
268
197
  const waitMs = numberFlag(flags['wait-ms'], 500, 0, 10000);
269
198
  const screenshotMode = String(flags.screenshot || 'never').toLowerCase();
270
- if (!['on-failure', 'always', 'never'].includes(screenshotMode)) {
271
- log.err('--screenshot 只支持 on-failure、alwaysnever');
199
+ if (!['always', 'never'].includes(screenshotMode)) {
200
+ log.err('--screenshot accepts always or never.');
272
201
  return 1;
273
202
  }
274
203
 
275
204
  let browser;
276
205
  try {
277
- if (flags.resource) {
278
- const resource = await verifyRemoteResource(projectDir, flags.resource);
279
- log.info(`UI source: remote committed ${resource.resource_type} ${resource.resource_id}`);
280
- log.info(`Remote version: ${resource.remote_version || '-'}; remote hash: ${resource.remote_hash || '-'}`);
281
- if (['local_modified', 'diverged'].includes(resource.state)) {
282
- log.warn(`Local worktree is not committed (${resource.state}); verify-ui will test the remote version.`);
283
- }
284
- if (!resource.local_matches_remote && resource.state !== 'local_modified') {
285
- throw new Error(`Local and remote content differ (${resource.state}); UI verification stopped.`);
286
- }
287
- } else {
288
- log.info('UI source: remote URL response; no local worktree version was asserted.');
289
- }
206
+ log.info('UI source: requested URL response.');
290
207
  const launch = await launchBrowser(
291
208
  chromium,
292
209
  flags.browser && String(flags.browser),
@@ -302,6 +219,15 @@ async function verifyUi(projectDir, positional, flags = {}) {
302
219
 
303
220
  const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
304
221
  if (waitMs) await page.waitForTimeout(waitMs);
222
+ let screenshotPath = null;
223
+ if (screenshotMode === 'always' || flags['capture-only']) {
224
+ screenshotPath = await captureScreenshot(projectDir, page);
225
+ }
226
+ if (flags['capture-only']) {
227
+ log.ok(`截图已生成:${width}x${height}`);
228
+ log.info(`截图:${screenshotPath}`);
229
+ return 0;
230
+ }
305
231
  const frames = await selectFrames(page, String(flags.frame || 'auto'));
306
232
  const states = [];
307
233
  for (const item of frames) states.push({ ...item, state: await inspectFrame(item.frame) });
@@ -326,16 +252,6 @@ async function verifyUi(projectDir, positional, flags = {}) {
326
252
  else log.info(`Selector ${flags.selector}: visible in ${match}`);
327
253
  }
328
254
 
329
- const shouldScreenshot = screenshotMode === 'always' || (screenshotMode === 'on-failure' && issues.length > 0);
330
- let screenshotPath = null;
331
- if (shouldScreenshot) {
332
- const dir = path.join(projectDir, '.draftgo', 'artifacts', 'ui');
333
- fs.mkdirSync(dir, { recursive: true });
334
- screenshotPath = path.join(dir, `ui-check-${process.pid}-${Date.now()}.png`);
335
- await page.screenshot({ path: screenshotPath, fullPage: true });
336
- runtimeFiles.register(projectDir, screenshotPath, 'ui-artifact', 'verify-ui');
337
- }
338
-
339
255
  if (issues.length) {
340
256
  issues.forEach((issue) => log.err(issue));
341
257
  if (consoleErrors.length) consoleErrors.slice(0, 5).forEach((msg) => log.dim(` console: ${msg}`));
@@ -344,7 +260,7 @@ async function verifyUi(projectDir, positional, flags = {}) {
344
260
  return 1;
345
261
  }
346
262
 
347
- log.ok(`UI smoke check 通过:${width}x${height},${frames.length} frame(s),${decision.reason}`);
263
+ log.ok(`UI smoke check passed: ${width}x${height}, ${frames.length} frame(s).`);
348
264
  if (screenshotPath) log.info(`截图:${screenshotPath}`);
349
265
  return 0;
350
266
  } catch (err) {
@@ -355,15 +271,11 @@ async function verifyUi(projectDir, positional, flags = {}) {
355
271
  }
356
272
  }
357
273
 
358
- module.exports = verifyUi;
359
- module.exports.decideMobileCheck = decideMobileCheck;
360
- module.exports.gitChangedFiles = gitChangedFiles;
361
- module.exports.isUiFile = isUiFile;
274
+ module.exports = visualVerify;
362
275
  module.exports.configuredUiUrl = configuredUiUrl;
363
276
  module.exports.redactUrlTokens = redactUrlTokens;
364
277
  module.exports.executableCandidates = executableCandidates;
365
278
  module.exports.playwrightCacheCandidates = playwrightCacheCandidates;
366
- module.exports.parseResourceSpec = parseResourceSpec;
367
- module.exports.verifyRemoteResource = verifyRemoteResource;
368
279
  module.exports.selectFrames = selectFrames;
369
280
  module.exports.inspectFrame = inspectFrame;
281
+ module.exports.captureScreenshot = captureScreenshot;