draftgo-cli 3.0.44 → 3.0.49
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 +12 -6
- package/package.json +1 -1
- package/resources/skill/SKILL.md +63 -136
- package/resources/skill/manifest.json +1 -3
- package/resources/skill/references/aihub.md +3 -4
- package/resources/skill/references/app-api.md +6 -51
- package/resources/skill/references/architecture.md +3 -28
- package/resources/skill/references/checkout.md +2 -1
- package/resources/skill/references/custom-services.md +4 -15
- package/resources/skill/references/data.md +1 -118
- package/resources/skill/references/frontend.md +26 -68
- package/resources/skill/references/mcp.md +47 -0
- package/resources/skill/references/modules.md +7 -7
- package/resources/skill/references/runtime.md +3 -24
- package/src/cli.js +2 -0
- package/src/commandRegistry.js +1 -0
- package/src/commands/api.js +102 -0
- package/src/commands/autoPush.js +1 -1
- package/src/commands/check.js +24 -2
- package/src/commands/commit.js +31 -5
- package/src/commands/deploy.js +1 -1
- package/src/commands/help.js +11 -3
- package/src/commands/init.js +1 -1
- package/src/commands/map.js +20 -22
- package/src/commands/mcp.js +26 -3
- package/src/commands/reconcile.js +20 -0
- package/src/commands/verifyUi.js +92 -10
- package/src/context/index.js +117 -47
- package/src/mcp/client.js +52 -19
- package/src/projectMap.js +7 -2
- package/src/worktree/index.js +272 -59
- package/src/worktree/status.js +122 -0
- package/resources/skill/push/SKILL.md +0 -62
- package/resources/skill/references/api-endpoints.md +0 -180
- package/resources/skill/references/debugging-syntax.md +0 -308
- package/resources/skill/references/parallel.md +0 -56
- package/resources/skill/references/security.md +0 -74
- package/resources/skill/references/ui-protocol.md +0 -99
- package/resources/skill/scripts/README.md +0 -10
package/src/commands/commit.js
CHANGED
|
@@ -9,11 +9,37 @@ async function commit(projectDir, positional, flags = {}) {
|
|
|
9
9
|
log.err('Usage: draftgo commit <pages|nav|docs> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
12
|
+
const streamed = [];
|
|
13
|
+
const printStatus = (result) => {
|
|
14
|
+
streamed.push(result);
|
|
15
|
+
if (flags.output === 'json') return;
|
|
16
|
+
if (result.status === 'committed') log.ok(`${result.resource_type} ${result.resource_id}: committed`);
|
|
17
|
+
else if (result.status === 'unchanged') log.dim(`${result.resource_type} ${result.resource_id}: unchanged`);
|
|
18
|
+
else if (result.status === 'failed') log.err(`${result.resource_type} ${result.resource_id}: failed (${result.code}) - ${result.message}`
|
|
19
|
+
+ (result.remote_change_possible ? ' Remote change is possible; run draftgo check --remote.' : ''));
|
|
20
|
+
else log.warn(`${result.resource_type} ${result.resource_id}: not started`);
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const results = await commitResources(projectDir, resourceType, ids, { onStatus: printStatus });
|
|
24
|
+
if (flags.output === 'json') console.log(JSON.stringify({
|
|
25
|
+
completed: results.filter((item) => ['committed', 'unchanged'].includes(item.status)),
|
|
26
|
+
failed: [],
|
|
27
|
+
not_started: [],
|
|
28
|
+
}, null, 2));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const batch = error.details && (error.details.batch || error.details);
|
|
31
|
+
const summary = batch && Array.isArray(batch.completed) ? batch : {
|
|
32
|
+
completed: streamed.filter((item) => ['committed', 'unchanged'].includes(item.status)),
|
|
33
|
+
failed: streamed.filter((item) => item.status === 'failed'),
|
|
34
|
+
not_started: streamed.filter((item) => item.status === 'not_started'),
|
|
35
|
+
};
|
|
36
|
+
if (flags.output === 'json') console.log(JSON.stringify({ error: {
|
|
37
|
+
code: error.code || 'COMMIT_FAILED', message: error.message,
|
|
38
|
+
}, ...summary }, null, 2));
|
|
39
|
+
else log.err(`Commit batch stopped: ${summary.completed.length} completed, `
|
|
40
|
+
+ `${summary.failed.length} failed, ${summary.not_started.length} not started. `
|
|
41
|
+
+ `${summary.completed.length ? 'Remote changes already occurred for completed resources.' : 'No remote content was changed.'}`);
|
|
42
|
+
return 1;
|
|
17
43
|
}
|
|
18
44
|
return 0;
|
|
19
45
|
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -11,7 +11,7 @@ async function deploy(projectDir, positional, flags = {}) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
log.step('deploy: validating checked-out content...');
|
|
14
|
-
const checkCode = check(projectDir, { strict: flags.strict });
|
|
14
|
+
const checkCode = await check(projectDir, { strict: flags.strict });
|
|
15
15
|
if (checkCode !== 0) {
|
|
16
16
|
log.err('Deploy stopped because local validation failed.');
|
|
17
17
|
return checkCode;
|
package/src/commands/help.js
CHANGED
|
@@ -14,7 +14,7 @@ function help() {
|
|
|
14
14
|
console.log(`draftgo v${getPackageVersion()} - DraftGo workbench CLI for AI coding agents
|
|
15
15
|
|
|
16
16
|
DraftGo Next frontend baseline: React + Vite.
|
|
17
|
-
Database pages use
|
|
17
|
+
Database pages may use native web technologies or bundled component libraries.
|
|
18
18
|
MCP handles live discovery and structured resources; complete page, navigation,
|
|
19
19
|
and document bodies use checkout/commit outside MCP context.
|
|
20
20
|
The context command combines exact local Reference sections with live MCP data;
|
|
@@ -45,8 +45,10 @@ Usage:
|
|
|
45
45
|
draftgo checkout <type> <id...> Download pages/nav/docs body + verified base.
|
|
46
46
|
--force explicitly replaces local changes.
|
|
47
47
|
draftgo check Validate checked-out worktree/base files only.
|
|
48
|
+
--remote also compares remote hash/version.
|
|
48
49
|
draftgo diff <type> <id> Show checkout base versus local body.
|
|
49
50
|
draftgo commit <type> <id...> Validate and upload complete checked-out bodies.
|
|
51
|
+
draftgo reconcile <type> <id...> Repair stale metadata only when local equals remote.
|
|
50
52
|
draftgo conflicts List unresolved conflicts; --all includes resolved.
|
|
51
53
|
draftgo conflict show <type> <id>
|
|
52
54
|
Show preserved base/local/remote paths.
|
|
@@ -58,6 +60,8 @@ Usage:
|
|
|
58
60
|
draftgo api <query> Search the live API contract through MCP.
|
|
59
61
|
draftgo api describe <operation_id>
|
|
60
62
|
Describe one live API operation through MCP.
|
|
63
|
+
draftgo api call <operation_id> --input <json-file>
|
|
64
|
+
Call one described operation with UTF-8 JSON.
|
|
61
65
|
draftgo delete <operation_id> [id]
|
|
62
66
|
Confirm and call a live delete operation.
|
|
63
67
|
Legacy <type> <id> works only when unambiguous.
|
|
@@ -106,10 +110,11 @@ Important flags:
|
|
|
106
110
|
content | project.
|
|
107
111
|
--type <type> (map) pages | nav/navigations | docs/articles.
|
|
108
112
|
--strict Treat check warnings as failures.
|
|
113
|
+
--remote (check) Compare checkout hashes/versions with remote.
|
|
109
114
|
--yes Skip supported confirmation prompts.
|
|
110
115
|
--operation-id <id> (delete) Select an operation explicitly.
|
|
111
|
-
--params <json> (delete) Pass an API parameter object.
|
|
112
|
-
--input <file> (delete) Read
|
|
116
|
+
--params <json> (api call/delete) Pass an API parameter object.
|
|
117
|
+
--input <file> (api call/delete) Read a UTF-8 JSON parameter object.
|
|
113
118
|
--delivery <mode> (deploy) local | preview | deploy.
|
|
114
119
|
--dry-run (push) Show diffs without committing.
|
|
115
120
|
--mobile-check <mode> (verify-ui) auto | always | never.
|
|
@@ -117,6 +122,9 @@ Important flags:
|
|
|
117
122
|
SAT to same-origin URLs as the token query parameter.
|
|
118
123
|
--screenshot <mode> (verify-ui) on-failure | always | never.
|
|
119
124
|
--browser <name> (verify-ui) chromium | chrome | msedge.
|
|
125
|
+
--browser-path <file> (verify-ui) Explicit browser executable; environment
|
|
126
|
+
fallback: DRAFTGO_BROWSER_PATH.
|
|
127
|
+
--resource <type:id> (verify-ui) Compare local and remote content first.
|
|
120
128
|
--selector <css> (verify-ui) Require a visible key element.
|
|
121
129
|
--width/--height <px> (verify-ui) Override the default 390x844 viewport.
|
|
122
130
|
|
package/src/commands/init.js
CHANGED
|
@@ -41,7 +41,7 @@ async function init(projectDir, positional, flags) {
|
|
|
41
41
|
|
|
42
42
|
// 2) Render skill body into each target's own directory (no shared
|
|
43
43
|
// .draftgo/skill/ indirection anymore — each AI tool gets a real copy).
|
|
44
|
-
log.step('写入各 AI 工具 skill 目录(含 SKILL.md / 子技能 / references
|
|
44
|
+
log.step('写入各 AI 工具 skill 目录(含 SKILL.md / 子技能 / references)');
|
|
45
45
|
ensureRuntime(projectDir);
|
|
46
46
|
const failures = [];
|
|
47
47
|
let installedCount = 0;
|
package/src/commands/map.js
CHANGED
|
@@ -4,11 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const log = require('../logger');
|
|
6
6
|
const { loadProjectConfig } = require('../projectConfig');
|
|
7
|
-
const { loadManifest, absolutePath } = require('../worktree/manifest');
|
|
8
|
-
const { hashFile } = require('../worktree/streams');
|
|
9
7
|
const { canonicalResourceType } = require('../worktree/types');
|
|
10
8
|
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
11
9
|
const { allWithAbort } = require('../mcp/parallel');
|
|
10
|
+
const { inspectRemoteCheckouts } = require('../worktree/status');
|
|
12
11
|
|
|
13
12
|
const REMOTE_RESOURCE_TYPES = Object.freeze({
|
|
14
13
|
pages: 'pages',
|
|
@@ -82,23 +81,6 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
|
|
|
82
81
|
return groups.flat();
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
async function localCheckouts(projectDir) {
|
|
86
|
-
const manifest = loadManifest(projectDir);
|
|
87
|
-
const entries = [];
|
|
88
|
-
for (const entry of Object.values(manifest.entries)) {
|
|
89
|
-
const local = absolutePath(projectDir, entry.local_path);
|
|
90
|
-
let currentHash = null;
|
|
91
|
-
if (fs.existsSync(local)) currentHash = (await hashFile(local)).hash;
|
|
92
|
-
entries.push({
|
|
93
|
-
...entry,
|
|
94
|
-
exists: currentHash !== null,
|
|
95
|
-
current_hash: currentHash,
|
|
96
|
-
changed: currentHash !== null && currentHash !== entry.base_hash,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return entries;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
84
|
function legacyCaches(projectDir) {
|
|
103
85
|
return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
|
|
104
86
|
.filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
|
|
@@ -113,7 +95,20 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
113
95
|
(options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
|
|
114
96
|
(options) => listRemoteResources(session, resourceTypes, options),
|
|
115
97
|
], options),
|
|
116
|
-
() =>
|
|
98
|
+
async (options) => {
|
|
99
|
+
const remoteStatus = await inspectRemoteCheckouts(projectDir, {
|
|
100
|
+
config,
|
|
101
|
+
client: session.client,
|
|
102
|
+
tools: session.tools,
|
|
103
|
+
signal: options.signal,
|
|
104
|
+
});
|
|
105
|
+
return remoteStatus.map((entry) => ({
|
|
106
|
+
...entry,
|
|
107
|
+
exists: entry.local_hash !== null,
|
|
108
|
+
current_hash: entry.local_hash,
|
|
109
|
+
changed: entry.local_hash !== null && entry.local_hash !== entry.base_hash,
|
|
110
|
+
}));
|
|
111
|
+
},
|
|
117
112
|
]);
|
|
118
113
|
const [overview, resources] = remote;
|
|
119
114
|
const result = {
|
|
@@ -138,8 +133,11 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
138
133
|
for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
|
|
139
134
|
log.info(`Local checkouts: ${checkouts.length}`);
|
|
140
135
|
for (const entry of checkouts) {
|
|
141
|
-
const state = !entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean';
|
|
142
|
-
log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}
|
|
136
|
+
const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
|
|
137
|
+
log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
|
|
138
|
+
+ ` local=${entry.local_hash || entry.current_hash || '-'} base=${entry.manifest_hash || entry.base_hash || '-'} remote=${entry.remote_hash || '-'} `
|
|
139
|
+
+ `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
|
|
140
|
+
+ `(${entry.local_path})`);
|
|
143
141
|
}
|
|
144
142
|
if (result.legacy_cache.detected.length) {
|
|
145
143
|
log.warn(`Ignored legacy cache indexes: ${result.legacy_cache.detected.join(', ')}`);
|
package/src/commands/mcp.js
CHANGED
|
@@ -13,6 +13,20 @@ const {
|
|
|
13
13
|
const { serveStdio } = require('../mcp/stdio');
|
|
14
14
|
const { parseTimeout } = require('../timeout');
|
|
15
15
|
|
|
16
|
+
function classifyMcpFailure(error) {
|
|
17
|
+
const message = String(error && error.message || error || '');
|
|
18
|
+
const code = error && error.code;
|
|
19
|
+
const status = error && error.status;
|
|
20
|
+
if (status === 401 || status === 403) return 'SAT authentication or authorization failed.';
|
|
21
|
+
if (/session|uninitialized|initialize first|not initialized/i.test(message)
|
|
22
|
+
|| ['SESSION_EXPIRED', 'MCP_SESSION_EXPIRED', -32002].includes(code)) {
|
|
23
|
+
return 'The MCP session was lost or the server rejected the initialized state; check sticky sessions and service restarts.';
|
|
24
|
+
}
|
|
25
|
+
if (status >= 500) return 'The MCP service returned a server error; inspect the service request ID and logs.';
|
|
26
|
+
if (/tools\/call|tool/i.test(message)) return 'Tool invocation failed after protocol setup; inspect the tool contract and server handler.';
|
|
27
|
+
return 'Connection or MCP protocol negotiation failed.';
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function targetArgs(positional, flags) {
|
|
17
31
|
const values = positional.slice();
|
|
18
32
|
if (flags.target) values.push(...String(flags.target).split(','));
|
|
@@ -87,19 +101,27 @@ function status(projectDir, positional = [], flags = {}) {
|
|
|
87
101
|
|
|
88
102
|
async function test(projectDir, _positional = [], flags = {}) {
|
|
89
103
|
let config;
|
|
104
|
+
const stages = [];
|
|
90
105
|
try {
|
|
91
106
|
config = loadProjectConfig(projectDir);
|
|
92
107
|
const result = await testConnection(config, {
|
|
93
108
|
timeoutMs: parseTimeout(flags.timeout),
|
|
109
|
+
onStage(stage, status) {
|
|
110
|
+
stages.push({ stage, status });
|
|
111
|
+
if (status === 'succeeded') log.ok(`MCP ${stage} succeeded.`);
|
|
112
|
+
else if (status === 'failed') log.err(`MCP ${stage} failed.`);
|
|
113
|
+
else log.step(`MCP ${stage}...`);
|
|
114
|
+
},
|
|
94
115
|
});
|
|
95
|
-
log.ok(
|
|
96
|
-
log.ok(`tools/list returned ${result.tools.length} tools.`);
|
|
97
|
-
log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
|
|
116
|
+
log.ok(`MCP diagnostic completed: ${result.testedCalls.length} tools/call checks passed.`);
|
|
98
117
|
log.dim(` protocol: ${result.protocolVersion}`);
|
|
99
118
|
return 0;
|
|
100
119
|
} catch (error) {
|
|
101
120
|
const token = config && String(config.token || config.sat || '');
|
|
102
121
|
log.err(redactText(error && error.message ? error.message : error, [token]));
|
|
122
|
+
const last = stages[stages.length - 1];
|
|
123
|
+
if (last) log.info(`MCP diagnostic stopped at ${last.stage} (${last.status}).`);
|
|
124
|
+
log.info(classifyMcpFailure(error));
|
|
103
125
|
return 1;
|
|
104
126
|
}
|
|
105
127
|
}
|
|
@@ -125,3 +147,4 @@ mcp.serve = serve;
|
|
|
125
147
|
mcp.printUsage = printUsage;
|
|
126
148
|
|
|
127
149
|
module.exports = mcp;
|
|
150
|
+
module.exports.classifyMcpFailure = classifyMcpFailure;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { reconcileResources } = require('../worktree');
|
|
5
|
+
|
|
6
|
+
async function reconcile(projectDir, positional, flags = {}) {
|
|
7
|
+
const [resourceType, ...ids] = positional;
|
|
8
|
+
if (!resourceType || !ids.length) {
|
|
9
|
+
log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
const results = await reconcileResources(projectDir, resourceType, ids);
|
|
13
|
+
if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
|
|
14
|
+
else for (const result of results) {
|
|
15
|
+
log.ok(`Reconciled ${result.resource_type} ${result.resource_id} -> ${result.base_version ?? result.base_revision}`);
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = reconcile;
|
package/src/commands/verifyUi.js
CHANGED
|
@@ -5,6 +5,10 @@ const path = require('path');
|
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
6
|
const log = require('../logger');
|
|
7
7
|
const { configPath, loadProjectConfig } = require('../projectConfig');
|
|
8
|
+
const { loadManifest, getEntry } = require('../worktree/manifest');
|
|
9
|
+
const { canonicalResourceType } = require('../worktree/types');
|
|
10
|
+
const { resolveMetadata } = require('../worktree/backend');
|
|
11
|
+
const { inspectEntry, openMetadataSession } = require('../worktree/status');
|
|
8
12
|
|
|
9
13
|
const UI_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte']);
|
|
10
14
|
|
|
@@ -77,6 +81,7 @@ function redactUrlTokens(message) {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
function executableCandidates() {
|
|
84
|
+
const explicit = process.env.DRAFTGO_BROWSER_PATH ? [process.env.DRAFTGO_BROWSER_PATH] : [];
|
|
80
85
|
if (process.platform === 'win32') {
|
|
81
86
|
const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
|
|
82
87
|
const rels = [
|
|
@@ -84,23 +89,47 @@ function executableCandidates() {
|
|
|
84
89
|
['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
85
90
|
['Chromium', 'Application', 'chrome.exe'],
|
|
86
91
|
];
|
|
87
|
-
return roots.flatMap((root) => rels.map((parts) => path.join(root, ...parts)));
|
|
92
|
+
return [...explicit, ...roots.flatMap((root) => rels.map((parts) => path.join(root, ...parts)))];
|
|
88
93
|
}
|
|
89
94
|
if (process.platform === 'darwin') {
|
|
90
|
-
return [
|
|
95
|
+
return [...explicit,
|
|
91
96
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
92
97
|
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
93
98
|
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
94
99
|
];
|
|
95
100
|
}
|
|
96
|
-
return ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/microsoft-edge', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
|
101
|
+
return [...explicit, '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/microsoft-edge', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
function playwrightCacheCandidates() {
|
|
105
|
+
const root = process.env.PLAYWRIGHT_BROWSERS_PATH
|
|
106
|
+
|| (process.platform === 'win32'
|
|
107
|
+
? path.join(process.env.LOCALAPPDATA || '', 'ms-playwright')
|
|
108
|
+
: path.join(process.env.HOME || '', '.cache', 'ms-playwright'));
|
|
109
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
110
|
+
const candidates = [];
|
|
111
|
+
const visit = (directory, depth) => {
|
|
112
|
+
if (depth > 3) return;
|
|
113
|
+
let entries;
|
|
114
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return; }
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const absolute = path.join(directory, entry.name);
|
|
117
|
+
if (entry.isFile() && /^(?:chrome(?:-headless-shell)?|chromium|msedge|headless_shell)(?:\.exe)?$/i.test(entry.name)) candidates.push(absolute);
|
|
118
|
+
else if (entry.isDirectory()) visit(absolute, depth + 1);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
visit(root, 0);
|
|
122
|
+
return candidates;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function launchBrowser(chromium, requested, requestedPath) {
|
|
100
126
|
const attempts = [];
|
|
127
|
+
if (requestedPath) attempts.push({ executablePath: requestedPath, label: `path:${requestedPath}` });
|
|
101
128
|
if (requested && requested !== 'chromium') attempts.push({ channel: requested });
|
|
102
|
-
for (const executablePath of executableCandidates().filter((candidate) => fs.existsSync(candidate))) {
|
|
103
|
-
attempts.
|
|
129
|
+
for (const executablePath of [...executableCandidates(), ...playwrightCacheCandidates()].filter((candidate) => fs.existsSync(candidate))) {
|
|
130
|
+
if (!attempts.some((attempt) => attempt.executablePath === executablePath)) {
|
|
131
|
+
attempts.push({ executablePath, label: `path:${executablePath}` });
|
|
132
|
+
}
|
|
104
133
|
}
|
|
105
134
|
if (!requested || requested === 'chromium') attempts.push({});
|
|
106
135
|
for (const channel of ['msedge', 'chrome']) {
|
|
@@ -108,14 +137,42 @@ async function launchBrowser(chromium, requested) {
|
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
let lastError = null;
|
|
140
|
+
const attempted = [];
|
|
111
141
|
for (const options of attempts) {
|
|
112
142
|
try {
|
|
113
|
-
|
|
143
|
+
const { label, ...launchOptions } = options;
|
|
144
|
+
const browser = await chromium.launch({ headless: true, ...launchOptions });
|
|
145
|
+
return { browser, selected: label || options.channel || 'playwright-managed', attempted };
|
|
114
146
|
} catch (err) {
|
|
115
147
|
lastError = err;
|
|
148
|
+
attempted.push(`${options.label || options.channel || 'playwright-managed'}: ${String(err.message || err).split('\n')[0]}`);
|
|
116
149
|
}
|
|
117
150
|
}
|
|
118
|
-
throw new Error(`未找到可用的 Chromium/Chrome/Edge
|
|
151
|
+
throw new Error(`未找到可用的 Chromium/Chrome/Edge。请使用 --browser-path <executable> 或 DRAFTGO_BROWSER_PATH。`
|
|
152
|
+
+ `尝试记录:${attempted.join('; ')}`
|
|
153
|
+
+ (lastError ? `;最后错误:${lastError.message.split('\n')[0]}` : ''));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseResourceSpec(value) {
|
|
157
|
+
const raw = String(value || '');
|
|
158
|
+
const separator = raw.indexOf(':');
|
|
159
|
+
if (separator <= 0 || separator === raw.length - 1) {
|
|
160
|
+
throw new Error('--resource 格式必须是 <pages|nav|docs>:<id>。');
|
|
161
|
+
}
|
|
162
|
+
return { resourceType: canonicalResourceType(raw.slice(0, separator)), resourceId: raw.slice(separator + 1) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function verifyRemoteResource(projectDir, spec) {
|
|
166
|
+
const { resourceType, resourceId } = parseResourceSpec(spec);
|
|
167
|
+
const entry = getEntry(loadManifest(projectDir), resourceType, resourceId);
|
|
168
|
+
if (!entry) throw new Error(`${resourceType} ${resourceId} 未 checkout。`);
|
|
169
|
+
const config = loadProjectConfig(projectDir);
|
|
170
|
+
const session = await openMetadataSession(config);
|
|
171
|
+
const remote = await resolveMetadata(config, resourceType, resourceId, {
|
|
172
|
+
...session,
|
|
173
|
+
clientInitialized: true,
|
|
174
|
+
});
|
|
175
|
+
return inspectEntry(projectDir, entry, remote);
|
|
119
176
|
}
|
|
120
177
|
|
|
121
178
|
async function verifyUi(projectDir, positional, flags = {}) {
|
|
@@ -136,7 +193,9 @@ async function verifyUi(projectDir, positional, flags = {}) {
|
|
|
136
193
|
log.err('--mobile-check 只支持 auto、always、never。');
|
|
137
194
|
return 1;
|
|
138
195
|
}
|
|
139
|
-
const decision =
|
|
196
|
+
const decision = flags.resource
|
|
197
|
+
? { run: true, reason: `resource=${flags.resource}` }
|
|
198
|
+
: decideMobileCheck(projectDir, mode);
|
|
140
199
|
if (!decision.run) {
|
|
141
200
|
log.ok(`跳过 UI smoke check:${decision.reason}`);
|
|
142
201
|
return 0;
|
|
@@ -169,7 +228,26 @@ async function verifyUi(projectDir, positional, flags = {}) {
|
|
|
169
228
|
|
|
170
229
|
let browser;
|
|
171
230
|
try {
|
|
172
|
-
|
|
231
|
+
if (flags.resource) {
|
|
232
|
+
const resource = await verifyRemoteResource(projectDir, flags.resource);
|
|
233
|
+
log.info(`UI source: remote committed ${resource.resource_type} ${resource.resource_id}`);
|
|
234
|
+
log.info(`Remote version: ${resource.remote_version || '-'}; remote hash: ${resource.remote_hash || '-'}`);
|
|
235
|
+
if (['local_modified', 'diverged'].includes(resource.state)) {
|
|
236
|
+
log.warn(`Local worktree is not committed (${resource.state}); verify-ui will test the remote version.`);
|
|
237
|
+
}
|
|
238
|
+
if (!resource.local_matches_remote && resource.state !== 'local_modified') {
|
|
239
|
+
throw new Error(`Local and remote content differ (${resource.state}); UI verification stopped.`);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
log.info('UI source: remote URL response; no local worktree version was asserted.');
|
|
243
|
+
}
|
|
244
|
+
const launch = await launchBrowser(
|
|
245
|
+
chromium,
|
|
246
|
+
flags.browser && String(flags.browser),
|
|
247
|
+
flags['browser-path'] && String(flags['browser-path']),
|
|
248
|
+
);
|
|
249
|
+
browser = launch.browser;
|
|
250
|
+
log.info(`Browser: ${launch.selected}`);
|
|
173
251
|
const page = await browser.newPage({ viewport: { width, height } });
|
|
174
252
|
const consoleErrors = [];
|
|
175
253
|
const pageErrors = [];
|
|
@@ -240,3 +318,7 @@ module.exports.gitChangedFiles = gitChangedFiles;
|
|
|
240
318
|
module.exports.isUiFile = isUiFile;
|
|
241
319
|
module.exports.configuredUiUrl = configuredUiUrl;
|
|
242
320
|
module.exports.redactUrlTokens = redactUrlTokens;
|
|
321
|
+
module.exports.executableCandidates = executableCandidates;
|
|
322
|
+
module.exports.playwrightCacheCandidates = playwrightCacheCandidates;
|
|
323
|
+
module.exports.parseResourceSpec = parseResourceSpec;
|
|
324
|
+
module.exports.verifyRemoteResource = verifyRemoteResource;
|
package/src/context/index.js
CHANGED
|
@@ -18,14 +18,6 @@ const LONG_CONTENT_RESOURCE_TYPES = Object.freeze([
|
|
|
18
18
|
]);
|
|
19
19
|
const DB_META_LIST_PATH = '/api/db-meta';
|
|
20
20
|
const DB_META_PAGE_SIZE = 100;
|
|
21
|
-
const ROOT_SECTIONS = [
|
|
22
|
-
'核心边界',
|
|
23
|
-
'强制预读:Reference 优先于 MCP',
|
|
24
|
-
'并行开发与所有权',
|
|
25
|
-
'标准工作流',
|
|
26
|
-
'安全规则',
|
|
27
|
-
'验证与交付',
|
|
28
|
-
];
|
|
29
21
|
|
|
30
22
|
const TASK_PROFILES = Object.freeze({
|
|
31
23
|
frontend: {
|
|
@@ -35,38 +27,96 @@ const TASK_PROFILES = Object.freeze({
|
|
|
35
27
|
{ resource_type: 'db' },
|
|
36
28
|
],
|
|
37
29
|
references: [
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{
|
|
30
|
+
{
|
|
31
|
+
file: 'references/architecture.md',
|
|
32
|
+
headings: ['核心概念(30 秒)', 'App 对象是什么(1 分钟)', '技术栈(30 秒)'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
file: 'references/modules.md',
|
|
36
|
+
headings: ['可开发模块(开发者负责实现)', '平台内置模块(开箱即用,不需实现)'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
file: 'references/frontend.md',
|
|
40
|
+
headings: [
|
|
41
|
+
'页面开发强制规则',
|
|
42
|
+
'页面实践经验',
|
|
43
|
+
'必须 / 禁止',
|
|
44
|
+
'前端底座与组件库清单',
|
|
45
|
+
'本地静态资源清单',
|
|
46
|
+
'GSAP 动效规范',
|
|
47
|
+
'颜色 Token(强制)',
|
|
48
|
+
'导航栏开发',
|
|
49
|
+
'退出登录',
|
|
50
|
+
'加载体验',
|
|
51
|
+
'选择器与表单体验',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
file: 'references/runtime.md',
|
|
56
|
+
headings: [
|
|
57
|
+
'iframe 注入机制',
|
|
58
|
+
'App 对象来源',
|
|
59
|
+
'URL 参数读取(标准三阶回落)',
|
|
60
|
+
'全局事件',
|
|
61
|
+
'前端全局层',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
file: 'references/app-api.md',
|
|
66
|
+
headings: ['请求', '反馈', '路由', '状态', '主题', '其他'],
|
|
67
|
+
},
|
|
68
|
+
{ file: 'references/mcp.md', headings: ['API 发现与调用'] },
|
|
69
|
+
{
|
|
70
|
+
file: 'references/checkout.md',
|
|
71
|
+
headings: ['适用范围', '命令', 'Commit 流程', '409 / 412 冲突'],
|
|
72
|
+
},
|
|
47
73
|
],
|
|
48
74
|
},
|
|
49
75
|
data: {
|
|
50
76
|
apiQueries: [{ resource_type: 'db' }],
|
|
51
77
|
references: [
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
{
|
|
79
|
+
file: 'references/modules.md',
|
|
80
|
+
headings: ['可开发模块(开发者负责实现)', '模块选型决策'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
file: 'references/data.md',
|
|
84
|
+
headings: [
|
|
85
|
+
'DB Meta 结构',
|
|
86
|
+
'关联关系(ref)',
|
|
87
|
+
'CRUD 操作范式',
|
|
88
|
+
'filters 操作符',
|
|
89
|
+
'db_meta 实时契约',
|
|
90
|
+
'通用筛选参数(非动态 DB)',
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{ file: 'references/mcp.md', headings: ['API 发现与调用', '协议与安全'] },
|
|
59
94
|
],
|
|
60
95
|
},
|
|
61
96
|
'custom-service': {
|
|
62
97
|
apiQueries: [{ resource_type: 'custom_scripts' }],
|
|
63
98
|
references: [
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
99
|
+
{
|
|
100
|
+
file: 'references/modules.md',
|
|
101
|
+
headings: ['可开发模块(开发者负责实现)', '模块选型决策', '自定义服务边界'],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
file: 'references/custom-services.md',
|
|
105
|
+
headings: [
|
|
106
|
+
'最小服务',
|
|
107
|
+
'资源读写',
|
|
108
|
+
'触发器',
|
|
109
|
+
'平台 SDK',
|
|
110
|
+
'权限与运行限制',
|
|
111
|
+
'实时 API',
|
|
112
|
+
'验收清单',
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
file: 'references/data.md',
|
|
117
|
+
headings: ['自定义服务内的 draftgo.DB.Query', 'filters 操作符'],
|
|
118
|
+
},
|
|
119
|
+
{ file: 'references/mcp.md', headings: ['API 发现与调用', '协议与安全'] },
|
|
70
120
|
],
|
|
71
121
|
},
|
|
72
122
|
aihub: {
|
|
@@ -75,11 +125,22 @@ const TASK_PROFILES = Object.freeze({
|
|
|
75
125
|
{ resource_type: 'agents' },
|
|
76
126
|
],
|
|
77
127
|
references: [
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
128
|
+
{
|
|
129
|
+
file: 'references/aihub.md',
|
|
130
|
+
headings: ['条目骨架', '`data.spec` 字段地图', '观测'],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
file: 'references/chat-sdk.md',
|
|
134
|
+
headings: [
|
|
135
|
+
'最小接入',
|
|
136
|
+
'协议',
|
|
137
|
+
'配置与布局',
|
|
138
|
+
'JavaScript API 与事件',
|
|
139
|
+
'历史与会话',
|
|
140
|
+
'鉴权与安全',
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{ file: 'references/mcp.md', headings: ['API 发现与调用', '协议与安全'] },
|
|
83
144
|
],
|
|
84
145
|
},
|
|
85
146
|
content: {
|
|
@@ -89,12 +150,12 @@ const TASK_PROFILES = Object.freeze({
|
|
|
89
150
|
{ resource_type: 'docs/articles' },
|
|
90
151
|
],
|
|
91
152
|
references: [
|
|
92
|
-
{ file: '
|
|
93
|
-
{ file: 'references/
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
153
|
+
{ file: 'references/architecture.md', headings: ['核心概念(30 秒)'] },
|
|
154
|
+
{ file: 'references/modules.md', headings: ['可开发模块(开发者负责实现)'] },
|
|
155
|
+
{
|
|
156
|
+
file: 'references/checkout.md',
|
|
157
|
+
headings: ['适用范围', '命令', 'Checkout 流程', 'Commit 流程', '409 / 412 冲突'],
|
|
158
|
+
},
|
|
98
159
|
],
|
|
99
160
|
},
|
|
100
161
|
project: {
|
|
@@ -105,12 +166,21 @@ const TASK_PROFILES = Object.freeze({
|
|
|
105
166
|
{ resource_type: 'db' },
|
|
106
167
|
],
|
|
107
168
|
references: [
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
169
|
+
{
|
|
170
|
+
file: 'references/architecture.md',
|
|
171
|
+
headings: ['核心概念(30 秒)', '为什么这样设计(1 分钟)', '技术栈(30 秒)'],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
file: 'references/modules.md',
|
|
175
|
+
headings: [
|
|
176
|
+
'可开发模块(开发者负责实现)',
|
|
177
|
+
'平台内置模块(开箱即用,不需实现)',
|
|
178
|
+
'模块选型决策',
|
|
179
|
+
'自定义服务边界',
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
{ file: 'references/mcp.md', headings: ['边界', 'API 发现与调用', '协议与安全'] },
|
|
183
|
+
{ file: 'references/checkout.md', headings: ['适用范围'] },
|
|
114
184
|
],
|
|
115
185
|
},
|
|
116
186
|
});
|