draftgo-cli 3.0.56 → 4.0.22

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 (96) hide show
  1. package/README.md +169 -297
  2. package/package.json +12 -7
  3. package/resources/custom-service-sdk/ai.go +520 -0
  4. package/resources/custom-service-sdk/ai_test.go +156 -0
  5. package/resources/custom-service-sdk/billing.go +596 -0
  6. package/resources/custom-service-sdk/billing_test.go +150 -0
  7. package/resources/custom-service-sdk/go.mod +3 -0
  8. package/resources/custom-service-sdk/manifest.json +72 -0
  9. package/resources/custom-service-sdk/platform.go +360 -0
  10. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  11. package/resources/custom-service-sdk/registration_test.go +39 -0
  12. package/resources/custom-service-sdk/resources.go +246 -0
  13. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  14. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  15. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  16. package/resources/custom-service-sdk/sdk.go +208 -0
  17. package/resources/skill/SKILL.md +36 -87
  18. package/resources/skill/init/SKILL.md +9 -14
  19. package/resources/skill/manifest.json +6 -2
  20. package/resources/skill/references/aihub.md +28 -5
  21. package/resources/skill/references/app-api.md +56 -6
  22. package/resources/skill/references/architecture.md +2 -2
  23. package/resources/skill/references/chat-sdk.md +4 -2
  24. package/resources/skill/references/checkout.md +21 -7
  25. package/resources/skill/references/custom-services.md +124 -222
  26. package/resources/skill/references/data.md +22 -6
  27. package/resources/skill/references/delivery.md +33 -0
  28. package/resources/skill/references/diagnostics.md +51 -0
  29. package/resources/skill/references/frontend.md +93 -499
  30. package/resources/skill/references/mcp.md +65 -101
  31. package/resources/skill/references/methods.md +189 -0
  32. package/resources/skill/references/modules.md +36 -8
  33. package/resources/skill/references/runtime.md +26 -3
  34. package/resources/skill/story/SKILL.md +1 -2
  35. package/src/apiContractCache.js +112 -0
  36. package/src/cli.js +24 -20
  37. package/src/commandRegistry.js +15 -12
  38. package/src/commands/api.js +41 -10
  39. package/src/commands/apiKey.js +34 -0
  40. package/src/commands/capabilities.js +93 -0
  41. package/src/commands/check.js +1 -10
  42. package/src/commands/checkout.js +1 -1
  43. package/src/commands/commit.js +1 -1
  44. package/src/commands/components.js +550 -0
  45. package/src/commands/conflict.js +1 -1
  46. package/src/commands/connect.js +18 -8
  47. package/src/commands/customService.js +22 -8
  48. package/src/commands/dataRange.js +33 -0
  49. package/src/commands/delete.js +34 -46
  50. package/src/commands/deploy.js +1 -1
  51. package/src/commands/diff.js +18 -2
  52. package/src/commands/grant.js +29 -0
  53. package/src/commands/group.js +38 -0
  54. package/src/commands/help.js +80 -51
  55. package/src/commands/init.js +6 -12
  56. package/src/commands/listTargets.js +1 -1
  57. package/src/commands/local.js +2 -6
  58. package/src/commands/map.js +145 -28
  59. package/src/commands/mcp.js +2 -2
  60. package/src/commands/reconcile.js +1 -1
  61. package/src/commands/role.js +32 -0
  62. package/src/commands/space.js +41 -0
  63. package/src/commands/status.js +111 -8
  64. package/src/commands/uninstall.js +3 -3
  65. package/src/commands/update.js +24 -12
  66. package/src/commands/verify.js +118 -21
  67. package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
  68. package/src/commands/worklog.js +90 -0
  69. package/src/consoleEncoding.js +34 -0
  70. package/src/contractCompatibility.js +57 -0
  71. package/src/customServices.js +278 -41
  72. package/src/diffReport.js +106 -0
  73. package/src/index.js +2 -0
  74. package/src/{localdev → localRuntime}/compose.js +14 -17
  75. package/src/{localdev → localRuntime}/detect.js +1 -1
  76. package/src/{localdev → localRuntime}/index.js +22 -23
  77. package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
  78. package/src/{localdev → localRuntime}/services.js +28 -37
  79. package/src/mcp/client.js +11 -2
  80. package/src/mcp/protocol.js +2 -2
  81. package/src/mcp/tools.js +14 -1
  82. package/src/platforms.js +9 -0
  83. package/src/projectConfig.js +8 -4
  84. package/src/releaseInstall.js +105 -0
  85. package/src/{installers/index.js → targets.js} +3 -5
  86. package/src/updateCheck.js +48 -28
  87. package/src/worklog.js +275 -0
  88. package/src/workspaceHealth.js +1 -1
  89. package/src/worktree/backend.js +1 -1
  90. package/src/worktree/index.js +86 -51
  91. package/src/changelog.js +0 -276
  92. package/src/commands/changelog.js +0 -24
  93. package/src/commands/localDev.js +0 -9
  94. package/src/commands/sync.js +0 -46
  95. package/src/commands/task.js +0 -408
  96. package/src/commands/verifyUiCompat.js +0 -16
