draftgo-cli 4.0.1 → 4.0.23
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 +87 -11
- package/package.json +9 -4
- package/resources/custom-service-sdk/ai.go +520 -0
- package/resources/custom-service-sdk/ai_test.go +156 -0
- package/resources/custom-service-sdk/auth_test.go +56 -0
- package/resources/custom-service-sdk/billing.go +596 -0
- package/resources/custom-service-sdk/billing_test.go +150 -0
- package/resources/custom-service-sdk/go.mod +3 -0
- package/resources/custom-service-sdk/manifest.json +77 -0
- package/resources/custom-service-sdk/platform.go +345 -0
- package/resources/custom-service-sdk/platform_logger_test.go +24 -0
- package/resources/custom-service-sdk/registration_test.go +39 -0
- package/resources/custom-service-sdk/resources.go +246 -0
- package/resources/custom-service-sdk/resources_billing_test.go +115 -0
- package/resources/custom-service-sdk/resources_files_test.go +57 -0
- package/resources/custom-service-sdk/resources_scope_test.go +87 -0
- package/resources/custom-service-sdk/sdk.go +208 -0
- package/resources/skill/SKILL.md +36 -88
- package/resources/skill/init/SKILL.md +4 -4
- package/resources/skill/manifest.json +5 -1
- package/resources/skill/references/aihub.md +25 -2
- package/resources/skill/references/app-api.md +56 -6
- package/resources/skill/references/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +4 -2
- package/resources/skill/references/checkout.md +17 -3
- package/resources/skill/references/custom-services.md +112 -47
- package/resources/skill/references/data.md +19 -4
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/diagnostics.md +51 -0
- package/resources/skill/references/frontend.md +34 -46
- package/resources/skill/references/mcp.md +33 -5
- package/resources/skill/references/methods.md +189 -0
- package/resources/skill/references/modules.md +37 -9
- package/resources/skill/references/runtime.md +23 -1
- package/src/cli.js +24 -0
- package/src/commandRegistry.js +9 -1
- package/src/commands/api.js +21 -10
- package/src/commands/apiKey.js +34 -0
- package/src/commands/capabilities.js +93 -0
- package/src/commands/checkout.js +1 -1
- package/src/commands/commit.js +1 -1
- package/src/commands/components.js +550 -0
- package/src/commands/conflict.js +1 -1
- package/src/commands/connect.js +18 -8
- package/src/commands/customService.js +20 -4
- package/src/commands/dataRange.js +33 -0
- package/src/commands/delete.js +12 -1
- package/src/commands/diff.js +18 -2
- package/src/commands/grant.js +29 -0
- package/src/commands/group.js +38 -0
- package/src/commands/help.js +64 -20
- package/src/commands/init.js +3 -3
- package/src/commands/map.js +145 -17
- package/src/commands/mcp.js +2 -2
- package/src/commands/reconcile.js +1 -1
- package/src/commands/role.js +32 -0
- package/src/commands/space.js +41 -0
- package/src/commands/status.js +110 -7
- package/src/commands/update.js +23 -11
- package/src/commands/verify.js +75 -0
- package/src/commands/worklog.js +6 -2
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +57 -0
- package/src/customServices.js +138 -18
- package/src/diffReport.js +106 -0
- package/src/index.js +2 -0
- package/src/localRuntime/compose.js +14 -17
- package/src/localRuntime/index.js +22 -23
- package/src/localRuntime/services.js +27 -36
- package/src/mcp/client.js +11 -2
- package/src/mcp/protocol.js +22 -2
- package/src/mcp/tools.js +14 -1
- package/src/platforms.js +9 -0
- package/src/projectConfig.js +6 -4
- package/src/releaseInstall.js +105 -0
- package/src/updateCheck.js +48 -28
- package/src/worklog.js +2 -1
- package/src/worktree/backend.js +1 -1
- package/src/worktree/index.js +7 -2
|
@@ -6,7 +6,7 @@ const { reconcileResources } = require('../worktree');
|
|
|
6
6
|
async function reconcile(projectDir, positional, flags = {}) {
|
|
7
7
|
const [resourceType, ...ids] = positional;
|
|
8
8
|
if (!resourceType || !ids.length) {
|
|
9
|
-
log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
|
|
9
|
+
log.err('Usage: draftgo reconcile <pages|nav|docs|custom-services> <id...>');
|
|
10
10
|
return 1;
|
|
11
11
|
}
|
|
12
12
|
if (require('./customService').isServiceType(resourceType)) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { callOperation } = require('./api');
|
|
5
|
+
|
|
6
|
+
// Role is a permission template. Use `draftgo grant` to bind it to a subject.
|
|
7
|
+
const OPERATIONS = Object.freeze({
|
|
8
|
+
permissions: 'listPermissionCatalog',
|
|
9
|
+
list: 'listRoles',
|
|
10
|
+
create: 'createRole',
|
|
11
|
+
get: 'getRole',
|
|
12
|
+
update: 'updateRole',
|
|
13
|
+
delete: 'deleteRole',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function operationKey(value) {
|
|
17
|
+
const action = String(value || 'list').trim().toLowerCase();
|
|
18
|
+
return OPERATIONS[action] ? action : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function roleCommand(projectDir, positional, flags = {}) {
|
|
22
|
+
const action = operationKey(positional[0]);
|
|
23
|
+
if (!action) {
|
|
24
|
+
log.err('Usage: draftgo role permissions|list|create|get|update|delete [--input <json-file>]');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
return callOperation(projectDir, OPERATIONS[action], flags);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = roleCommand;
|
|
31
|
+
module.exports.OPERATIONS = OPERATIONS;
|
|
32
|
+
module.exports.operationKey = operationKey;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { callOperation } = require('./api');
|
|
5
|
+
|
|
6
|
+
const OPERATIONS = Object.freeze({
|
|
7
|
+
list: 'listSpaces',
|
|
8
|
+
create: 'createSpace',
|
|
9
|
+
get: 'getSpace',
|
|
10
|
+
update: 'updateSpace',
|
|
11
|
+
replace: 'replaceSpace',
|
|
12
|
+
disable: 'deleteSpace',
|
|
13
|
+
'members.list': 'listWorkspaceMembers',
|
|
14
|
+
'members.add': 'addWorkspaceMember',
|
|
15
|
+
'members.get': 'getWorkspaceMember',
|
|
16
|
+
'members.update': 'updateWorkspaceMember',
|
|
17
|
+
'members.replace': 'replaceWorkspaceMember',
|
|
18
|
+
'members.remove': 'removeWorkspaceMember',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function operationKey(parts) {
|
|
22
|
+
const values = parts.map((part) => String(part || '').trim().toLowerCase()).filter(Boolean);
|
|
23
|
+
if (values.length === 1) return values[0] === 'delete' ? 'disable' : values[0];
|
|
24
|
+
if (values.length === 2 && ['member', 'members'].includes(values[0])) {
|
|
25
|
+
return `members.${values[1] === 'delete' ? 'remove' : values[1]}`;
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function spaceCommand(projectDir, positional, flags = {}) {
|
|
31
|
+
const key = operationKey(positional);
|
|
32
|
+
if (!OPERATIONS[key]) {
|
|
33
|
+
log.err('Usage: draftgo space list|create|get|update|replace|disable | members list|add|get|update|replace|remove [--input <json-file>]');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
return callOperation(projectDir, OPERATIONS[key], flags);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = spaceCommand;
|
|
40
|
+
module.exports.OPERATIONS = OPERATIONS;
|
|
41
|
+
module.exports.operationKey = operationKey;
|
package/src/commands/status.js
CHANGED
|
@@ -3,21 +3,124 @@
|
|
|
3
3
|
const log = require('../logger');
|
|
4
4
|
const { all } = require('../targets');
|
|
5
5
|
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
7
|
+
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
8
|
+
const { normalizeDescription } = require('../apiContractCache');
|
|
9
|
+
const { parseTimeout } = require('../timeout');
|
|
10
|
+
|
|
11
|
+
const API_KEY_STATUS_OPERATION = 'getCurrentUserAPIKey';
|
|
12
|
+
|
|
13
|
+
function asObject(value) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scopeFrom(value) {
|
|
18
|
+
let candidates = [asObject(value)];
|
|
19
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
20
|
+
const next = [];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
const type = String(candidate.scope_type || candidate.type || '').trim().toLowerCase();
|
|
23
|
+
if (type === 'platform') return { scope_type: 'platform', workspace_id: null, space_id: null };
|
|
24
|
+
if (type === 'space') {
|
|
25
|
+
return {
|
|
26
|
+
scope_type: 'space',
|
|
27
|
+
workspace_id: candidate.workspace_id ?? null,
|
|
28
|
+
space_id: candidate.space_id ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
for (const key of ['context', 'scope', 'principal', 'credential', 'authentication', 'response', 'data', 'value']) {
|
|
32
|
+
const nested = asObject(candidate[key]);
|
|
33
|
+
if (Object.keys(nested).length) next.push(nested);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
candidates = next;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function connectionDiagnostic(projectDir, options = {}) {
|
|
42
|
+
let config;
|
|
43
|
+
try {
|
|
44
|
+
config = loadProjectConfig(projectDir);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { health: 'not_configured', connected: false, error: error.message };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const diagnostic = { health: 'unhealthy', connected: false, server: config.server };
|
|
50
|
+
try {
|
|
51
|
+
const session = await openToolSession(config, [
|
|
52
|
+
TOOL_NAMES.projectOverview, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
|
|
53
|
+
], { timeoutMs: options.timeoutMs });
|
|
54
|
+
const overview = await callStructured(session, TOOL_NAMES.projectOverview, {}, { timeoutMs: options.timeoutMs });
|
|
55
|
+
const project = asObject(asObject(overview).project);
|
|
56
|
+
diagnostic.connected = true;
|
|
57
|
+
diagnostic.server_version = project.version || null;
|
|
58
|
+
diagnostic.protocol_version = session.client.protocolVersion || null;
|
|
59
|
+
diagnostic.registry_revision = asObject(asObject(overview).operation_registry).registry_revision || null;
|
|
60
|
+
|
|
61
|
+
let scope = scopeFrom(overview);
|
|
62
|
+
if (!scope) {
|
|
63
|
+
const described = normalizeDescription(await callStructured(
|
|
64
|
+
session, TOOL_NAMES.apiDescribe, { operation_id: API_KEY_STATUS_OPERATION }, { timeoutMs: options.timeoutMs },
|
|
65
|
+
));
|
|
66
|
+
if (!described) throw new Error('DraftGo API Key status contract is missing revision metadata.');
|
|
67
|
+
const result = await callStructured(session, TOOL_NAMES.apiCall, {
|
|
68
|
+
operation_id: API_KEY_STATUS_OPERATION,
|
|
69
|
+
registry_revision: described.registry_revision,
|
|
70
|
+
}, { timeoutMs: options.timeoutMs });
|
|
71
|
+
scope = scopeFrom(asObject(result).response || result);
|
|
72
|
+
}
|
|
73
|
+
if (!scope) throw new Error('DraftGo server did not report the current API Key scope.');
|
|
74
|
+
Object.assign(diagnostic, scope, { health: 'healthy' });
|
|
75
|
+
return diagnostic;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
diagnostic.error = error && error.message || String(error);
|
|
78
|
+
return diagnostic;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function status(projectDir, _positional = [], flags = {}) {
|
|
83
|
+
const targets = all.map((target) => {
|
|
84
|
+
const value = target.status(projectDir);
|
|
85
|
+
return { name: target.name, display_name: target.displayName, installed: value.installed === true };
|
|
86
|
+
});
|
|
87
|
+
const connection = await connectionDiagnostic(projectDir, { timeoutMs: parseTimeout(flags.timeout) });
|
|
88
|
+
const result = {
|
|
89
|
+
project_dir: projectDir,
|
|
90
|
+
cli_version: getPackageVersion(),
|
|
91
|
+
skill_version: readInstalledVersion(projectDir) || null,
|
|
92
|
+
connection,
|
|
93
|
+
targets,
|
|
94
|
+
};
|
|
95
|
+
if (flags.output === 'json') {
|
|
96
|
+
console.log(JSON.stringify(result, null, 2));
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
6
99
|
|
|
7
|
-
function status(projectDir) {
|
|
8
100
|
log.title('draftgo status');
|
|
9
101
|
log.info(`项目目录:${projectDir}`);
|
|
10
|
-
log.info(`CLI 版本:${
|
|
11
|
-
log.info(`已装 skill 版本:${
|
|
102
|
+
log.info(`CLI 版本:${result.cli_version}`);
|
|
103
|
+
log.info(`已装 skill 版本:${result.skill_version || '(未安装)'}`);
|
|
104
|
+
log.info(`服务端:${connection.server || '(未连接)'}`);
|
|
105
|
+
if (connection.health === 'healthy') {
|
|
106
|
+
log.ok(`连接健康:healthy${connection.server_version ? `(server ${connection.server_version})` : ''}`);
|
|
107
|
+
const ids = connection.scope_type === 'space'
|
|
108
|
+
? `,workspace_id=${connection.workspace_id ?? '-'},space_id=${connection.space_id ?? '-'}` : '';
|
|
109
|
+
log.info(`当前上下文:${connection.scope_type}${ids}`);
|
|
110
|
+
} else {
|
|
111
|
+
log.warn(`连接健康:${connection.health}`);
|
|
112
|
+
if (connection.error) log.dim(` ${connection.error}`);
|
|
113
|
+
}
|
|
12
114
|
|
|
13
115
|
console.log('');
|
|
14
116
|
console.log('AI 工具入口:');
|
|
15
|
-
for (const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
console.log(` ${mark} ${t.name.padEnd(12)} ${t.displayName}`);
|
|
117
|
+
for (const target of targets) {
|
|
118
|
+
const mark = target.installed ? log.c.green('✓') : log.c.gray('·');
|
|
119
|
+
console.log(` ${mark} ${target.name.padEnd(12)} ${target.display_name}`);
|
|
19
120
|
}
|
|
20
121
|
return 0;
|
|
21
122
|
}
|
|
22
123
|
|
|
23
124
|
module.exports = status;
|
|
125
|
+
module.exports.scopeFrom = scopeFrom;
|
|
126
|
+
module.exports.connectionDiagnostic = connectionDiagnostic;
|
package/src/commands/update.js
CHANGED
|
@@ -6,6 +6,7 @@ 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');
|
|
9
|
+
const { readInstallMarker, installRelease } = require('../releaseInstall');
|
|
9
10
|
|
|
10
11
|
const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
|
|
11
12
|
|
|
@@ -30,19 +31,30 @@ async function updateCliIfNeeded(projectDir, positional, flags) {
|
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const marker = readInstallMarker();
|
|
35
|
+
let command = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
|
|
34
36
|
log.step(`更新 draftgo-cli:${current} → ${latest}`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
if (marker) {
|
|
38
|
+
try {
|
|
39
|
+
const result = await installRelease(latest, null, marker);
|
|
40
|
+
command = result.launcher;
|
|
41
|
+
log.ok(`CLI Release updated to ${latest}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
log.err(`CLI Release update failed: ${error.message}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
48
|
+
const install = spawnSync(npm, ['install', '-g', `draftgo-cli@${latest}`], { stdio: 'inherit', shell: false });
|
|
49
|
+
if (install.error) {
|
|
50
|
+
log.err(`CLI 更新启动失败:${install.error.message}`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (install.status !== 0) {
|
|
54
|
+
log.err(`npm install 返回码 ${install.status}`);
|
|
55
|
+
return Number.isInteger(install.status) ? install.status : 1;
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
const command = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
|
|
46
58
|
const rerun = spawnSync(command, ['update', ...positional, '--project', projectDir, '--skip-update-check'], {
|
|
47
59
|
stdio: 'inherit',
|
|
48
60
|
shell: false,
|
package/src/commands/verify.js
CHANGED
|
@@ -4,6 +4,11 @@ const log = require('../logger');
|
|
|
4
4
|
const check = require('./check');
|
|
5
5
|
const visualVerify = require('./visualVerify');
|
|
6
6
|
const { canonicalResourceType } = require('../worktree/types');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const parse5 = require('parse5');
|
|
10
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
11
|
+
const { loadManifest, getEntry, absolutePath } = require('../worktree/manifest');
|
|
7
12
|
|
|
8
13
|
function mode(value, fallback, flag) {
|
|
9
14
|
const normalized = String(value == null ? fallback : value).toLowerCase();
|
|
@@ -53,6 +58,73 @@ function visualRequest(flags, url) {
|
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
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
|
+
|
|
56
128
|
async function verify(projectDir, positional = [], flags = {}) {
|
|
57
129
|
let selected;
|
|
58
130
|
let remoteMode;
|
|
@@ -65,6 +137,8 @@ async function verify(projectDir, positional = [], flags = {}) {
|
|
|
65
137
|
}
|
|
66
138
|
|
|
67
139
|
log.title('draftgo verify');
|
|
140
|
+
try { await verifyPageComponents(projectDir, selected); }
|
|
141
|
+
catch (error) { log.err(error.message); return 1; }
|
|
68
142
|
const checkCode = await check(projectDir, [], {
|
|
69
143
|
strict: flags.strict,
|
|
70
144
|
remote: remoteMode === 'always',
|
|
@@ -110,3 +184,4 @@ module.exports.mode = mode;
|
|
|
110
184
|
module.exports.resources = resources;
|
|
111
185
|
module.exports.viewports = viewports;
|
|
112
186
|
module.exports.visualRequest = visualRequest;
|
|
187
|
+
module.exports.verifyPageComponents = verifyPageComponents;
|
package/src/commands/worklog.js
CHANGED
|
@@ -38,8 +38,12 @@ function add(projectDir, positional, flags, status) {
|
|
|
38
38
|
const next = appendItem(blocks, title, status, flags.note ? [String(flags.note).trim()] : [], date);
|
|
39
39
|
return { blocks: next, reference: latestReference(next) };
|
|
40
40
|
});
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const item = resultFor(result.blocks, result.reference);
|
|
42
|
+
output(flags, { path: result.path, item });
|
|
43
|
+
if (flags.output !== 'json') {
|
|
44
|
+
const verb = status === 'active' ? 'Started' : 'Added';
|
|
45
|
+
log.ok(`${verb} work item #${item.number} (${item.date}#${item.number}): ${item.title}`);
|
|
46
|
+
}
|
|
43
47
|
return 0;
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Windows console hosts still use the active console code page to decode the
|
|
7
|
+
* UTF-8 bytes Node writes. Switch only an interactive console to UTF-8.
|
|
8
|
+
*
|
|
9
|
+
* This deliberately does not run for redirected stdout: JSON consumers and
|
|
10
|
+
* MCP stdio use pipes, where Node's UTF-8 byte stream is already correct and
|
|
11
|
+
* changing a shared console would be both unnecessary and surprising.
|
|
12
|
+
*/
|
|
13
|
+
function configureConsoleUtf8(options = {}) {
|
|
14
|
+
const platform = options.platform || process.platform;
|
|
15
|
+
const stdout = options.stdout || process.stdout;
|
|
16
|
+
const run = options.spawnSync || spawnSync;
|
|
17
|
+
|
|
18
|
+
if (platform !== 'win32' || !stdout || !stdout.isTTY) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// chcp changes the code page for the inherited Windows console. Redirect
|
|
22
|
+
// its own localized status line so stdout remains exclusively CLI output.
|
|
23
|
+
run('cmd.exe', ['/d', '/s', '/c', 'chcp 65001 >nul'], {
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
// Encoding setup is a presentation enhancement; never block the command.
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { configureConsoleUtf8 };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HARD_INCOMPATIBLE_CODES = new Set([
|
|
4
|
+
'CONTRACT_INCOMPATIBLE',
|
|
5
|
+
'CLIENT_VERSION_UNSUPPORTED',
|
|
6
|
+
'CLI_VERSION_UNSUPPORTED',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function errorCode(error) {
|
|
10
|
+
return String(error && error.code || '').trim().toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isContractCompatibilityError(error) {
|
|
14
|
+
const code = errorCode(error);
|
|
15
|
+
return code === 'CONTRACT_CHANGED' || HARD_INCOMPATIBLE_CODES.has(code);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isHardContractIncompatibility(error) {
|
|
19
|
+
return HARD_INCOMPATIBLE_CODES.has(errorCode(error));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDangerousOperation(contract) {
|
|
23
|
+
const operation = contract && contract.operation || contract || {};
|
|
24
|
+
return operation.destructive === true || String(operation.risk || '').toLowerCase() === 'high';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canonicalValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
29
|
+
if (!value || typeof value !== 'object') return value;
|
|
30
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameOperationContract(left, right) {
|
|
34
|
+
const leftOperation = left && left.operation || left || {};
|
|
35
|
+
const rightOperation = right && right.operation || right || {};
|
|
36
|
+
return JSON.stringify(canonicalValue(leftOperation)) === JSON.stringify(canonicalValue(rightOperation));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function incompatibleOperationError(operationId, error) {
|
|
40
|
+
const wrapped = new Error(
|
|
41
|
+
`DraftGo CLI and server contracts are incompatible for dangerous operation ${operationId}; `
|
|
42
|
+
+ 'the call was blocked before retry. Upgrade DraftGo CLI, then describe and confirm the operation again.',
|
|
43
|
+
);
|
|
44
|
+
wrapped.code = 'CLI_CONTRACT_INCOMPATIBLE';
|
|
45
|
+
wrapped.details = error && (error.details || error.data);
|
|
46
|
+
wrapped.cause = error;
|
|
47
|
+
return wrapped;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
errorCode,
|
|
52
|
+
isContractCompatibilityError,
|
|
53
|
+
isHardContractIncompatibility,
|
|
54
|
+
isDangerousOperation,
|
|
55
|
+
sameOperationContract,
|
|
56
|
+
incompatibleOperationError,
|
|
57
|
+
};
|