draftgo-cli 1.0.4
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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/draftgo.js +9 -0
- package/package.json +70 -0
- package/resources/project-design/README.md +42 -0
- package/resources/skill/SKILL.md +62 -0
- package/resources/skill/init/SKILL.md +41 -0
- package/resources/skill/manifest.json +35 -0
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +97 -0
- package/resources/skill/references/architecture.md +13 -0
- package/resources/skill/references/chat-sdk.md +205 -0
- package/resources/skill/references/checkout.md +140 -0
- package/resources/skill/references/data.md +49 -0
- package/resources/skill/references/db-relations.md +29 -0
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/development.md +41 -0
- package/resources/skill/references/diagnostics.md +50 -0
- package/resources/skill/references/frontend.md +158 -0
- package/resources/skill/references/mcp.md +110 -0
- package/resources/skill/references/methods.md +143 -0
- package/resources/skill/references/modules.md +75 -0
- package/resources/skill/references/runtime.md +109 -0
- package/resources/skill/references/services.md +32 -0
- package/src/apiContractCache.js +120 -0
- package/src/cli.js +100 -0
- package/src/commandRegistry.js +46 -0
- package/src/commands/api.js +244 -0
- package/src/commands/apiKey.js +30 -0
- package/src/commands/autoPush.js +36 -0
- package/src/commands/capabilities.js +100 -0
- package/src/commands/check.js +82 -0
- package/src/commands/checkout.js +18 -0
- package/src/commands/clean.js +72 -0
- package/src/commands/commit.js +47 -0
- package/src/commands/components.js +554 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +91 -0
- package/src/commands/delete.js +95 -0
- package/src/commands/deploy.js +77 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/group.js +37 -0
- package/src/commands/help.js +190 -0
- package/src/commands/init.js +126 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/local.js +79 -0
- package/src/commands/map.js +395 -0
- package/src/commands/mcp.js +150 -0
- package/src/commands/reconcile.js +20 -0
- package/src/commands/role.js +31 -0
- package/src/commands/status.js +98 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +79 -0
- package/src/commands/verify.js +188 -0
- package/src/commands/visualVerify.js +281 -0
- package/src/commands/worklog.js +117 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +65 -0
- package/src/detect.js +25 -0
- package/src/diffReport.js +106 -0
- package/src/fsx.js +67 -0
- package/src/index.js +46 -0
- package/src/localRuntime/compose.js +119 -0
- package/src/localRuntime/detect.js +77 -0
- package/src/localRuntime/index.js +211 -0
- package/src/localRuntime/mysqlClient.js +155 -0
- package/src/localRuntime/services.js +117 -0
- package/src/logger.js +37 -0
- package/src/mcp/client.js +558 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/parallel.js +54 -0
- package/src/mcp/protocol.js +223 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +51 -0
- package/src/paths.js +32 -0
- package/src/platforms.js +110 -0
- package/src/projectConfig.js +139 -0
- package/src/projectDesign.js +19 -0
- package/src/projectHealth.js +33 -0
- package/src/projectMap.js +220 -0
- package/src/prompt.js +94 -0
- package/src/releaseInstall.js +105 -0
- package/src/runtimeFiles.js +45 -0
- package/src/skill.js +295 -0
- package/src/targets.js +43 -0
- package/src/timeout.js +18 -0
- package/src/updateCheck.js +100 -0
- package/src/worklog.js +276 -0
- package/src/worktree/backend.js +438 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +751 -0
- package/src/worktree/inlineScripts.js +99 -0
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +89 -0
- package/src/worktree/status.js +124 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all } = require('../targets');
|
|
5
|
+
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
7
|
+
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
8
|
+
const { parseTimeout } = require('../timeout');
|
|
9
|
+
|
|
10
|
+
function asObject(value) {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findField(value, keys) {
|
|
15
|
+
let candidates = [asObject(value)];
|
|
16
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
17
|
+
const next = [];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
for (const key of keys) if (candidate[key] != null) return candidate[key];
|
|
20
|
+
for (const key of ['operation_registry', 'registry', 'response', 'data', 'value']) {
|
|
21
|
+
const nested = asObject(candidate[key]);
|
|
22
|
+
if (Object.keys(nested).length) next.push(nested);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
candidates = next;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function connectionDiagnostic(projectDir, options = {}) {
|
|
31
|
+
let config;
|
|
32
|
+
try {
|
|
33
|
+
config = loadProjectConfig(projectDir);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { health: 'not_configured', connected: false, error: error.message };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const diagnostic = { health: 'unhealthy', connected: false, server: config.server };
|
|
39
|
+
try {
|
|
40
|
+
const session = await openToolSession(config, [TOOL_NAMES.apiSearch], { timeoutMs: options.timeoutMs });
|
|
41
|
+
const catalog = await callStructured(session, TOOL_NAMES.apiSearch, {
|
|
42
|
+
query: 'system', limit: 1,
|
|
43
|
+
}, { timeoutMs: options.timeoutMs });
|
|
44
|
+
diagnostic.connected = true;
|
|
45
|
+
diagnostic.server_version = asObject(session.initialized.serverInfo).version || null;
|
|
46
|
+
diagnostic.protocol_version = session.client.protocolVersion || null;
|
|
47
|
+
diagnostic.registry_revision = findField(catalog, ['registry_revision']) || null;
|
|
48
|
+
diagnostic.health = 'healthy';
|
|
49
|
+
return diagnostic;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
diagnostic.error = error && error.message || String(error);
|
|
52
|
+
return diagnostic;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function status(projectDir, _positional = [], flags = {}) {
|
|
57
|
+
const targets = all.map((target) => {
|
|
58
|
+
const value = target.status(projectDir);
|
|
59
|
+
return { name: target.name, display_name: target.displayName, installed: value.installed === true };
|
|
60
|
+
});
|
|
61
|
+
const connection = await connectionDiagnostic(projectDir, { timeoutMs: parseTimeout(flags.timeout) });
|
|
62
|
+
const result = {
|
|
63
|
+
project_dir: projectDir,
|
|
64
|
+
cli_version: getPackageVersion(),
|
|
65
|
+
skill_version: readInstalledVersion(projectDir) || null,
|
|
66
|
+
connection,
|
|
67
|
+
targets,
|
|
68
|
+
};
|
|
69
|
+
if (flags.output === 'json') {
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log.title('draftgo status');
|
|
75
|
+
log.info(`项目目录:${projectDir}`);
|
|
76
|
+
log.info(`CLI 版本:${result.cli_version}`);
|
|
77
|
+
log.info(`已装 skill 版本:${result.skill_version || '(未安装)'}`);
|
|
78
|
+
log.info(`服务端:${connection.server || '(未连接)'}`);
|
|
79
|
+
if (connection.health === 'healthy') {
|
|
80
|
+
log.ok(`连接健康:healthy${connection.server_version ? `(server ${connection.server_version})` : ''}`);
|
|
81
|
+
log.info(`Registry revision:${connection.registry_revision || '-'}`);
|
|
82
|
+
} else {
|
|
83
|
+
log.warn(`连接健康:${connection.health}`);
|
|
84
|
+
if (connection.error) log.dim(` ${connection.error}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log('AI 工具入口:');
|
|
89
|
+
for (const target of targets) {
|
|
90
|
+
const mark = target.installed ? log.c.green('✓') : log.c.gray('·');
|
|
91
|
+
console.log(` ${mark} ${target.name.padEnd(12)} ${target.display_name}`);
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = status;
|
|
97
|
+
module.exports.findField = findField;
|
|
98
|
+
module.exports.connectionDiagnostic = connectionDiagnostic;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all, resolveTargets } = require('../targets');
|
|
5
|
+
const { removePath } = require('../fsx');
|
|
6
|
+
const { dgDir } = require('../paths');
|
|
7
|
+
|
|
8
|
+
async function uninstall(projectDir, positional, flags) {
|
|
9
|
+
log.title('draftgo uninstall');
|
|
10
|
+
|
|
11
|
+
let resolved = [];
|
|
12
|
+
let unknown = [];
|
|
13
|
+
if (positional.length === 0) {
|
|
14
|
+
log.err('Please specify a target, or use `draftgo uninstall all` to remove every target.');
|
|
15
|
+
log.dim(` Supported targets: ${all.map((i) => i.name).join(', ')}`);
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
if (positional.includes('all')) {
|
|
19
|
+
resolved = all.slice();
|
|
20
|
+
} else {
|
|
21
|
+
({ resolved, unknown } = resolveTargets(positional));
|
|
22
|
+
if (unknown.length) {
|
|
23
|
+
log.err(`未知 target:${unknown.join(', ')}`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
log.step('移除入口文件');
|
|
29
|
+
for (const t of resolved) {
|
|
30
|
+
try {
|
|
31
|
+
const removed = t.uninstall(projectDir);
|
|
32
|
+
if (removed) log.ok(`${t.displayName} 入口已移除`);
|
|
33
|
+
else log.dim(`${t.displayName} 入口不存在,跳过`);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
log.err(`${t.displayName} 卸载失败:${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Each AI-tool skill directory is self-contained, so per-target uninstall
|
|
40
|
+
// already wiped its content above. Only --purge nukes runtime data under
|
|
41
|
+
// .draftgo/ (config/worklog/lessons/cache etc.).
|
|
42
|
+
if (flags.purge) {
|
|
43
|
+
if (removePath(dgDir(projectDir))) {
|
|
44
|
+
log.warn('已使用 --purge:.draftgo/(含 config/worklog/lessons 等运行时数据)已删除。');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log.title('完成');
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = uninstall;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all, resolveTargets } = require('../targets');
|
|
5
|
+
const { ensureRuntime, writeInstalledVersion, readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
|
|
7
|
+
function installedTargetsNotRefreshed(projectDir, refreshedTargets) {
|
|
8
|
+
const refreshed = new Set(refreshedTargets.map((target) => target.name));
|
|
9
|
+
return all.filter((target) =>
|
|
10
|
+
!refreshed.has(target.name) && target.status(projectDir).installed);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function targetInstalled(projectDir, target) {
|
|
14
|
+
return typeof target.status === 'function' && target.status(projectDir).installed === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function installedTargets(projectDir) {
|
|
18
|
+
return all.filter((target) => targetInstalled(projectDir, target));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function update(projectDir, positional) {
|
|
22
|
+
log.title('draftgo update');
|
|
23
|
+
|
|
24
|
+
const prev = readInstalledVersion(projectDir);
|
|
25
|
+
log.info(`当前已装 skill:${prev || '未安装'},CLI 版本:${getPackageVersion()}`);
|
|
26
|
+
|
|
27
|
+
// Implicit update and `update all` only refresh existing Skills. An explicit
|
|
28
|
+
// target is a deliberate request and may install that target when it is missing.
|
|
29
|
+
let resolved = [];
|
|
30
|
+
let unknown = [];
|
|
31
|
+
|
|
32
|
+
if (positional.length === 0 || positional.includes('all')) {
|
|
33
|
+
resolved = installedTargets(projectDir);
|
|
34
|
+
if (resolved.length === 0) {
|
|
35
|
+
log.warn('未检测到已安装的 DraftGo Skill,无可刷新内容。');
|
|
36
|
+
log.dim(' 需要新增 AI 工具时,请使用 `draftgo init <target>`。');
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
log.info(`刷新目标:${resolved.map((r) => r.displayName).join(', ')}`);
|
|
40
|
+
} else {
|
|
41
|
+
({ resolved, unknown } = resolveTargets(positional));
|
|
42
|
+
if (unknown.length) {
|
|
43
|
+
log.err(`未知 target:${unknown.join(', ')}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ensureRuntime(projectDir);
|
|
49
|
+
log.step('用 CLI 内置版本覆盖各 AI 工具 skill 目录');
|
|
50
|
+
const failures = [];
|
|
51
|
+
for (const t of resolved) {
|
|
52
|
+
try {
|
|
53
|
+
const r = t.install(projectDir, { force: true });
|
|
54
|
+
log.ok(`${t.displayName.padEnd(16)} → ${r.path}`);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
log.err(`${t.displayName} 更新失败:${e.message}`);
|
|
57
|
+
failures.push(t.displayName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (failures.length) {
|
|
61
|
+
log.err(`更新未完成,失败目标:${failures.join(', ')}`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const staleInstalled = installedTargetsNotRefreshed(projectDir, resolved);
|
|
65
|
+
if (staleInstalled.length) {
|
|
66
|
+
log.dim(
|
|
67
|
+
` 未更新全局 skill 版本标记;尚有未刷新的已安装目标:${staleInstalled
|
|
68
|
+
.map((target) => target.displayName).join(', ')}`,
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
writeInstalledVersion(projectDir);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log.title('完成');
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = update;
|
|
79
|
+
module.exports.installedTargetsNotRefreshed = installedTargetsNotRefreshed;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const check = require('./check');
|
|
5
|
+
const visualVerify = require('./visualVerify');
|
|
6
|
+
const { canonicalResourceType } = require('../worktree/types');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const parse5 = require('parse5');
|
|
10
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
11
|
+
const { loadManifest, getEntry, absolutePath } = require('../worktree/manifest');
|
|
12
|
+
|
|
13
|
+
function mode(value, fallback, flag) {
|
|
14
|
+
const normalized = String(value == null ? fallback : value).toLowerCase();
|
|
15
|
+
if (!['always', 'never'].includes(normalized)) throw new Error(`${flag} must be always or never.`);
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resources(positional) {
|
|
20
|
+
if (!positional.length) return [];
|
|
21
|
+
const [rawType, ...ids] = positional;
|
|
22
|
+
if (!ids.length) throw new Error('Usage: draftgo verify [<pages|nav|docs> <id...>] [--url <url>]');
|
|
23
|
+
const resourceType = canonicalResourceType(rawType);
|
|
24
|
+
return ids.map((resourceId) => ({ resourceType, resourceId: String(resourceId) }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function viewports(flags) {
|
|
28
|
+
const value = String(flags.viewport || '').toLowerCase();
|
|
29
|
+
if (!value) return [{
|
|
30
|
+
name: flags.width || flags.height ? 'custom' : 'desktop',
|
|
31
|
+
width: flags.width || 1440,
|
|
32
|
+
height: flags.height || 900,
|
|
33
|
+
}];
|
|
34
|
+
if (value === 'mobile') return [{ name: 'mobile', width: 390, height: 844 }];
|
|
35
|
+
if (value === 'desktop') return [{ name: 'desktop', width: 1440, height: 900 }];
|
|
36
|
+
if (value === 'both') return [
|
|
37
|
+
{ name: 'mobile', width: 390, height: 844 },
|
|
38
|
+
{ name: 'desktop', width: 1440, height: 900 },
|
|
39
|
+
];
|
|
40
|
+
throw new Error('--viewport must be mobile, desktop, or both.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function visualRequest(flags, url) {
|
|
44
|
+
const uiMode = mode(flags.ui, 'never', '--ui');
|
|
45
|
+
const screenshotMode = String(flags.screenshot || 'never').toLowerCase();
|
|
46
|
+
if (!['always', 'never'].includes(screenshotMode)) {
|
|
47
|
+
throw new Error('--screenshot must be always or never.');
|
|
48
|
+
}
|
|
49
|
+
const screenshotRequested = screenshotMode !== 'never';
|
|
50
|
+
if ((uiMode === 'always' || screenshotRequested) && !url) {
|
|
51
|
+
throw new Error('Visual verification requires --url <http://localhost:port/path>.');
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
uiMode,
|
|
55
|
+
screenshotMode,
|
|
56
|
+
captureOnly: uiMode === 'never' && screenshotMode === 'always',
|
|
57
|
+
run: Boolean(url) && (uiMode === 'always' || screenshotRequested),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkNodes(node, visit) {
|
|
62
|
+
if (!node) return;
|
|
63
|
+
visit(node);
|
|
64
|
+
for (const child of node.childNodes || []) walkNodes(child, visit);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function verifyPageComponents(projectDir, selected) {
|
|
68
|
+
const pages = selected.filter(item => item.resourceType === 'pages');
|
|
69
|
+
if (!pages.length) return [];
|
|
70
|
+
const config = loadProjectConfig(projectDir);
|
|
71
|
+
const manifest = loadManifest(projectDir);
|
|
72
|
+
const uses = [];
|
|
73
|
+
for (const page of pages) {
|
|
74
|
+
const entry = getEntry(manifest, 'pages', page.resourceId);
|
|
75
|
+
if (!entry) continue;
|
|
76
|
+
const file = absolutePath(projectDir, entry.local_path);
|
|
77
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
78
|
+
const document = parse5.parse(source);
|
|
79
|
+
const instances = new Set();
|
|
80
|
+
walkNodes(document, node => {
|
|
81
|
+
const attrs = new Map((node.attrs || []).map(attr => [attr.name, attr.value]));
|
|
82
|
+
const name = attrs.get('data-dg-use');
|
|
83
|
+
if (!name) return;
|
|
84
|
+
if (!/^([a-z][a-z0-9-]*)\/([a-z][a-z0-9-]*)$/.test(name)) throw new Error(`pages ${page.resourceId}: invalid data-dg-use "${name}"`);
|
|
85
|
+
const instance = attrs.get('data-dg-instance');
|
|
86
|
+
if (instance) { if (instances.has(instance)) throw new Error(`pages ${page.resourceId}: duplicate data-dg-instance "${instance}"`); instances.add(instance); }
|
|
87
|
+
uses.push({ page: page.resourceId, node, attrs, name });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const cache = new Map();
|
|
91
|
+
for (const use of uses) {
|
|
92
|
+
if (!cache.has(use.name)) {
|
|
93
|
+
const slug = use.name.split('/')[1];
|
|
94
|
+
const response = await fetch(`${config.server}/api/components?library=${encodeURIComponent(use.name.split('/')[0])}&q=${encodeURIComponent(slug)}&page_size=100`, { headers: { Authorization: `Bearer ${config.token}` } });
|
|
95
|
+
const payload = await response.json().catch(() => ({}));
|
|
96
|
+
if (!response.ok) throw new Error(`component lookup failed for ${use.name} (${response.status})`);
|
|
97
|
+
const items = payload.data?.items || payload.items || [];
|
|
98
|
+
cache.set(use.name, items.find(item => item.slug === slug && (item.library_slug === use.name.split('/')[0] || item.full_name === use.name)) || null);
|
|
99
|
+
}
|
|
100
|
+
const component = cache.get(use.name);
|
|
101
|
+
if (!component) throw new Error(`pages ${use.page}: component not found ${use.name}`);
|
|
102
|
+
if (!component.published || Number(component.published_revision || 0) < 1) throw new Error(`pages ${use.page}: component is not published ${use.name}`);
|
|
103
|
+
const definition = component.published;
|
|
104
|
+
const rootTag = String(definition.root_tag || '').toLowerCase();
|
|
105
|
+
if (rootTag && String(use.node.nodeName || '').toLowerCase() !== rootTag) throw new Error(`pages ${use.page}: ${use.name} requires <${rootTag}> but found <${use.node.nodeName}>`);
|
|
106
|
+
const props = new Map((definition.props || []).map(prop => [prop.name, prop]));
|
|
107
|
+
for (const [name, value] of use.attrs) {
|
|
108
|
+
if (!name.startsWith('data-dg-prop-')) continue;
|
|
109
|
+
const propName = name.slice('data-dg-prop-'.length);
|
|
110
|
+
const prop = props.get(propName);
|
|
111
|
+
if (!prop) throw new Error(`pages ${use.page}: unknown prop ${name} on ${use.name}`);
|
|
112
|
+
if (prop.type === 'boolean' && !['', 'true', 'false', '1', '0'].includes(String(value).toLowerCase())) {
|
|
113
|
+
throw new Error(`pages ${use.page}: invalid boolean prop ${name} on ${use.name}`);
|
|
114
|
+
}
|
|
115
|
+
if (prop.type === 'number' && !Number.isFinite(Number(value))) {
|
|
116
|
+
throw new Error(`pages ${use.page}: invalid number prop ${name} on ${use.name}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const slots = new Set((definition.slots || []).map(slot => slot.name));
|
|
120
|
+
for (const child of use.node.childNodes || []) {
|
|
121
|
+
const slot = (child.attrs || []).find(attr => attr.name === 'data-dg-slot')?.value;
|
|
122
|
+
if (slot && slots.size && !slots.has(slot)) throw new Error(`pages ${use.page}: unknown slot ${slot} on ${use.name}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return uses;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function verify(projectDir, positional = [], flags = {}) {
|
|
129
|
+
let selected;
|
|
130
|
+
let remoteMode;
|
|
131
|
+
let visual;
|
|
132
|
+
const url = String(flags.url || '').trim();
|
|
133
|
+
try {
|
|
134
|
+
selected = resources(positional);
|
|
135
|
+
remoteMode = flags.remote ? 'always' : 'never';
|
|
136
|
+
visual = visualRequest(flags, url);
|
|
137
|
+
if (flags.output === 'json' && visual.run) throw new Error('Visual verification does not support --output json; run the explicit visual check separately.');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
log.err(error.message);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (flags.output !== 'json') log.title('draftgo verify');
|
|
144
|
+
try { if (flags.remote) await verifyPageComponents(projectDir, selected); }
|
|
145
|
+
catch (error) { log.err(error.message); return 1; }
|
|
146
|
+
const checkCode = await check(projectDir, [], {
|
|
147
|
+
output: flags.output,
|
|
148
|
+
strict: flags.strict,
|
|
149
|
+
remote: remoteMode === 'always',
|
|
150
|
+
resourceKeys: selected.length
|
|
151
|
+
? selected.map((entry) => `${entry.resourceType}:${entry.resourceId}`)
|
|
152
|
+
: null,
|
|
153
|
+
});
|
|
154
|
+
if (checkCode !== 0) return checkCode;
|
|
155
|
+
|
|
156
|
+
if (!visual.run) {
|
|
157
|
+
if (url) log.warn('--url was provided without --screenshot always or --ui always; visual verification was skipped.');
|
|
158
|
+
if (flags.output !== 'json') log.ok('Local verification passed; browser and business interactions were not tested.');
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
let targets;
|
|
162
|
+
try { targets = viewports(flags); }
|
|
163
|
+
catch (error) { log.err(error.message); return 1; }
|
|
164
|
+
for (const viewport of targets) {
|
|
165
|
+
const uiFlags = {
|
|
166
|
+
...flags,
|
|
167
|
+
url,
|
|
168
|
+
width: viewport.width,
|
|
169
|
+
height: viewport.height,
|
|
170
|
+
'capture-only': visual.captureOnly,
|
|
171
|
+
};
|
|
172
|
+
const code = await visualVerify(projectDir, [url], uiFlags);
|
|
173
|
+
if (code !== 0) return code;
|
|
174
|
+
}
|
|
175
|
+
if (visual.captureOnly) {
|
|
176
|
+
log.ok(`Visual capture completed (${targets.map((item) => item.name).join(', ')}).`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
log.ok(`Unified verification passed (${targets.map((item) => item.name).join(', ')}).`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = verify;
|
|
184
|
+
module.exports.mode = mode;
|
|
185
|
+
module.exports.resources = resources;
|
|
186
|
+
module.exports.viewports = viewports;
|
|
187
|
+
module.exports.visualRequest = visualRequest;
|
|
188
|
+
module.exports.verifyPageComponents = verifyPageComponents;
|