@@ -2,12 +2,17 @@
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
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const parse5 = require('parse5');
10
+ const { loadProjectConfig } = require('../projectConfig');
11
+ const { loadManifest, getEntry, absolutePath } = require('../worktree/manifest');
7
12
 
8
13
  function mode(value, fallback, flag) {
9
14
  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.`);
15
+ if (!['always', 'never'].includes(normalized)) throw new Error(`${flag} must be always or never.`);
11
16
  return normalized;
12
17
  }
13
18
 
@@ -21,7 +26,11 @@ function resources(positional) {
21
26
 
22
27
  function viewports(flags) {
23
28
  const value = String(flags.viewport || '').toLowerCase();
24
- if (!value) return [{ name: 'custom', width: flags.width, height: flags.height }];
29
+ if (!value) return [{
30
+ name: flags.width || flags.height ? 'custom' : 'desktop',
31
+ width: flags.width || 1440,
32
+ height: flags.height || 900,
33
+ }];
25
34
  if (value === 'mobile') return [{ name: 'mobile', width: 390, height: 844 }];
26
35
  if (value === 'desktop') return [{ name: 'desktop', width: 1440, height: 900 }];
27
36
  if (value === 'both') return [
@@ -31,13 +40,96 @@ function viewports(flags) {
31
40
  throw new Error('--viewport must be mobile, desktop, or both.');
32
41
  }
33
42
 
43
+ function visualRequest(flags, url) {
44
+ const uiMode = mode(flags.ui, 'never', '--ui');
45
+ const screenshotMode = String(flags.screenshot || 'never').toLowerCase();
46
+ if (!['always', 'never'].includes(screenshotMode)) {
47
+ throw new Error('--screenshot must be always or never.');
48
+ }
49
+ const screenshotRequested = screenshotMode !== 'never';
50
+ if ((uiMode === 'always' || screenshotRequested) && !url) {
51
+ throw new Error('Visual verification requires --url <http://localhost:port/path>.');
52
+ }
53
+ return {
54
+ uiMode,
55
+ screenshotMode,
56
+ captureOnly: uiMode === 'never' && screenshotMode === 'always',
57
+ run: Boolean(url) && (uiMode === 'always' || screenshotRequested),
58
+ };
59
+ }
60
+
61
+ function walkNodes(node, visit) {
62
+ if (!node) return;
63
+ visit(node);
64
+ for (const child of node.childNodes || []) walkNodes(child, visit);
65
+ }
66
+
67
+ async function verifyPageComponents(projectDir, selected) {
68
+ const pages = selected.filter(item => item.resourceType === 'pages');
69
+ if (!pages.length) return [];
70
+ const config = loadProjectConfig(projectDir);
71
+ const manifest = loadManifest(projectDir);
72
+ const uses = [];
73
+ for (const page of pages) {
74
+ const entry = getEntry(manifest, 'pages', page.resourceId);
75
+ if (!entry) continue;
76
+ const file = absolutePath(projectDir, entry.local_path);
77
+ const source = fs.readFileSync(file, 'utf8');
78
+ const document = parse5.parse(source);
79
+ const instances = new Set();
80
+ walkNodes(document, node => {
81
+ const attrs = new Map((node.attrs || []).map(attr => [attr.name, attr.value]));
82
+ const name = attrs.get('data-dg-use');
83
+ if (!name) return;
84
+ if (!/^([a-z][a-z0-9-]*)\/([a-z][a-z0-9-]*)$/.test(name)) throw new Error(`pages ${page.resourceId}: invalid data-dg-use "${name}"`);
85
+ const instance = attrs.get('data-dg-instance');
86
+ if (instance) { if (instances.has(instance)) throw new Error(`pages ${page.resourceId}: duplicate data-dg-instance "${instance}"`); instances.add(instance); }
87
+ uses.push({ page: page.resourceId, node, attrs, name });
88
+ });
89
+ }
90
+ const cache = new Map();
91
+ for (const use of uses) {
92
+ if (!cache.has(use.name)) {
93
+ const slug = use.name.split('/')[1];
94
+ const response = await fetch(`${config.server}/api/components?library=${encodeURIComponent(use.name.split('/')[0])}&q=${encodeURIComponent(slug)}&page_size=100`, { headers: { Authorization: `Bearer ${config.token}` } });
95
+ const payload = await response.json().catch(() => ({}));
96
+ if (!response.ok) throw new Error(`component lookup failed for ${use.name} (${response.status})`);
97
+ const items = payload.data?.items || payload.items || [];
98
+ cache.set(use.name, items.find(item => item.slug === slug && (item.library_slug === use.name.split('/')[0] || item.full_name === use.name)) || null);
99
+ }
100
+ const component = cache.get(use.name);
101
+ if (!component) throw new Error(`pages ${use.page}: component not found ${use.name}`);
102
+ if (!component.published || Number(component.published_revision || 0) < 1) throw new Error(`pages ${use.page}: component is not published ${use.name}`);
103
+ const definition = component.published;
104
+ const rootTag = String(definition.root_tag || '').toLowerCase();
105
+ if (rootTag && String(use.node.nodeName || '').toLowerCase() !== rootTag) throw new Error(`pages ${use.page}: ${use.name} requires <${rootTag}> but found <${use.node.nodeName}>`);
106
+ const props = new Map((definition.props || []).map(prop => [prop.name, prop]));
107
+ for (const [name, value] of use.attrs) {
108
+ if (!name.startsWith('data-dg-prop-')) continue;
109
+ const propName = name.slice('data-dg-prop-'.length);
110
+ const prop = props.get(propName);
111
+ if (!prop) throw new Error(`pages ${use.page}: unknown prop ${name} on ${use.name}`);
112
+ if (prop.type === 'boolean' && !['', 'true', 'false', '1', '0'].includes(String(value).toLowerCase())) {
113
+ throw new Error(`pages ${use.page}: invalid boolean prop ${name} on ${use.name}`);
114
+ }
115
+ if (prop.type === 'number' && !Number.isFinite(Number(value))) {
116
+ throw new Error(`pages ${use.page}: invalid number prop ${name} on ${use.name}`);
117
+ }
118
+ }
119
+ const slots = new Set((definition.slots || []).map(slot => slot.name));
120
+ for (const child of use.node.childNodes || []) {
121
+ const slot = (child.attrs || []).find(attr => attr.name === 'data-dg-slot')?.value;
122
+ if (slot && slots.size && !slots.has(slot)) throw new Error(`pages ${use.page}: unknown slot ${slot} on ${use.name}`);
123
+ }
124
+ }
125
+ return uses;
126
+ }
127
+
34
128
  async function verify(projectDir, positional = [], flags = {}) {
35
129
  let selected;
36
- let uiMode;
37
130
  let remoteMode;
38
131
  try {
39
132
  selected = resources(positional);
40
- uiMode = mode(flags.ui, 'auto', '--ui');
41
133
  remoteMode = flags.remote ? 'always' : 'never';
42
134
  } catch (error) {
43
135
  log.err(error.message);
@@ -45,6 +137,8 @@ async function verify(projectDir, positional = [], flags = {}) {
45
137
  }
46
138
 
47
139
  log.title('draftgo verify');
140
+ try { await verifyPageComponents(projectDir, selected); }
141
+ catch (error) { log.err(error.message); return 1; }
48
142
  const checkCode = await check(projectDir, [], {
49
143
  strict: flags.strict,
50
144
  remote: remoteMode === 'always',
@@ -55,31 +149,32 @@ async function verify(projectDir, positional = [], flags = {}) {
55
149
  if (checkCode !== 0) return checkCode;
56
150
 
57
151
  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
- }
152
+ let visual;
153
+ try { visual = visualRequest(flags, url); }
154
+ catch (error) { log.err(error.message); return 1; }
155
+ if (!visual.run) {
156
+ if (url) log.warn('--url was provided without --screenshot always or --ui always; visual verification was skipped.');
64
157
  log.ok('Unified verification passed (UI skipped).');
65
158
  return 0;
66
159
  }
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
160
  let targets;
73
161
  try { targets = viewports(flags); }
74
162
  catch (error) { log.err(error.message); return 1; }
75
163
  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);
164
+ const uiFlags = {
165
+ ...flags,
166
+ url,
167
+ width: viewport.width,
168
+ height: viewport.height,
169
+ 'capture-only': visual.captureOnly,
170
+ };
171
+ const code = await visualVerify(projectDir, [url], uiFlags);
81
172
  if (code !== 0) return code;
82
173
  }
174
+ if (visual.captureOnly) {
175
+ log.ok(`Visual capture completed (${targets.map((item) => item.name).join(', ')}).`);
176
+ return 0;
177
+ }
83
178
  log.ok(`Unified verification passed (${targets.map((item) => item.name).join(', ')}).`);
84
179
  return 0;
85
180
  }
@@ -88,3 +183,5 @@ module.exports = verify;
88
183
  module.exports.mode = mode;
89
184
  module.exports.resources = resources;
90
185
  module.exports.viewports = viewports;
186
+ module.exports.visualRequest = visualRequest;
187
+ module.exports.verifyPageComponents = verifyPageComponents;
@@ -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;
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const {
5
+ appendItem,
6
+ mutateWorklog,
7
+ normalizeDate,
8
+ readWorklog,
9
+ renderWorklog,
10
+ resolveReference,
11
+ updateItem,
12
+ worklogPath,
13
+ } = require('../worklog');
14
+
15
+ function printUsage() {
16
+ log.err('Usage: draftgo work start|add|start-item|complete|show|list <value>');
17
+ }
18
+
19
+ function output(flags, value) {
20
+ if (flags.output === 'json') console.log(JSON.stringify(value, null, 2));
21
+ }
22
+
23
+ function latestReference(blocks) {
24
+ const block = blocks[blocks.length - 1];
25
+ return `${block.date}#${block.entries[block.entries.length - 1].number}`;
26
+ }
27
+
28
+ function resultFor(blocks, reference) {
29
+ const resolved = resolveReference(blocks, reference);
30
+ return { date: resolved.block.date, ...resolved.entry };
31
+ }
32
+
33
+ function add(projectDir, positional, flags, status) {
34
+ const title = positional.join(' ').trim();
35
+ if (!title) { printUsage(); return 1; }
36
+ const result = mutateWorklog(projectDir, (blocks) => {
37
+ const date = normalizeDate(flags.date);
38
+ const next = appendItem(blocks, title, status, flags.note ? [String(flags.note).trim()] : [], date);
39
+ return { blocks: next, reference: latestReference(next) };
40
+ });
41
+ const item = resultFor(result.blocks, result.reference);
42
+ output(flags, { path: result.path, item });
43
+ if (flags.output !== 'json') {
44
+ const verb = status === 'active' ? 'Started' : 'Added';
45
+ log.ok(`${verb} work item #${item.number} (${item.date}#${item.number}): ${item.title}`);
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ function change(projectDir, positional, flags, status) {
51
+ const reference = positional[0];
52
+ if (!reference) { printUsage(); return 1; }
53
+ const result = mutateWorklog(projectDir, (blocks) => {
54
+ const note = flags.note == null ? '' : String(flags.note).trim();
55
+ const next = updateItem(blocks, reference, status, note);
56
+ return { blocks: next, reference };
57
+ });
58
+ output(flags, { path: result.path, item: resultFor(result.blocks, result.reference) });
59
+ if (flags.output !== 'json') log.ok(`Worklog item ${result.reference} marked ${status}.`);
60
+ return 0;
61
+ }
62
+
63
+ function show(projectDir, flags) {
64
+ const blocks = readWorklog(projectDir);
65
+ output(flags, { path: worklogPath(projectDir), blocks });
66
+ if (flags.output !== 'json') console.log(renderWorklog(blocks) || 'Worklog is empty.');
67
+ return 0;
68
+ }
69
+
70
+ function list(projectDir, flags) {
71
+ const blocks = readWorklog(projectDir);
72
+ const items = blocks.flatMap((block) => block.entries.map((entry) => ({ date: block.date, ...entry })));
73
+ output(flags, { path: worklogPath(projectDir), items });
74
+ if (flags.output !== 'json') items.forEach((item) => log.info(`${item.date}#${item.number} [${item.status}] ${item.title}`));
75
+ return 0;
76
+ }
77
+
78
+ async function worklog(projectDir, positional = [], flags = {}) {
79
+ const operation = positional[0] || 'show';
80
+ if (operation === 'start') return add(projectDir, positional.slice(1), flags, 'active');
81
+ if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'pending');
82
+ if (operation === 'start-item' || operation === 'start_item') return change(projectDir, positional.slice(1), flags, 'active');
83
+ if (operation === 'complete' || operation === 'done') return change(projectDir, positional.slice(1), flags, 'completed');
84
+ if (operation === 'show') return show(projectDir, flags);
85
+ if (operation === 'list') return list(projectDir, flags);
86
+ throw new Error(`Unknown worklog operation ${operation}.`);
87
+ }
88
+
89
+ worklog.printUsage = printUsage;
90
+ module.exports = worklog;
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+
5
+ /**
6
+ * Windows console hosts still use the active console code page to decode the
7
+ * UTF-8 bytes Node writes. Switch only an interactive console to UTF-8.
8
+ *
9
+ * This deliberately does not run for redirected stdout: JSON consumers and
10
+ * MCP stdio use pipes, where Node's UTF-8 byte stream is already correct and
11
+ * changing a shared console would be both unnecessary and surprising.
12
+ */
13
+ function configureConsoleUtf8(options = {}) {
14
+ const platform = options.platform || process.platform;
15
+ const stdout = options.stdout || process.stdout;
16
+ const run = options.spawnSync || spawnSync;
17
+
18
+ if (platform !== 'win32' || !stdout || !stdout.isTTY) return false;
19
+
20
+ try {
21
+ // chcp changes the code page for the inherited Windows console. Redirect
22
+ // its own localized status line so stdout remains exclusively CLI output.
23
+ run('cmd.exe', ['/d', '/s', '/c', 'chcp 65001 >nul'], {
24
+ stdio: 'ignore',
25
+ windowsHide: true,
26
+ });
27
+ return true;
28
+ } catch {
29
+ // Encoding setup is a presentation enhancement; never block the command.
30
+ return false;
31
+ }
32
+ }
33
+
34
+ module.exports = { configureConsoleUtf8 };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const HARD_INCOMPATIBLE_CODES = new Set([
4
+ 'CONTRACT_INCOMPATIBLE',
5
+ 'CLIENT_VERSION_UNSUPPORTED',
6
+ 'CLI_VERSION_UNSUPPORTED',
7
+ ]);
8
+
9
+ function errorCode(error) {
10
+ return String(error && error.code || '').trim().toUpperCase();
11
+ }
12
+
13
+ function isContractCompatibilityError(error) {
14
+ const code = errorCode(error);
15
+ return code === 'CONTRACT_CHANGED' || HARD_INCOMPATIBLE_CODES.has(code);
16
+ }
17
+
18
+ function isHardContractIncompatibility(error) {
19
+ return HARD_INCOMPATIBLE_CODES.has(errorCode(error));
20
+ }
21
+
22
+ function isDangerousOperation(contract) {
23
+ const operation = contract && contract.operation || contract || {};
24
+ return operation.destructive === true || String(operation.risk || '').toLowerCase() === 'high';
25
+ }
26
+
27
+ function canonicalValue(value) {
28
+ if (Array.isArray(value)) return value.map(canonicalValue);
29
+ if (!value || typeof value !== 'object') return value;
30
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
31
+ }
32
+
33
+ function sameOperationContract(left, right) {
34
+ const leftOperation = left && left.operation || left || {};
35
+ const rightOperation = right && right.operation || right || {};
36
+ return JSON.stringify(canonicalValue(leftOperation)) === JSON.stringify(canonicalValue(rightOperation));
37
+ }
38
+
39
+ function incompatibleOperationError(operationId, error) {
40
+ const wrapped = new Error(
41
+ `DraftGo CLI and server contracts are incompatible for dangerous operation ${operationId}; `
42
+ + 'the call was blocked before retry. Upgrade DraftGo CLI, then describe and confirm the operation again.',
43
+ );
44
+ wrapped.code = 'CLI_CONTRACT_INCOMPATIBLE';
45
+ wrapped.details = error && (error.details || error.data);
46
+ wrapped.cause = error;
47
+ return wrapped;
48
+ }
49
+
50
+ module.exports = {
51
+ errorCode,
52
+ isContractCompatibilityError,
53
+ isHardContractIncompatibility,
54
+ isDangerousOperation,
55
+ sameOperationContract,
56
+ incompatibleOperationError,
57
+ };