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.
Files changed (39) hide show
  1. package/README.md +12 -6
  2. package/package.json +1 -1
  3. package/resources/skill/SKILL.md +63 -136
  4. package/resources/skill/manifest.json +1 -3
  5. package/resources/skill/references/aihub.md +3 -4
  6. package/resources/skill/references/app-api.md +6 -51
  7. package/resources/skill/references/architecture.md +3 -28
  8. package/resources/skill/references/checkout.md +2 -1
  9. package/resources/skill/references/custom-services.md +4 -15
  10. package/resources/skill/references/data.md +1 -118
  11. package/resources/skill/references/frontend.md +26 -68
  12. package/resources/skill/references/mcp.md +47 -0
  13. package/resources/skill/references/modules.md +7 -7
  14. package/resources/skill/references/runtime.md +3 -24
  15. package/src/cli.js +2 -0
  16. package/src/commandRegistry.js +1 -0
  17. package/src/commands/api.js +102 -0
  18. package/src/commands/autoPush.js +1 -1
  19. package/src/commands/check.js +24 -2
  20. package/src/commands/commit.js +31 -5
  21. package/src/commands/deploy.js +1 -1
  22. package/src/commands/help.js +11 -3
  23. package/src/commands/init.js +1 -1
  24. package/src/commands/map.js +20 -22
  25. package/src/commands/mcp.js +26 -3
  26. package/src/commands/reconcile.js +20 -0
  27. package/src/commands/verifyUi.js +92 -10
  28. package/src/context/index.js +117 -47
  29. package/src/mcp/client.js +52 -19
  30. package/src/projectMap.js +7 -2
  31. package/src/worktree/index.js +272 -59
  32. package/src/worktree/status.js +122 -0
  33. package/resources/skill/push/SKILL.md +0 -62
  34. package/resources/skill/references/api-endpoints.md +0 -180
  35. package/resources/skill/references/debugging-syntax.md +0 -308
  36. package/resources/skill/references/parallel.md +0 -56
  37. package/resources/skill/references/security.md +0 -74
  38. package/resources/skill/references/ui-protocol.md +0 -99
  39. package/resources/skill/scripts/README.md +0 -10
@@ -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 results = await commitResources(projectDir, resourceType, ids);
13
- if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
14
- else for (const result of results) {
15
- if (result.status === 'unchanged') log.dim(`${result.resource_type} ${result.resource_id}: unchanged`);
16
- else log.ok(`Committed ${result.resource_type} ${result.resource_id}`);
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
  }
@@ -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;
@@ -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 standard HTML + Tailwind CSS.
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 the parameter object from JSON.
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
 
@@ -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 / scripts)');
44
+ log.step('写入各 AI 工具 skill 目录(含 SKILL.md / 子技能 / references)');
45
45
  ensureRuntime(projectDir);
46
46
  const failures = [];
47
47
  let installedCount = 0;
@@ -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
- () => localCheckouts(projectDir),
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} (${entry.local_path})`);
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(', ')}`);
@@ -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('DraftGo MCP initialize succeeded.');
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;
@@ -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
- async function launchBrowser(chromium, requested) {
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.push({ executablePath });
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
- return await chromium.launch({ headless: true, ...options });
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。${lastError ? ` ${lastError.message.split('\n')[0]}` : ''}`);
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 = decideMobileCheck(projectDir, mode);
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
- browser = await launchBrowser(chromium, flags.browser && String(flags.browser));
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;
@@ -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
- { file: 'SKILL.md', headings: ROOT_SECTIONS },
39
- { file: 'references/architecture.md' },
40
- { file: 'references/modules.md' },
41
- { file: 'references/frontend.md' },
42
- { file: 'references/runtime.md' },
43
- { file: 'references/app-api.md' },
44
- { file: 'references/security.md' },
45
- { file: 'references/checkout.md' },
46
- { file: 'references/parallel.md' },
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
- { file: 'SKILL.md', headings: ROOT_SECTIONS },
53
- { file: 'references/modules.md' },
54
- { file: 'references/data.md' },
55
- { file: 'references/db-relations.md' },
56
- { file: 'references/security.md' },
57
- { file: 'references/mcp.md', headings: ['边界', '协议与安全'] },
58
- { file: 'references/parallel.md' },
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
- { file: 'SKILL.md', headings: ROOT_SECTIONS },
65
- { file: 'references/modules.md' },
66
- { file: 'references/custom-services.md' },
67
- { file: 'references/data.md' },
68
- { file: 'references/security.md' },
69
- { file: 'references/parallel.md' },
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
- { file: 'SKILL.md', headings: ROOT_SECTIONS },
79
- { file: 'references/aihub.md' },
80
- { file: 'references/chat-sdk.md' },
81
- { file: 'references/security.md' },
82
- { file: 'references/parallel.md' },
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: 'SKILL.md', headings: ROOT_SECTIONS },
93
- { file: 'references/architecture.md' },
94
- { file: 'references/modules.md' },
95
- { file: 'references/checkout.md' },
96
- { file: 'references/security.md' },
97
- { file: 'references/parallel.md' },
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
- { file: 'SKILL.md', headings: ROOT_SECTIONS },
109
- { file: 'references/architecture.md' },
110
- { file: 'references/modules.md' },
111
- { file: 'references/mcp.md' },
112
- { file: 'references/checkout.md' },
113
- { file: 'references/parallel.md' },
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
  });