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.
- package/README.md +112 -316
- package/package.json +5 -5
- package/resources/skill/SKILL.md +25 -24
- package/resources/skill/init/SKILL.md +5 -10
- package/resources/skill/manifest.json +2 -2
- package/resources/skill/references/aihub.md +10 -5
- package/resources/skill/references/chat-sdk.md +10 -0
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/custom-services.md +65 -226
- package/resources/skill/references/data.md +3 -2
- package/resources/skill/references/frontend.md +96 -490
- package/resources/skill/references/mcp.md +39 -103
- package/resources/skill/references/runtime.md +3 -2
- package/resources/skill/story/SKILL.md +1 -2
- package/src/apiContractCache.js +112 -0
- package/src/cli.js +1 -21
- package/src/commandRegistry.js +6 -11
- package/src/commands/api.js +28 -8
- package/src/commands/check.js +1 -10
- package/src/commands/customService.js +2 -4
- package/src/commands/delete.js +23 -46
- package/src/commands/deploy.js +1 -1
- package/src/commands/help.js +16 -31
- package/src/commands/init.js +4 -10
- package/src/commands/listTargets.js +1 -1
- package/src/commands/local.js +2 -6
- package/src/commands/map.js +0 -11
- package/src/commands/status.js +1 -1
- package/src/commands/uninstall.js +3 -3
- package/src/commands/update.js +1 -1
- package/src/commands/verify.js +43 -21
- package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
- package/src/commands/worklog.js +86 -0
- package/src/customServices.js +150 -33
- package/src/{localdev → localRuntime}/detect.js +1 -1
- package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
- package/src/{localdev → localRuntime}/services.js +1 -1
- package/src/projectConfig.js +2 -0
- package/src/{installers/index.js → targets.js} +3 -5
- package/src/worklog.js +274 -0
- package/src/workspaceHealth.js +1 -1
- package/src/worktree/index.js +81 -51
- package/src/changelog.js +0 -276
- package/src/commands/changelog.js +0 -24
- package/src/commands/localDev.js +0 -9
- package/src/commands/sync.js +0 -46
- package/src/commands/task.js +0 -408
- package/src/commands/verifyUiCompat.js +0 -16
- /package/src/{localdev → localRuntime}/compose.js +0 -0
- /package/src/{localdev → localRuntime}/index.js +0 -0
package/src/commands/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { all } = require('../
|
|
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
|
|
47
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
135
|
-
--ui <mode> (verify)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
171
|
-
draftgo
|
|
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
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -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('../
|
|
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
|
|
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
|
|
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('
|
|
103
|
+
return await require('../localRuntime').runWizard(projectDir, { yes: true });
|
|
110
104
|
}
|
|
111
105
|
if (choice !== 'C') {
|
|
112
106
|
log.warn('无效选择,已跳过基座处理。');
|
package/src/commands/local.js
CHANGED
|
@@ -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('../
|
|
8
|
-
const { runWizard } = require('../
|
|
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
|
}
|
package/src/commands/map.js
CHANGED
|
@@ -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
|
|
package/src/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
|
-
const { all, resolveTargets } = require('../
|
|
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/
|
|
41
|
+
// .draftgo/ (config/worklog/lessons/cache etc.).
|
|
42
42
|
if (flags.purge) {
|
|
43
43
|
if (removePath(dgDir(projectDir))) {
|
|
44
|
-
log.warn('已使用 --purge:.draftgo/(含 config/
|
|
44
|
+
log.warn('已使用 --purge:.draftgo/(含 config/worklog/lessons 等运行时数据)已删除。');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
package/src/commands/update.js
CHANGED
|
@@ -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('../
|
|
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');
|
package/src/commands/verify.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
4
|
const check = require('./check');
|
|
5
|
-
const
|
|
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 (!['
|
|
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 [{
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
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('
|
|
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
|
|
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(
|
|
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,
|
|
267
|
-
const height = numberFlag(flags.height,
|
|
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 (!['
|
|
271
|
-
log.err('--screenshot
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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;
|