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.
Files changed (79) hide show
  1. package/README.md +87 -11
  2. package/package.json +9 -4
  3. package/resources/custom-service-sdk/ai.go +520 -0
  4. package/resources/custom-service-sdk/ai_test.go +156 -0
  5. package/resources/custom-service-sdk/auth_test.go +56 -0
  6. package/resources/custom-service-sdk/billing.go +596 -0
  7. package/resources/custom-service-sdk/billing_test.go +150 -0
  8. package/resources/custom-service-sdk/go.mod +3 -0
  9. package/resources/custom-service-sdk/manifest.json +77 -0
  10. package/resources/custom-service-sdk/platform.go +345 -0
  11. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  12. package/resources/custom-service-sdk/registration_test.go +39 -0
  13. package/resources/custom-service-sdk/resources.go +246 -0
  14. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  15. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  16. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  17. package/resources/custom-service-sdk/sdk.go +208 -0
  18. package/resources/skill/SKILL.md +36 -88
  19. package/resources/skill/init/SKILL.md +4 -4
  20. package/resources/skill/manifest.json +5 -1
  21. package/resources/skill/references/aihub.md +25 -2
  22. package/resources/skill/references/app-api.md +56 -6
  23. package/resources/skill/references/architecture.md +2 -2
  24. package/resources/skill/references/chat-sdk.md +4 -2
  25. package/resources/skill/references/checkout.md +17 -3
  26. package/resources/skill/references/custom-services.md +112 -47
  27. package/resources/skill/references/data.md +19 -4
  28. package/resources/skill/references/delivery.md +33 -0
  29. package/resources/skill/references/diagnostics.md +51 -0
  30. package/resources/skill/references/frontend.md +34 -46
  31. package/resources/skill/references/mcp.md +33 -5
  32. package/resources/skill/references/methods.md +189 -0
  33. package/resources/skill/references/modules.md +37 -9
  34. package/resources/skill/references/runtime.md +23 -1
  35. package/src/cli.js +24 -0
  36. package/src/commandRegistry.js +9 -1
  37. package/src/commands/api.js +21 -10
  38. package/src/commands/apiKey.js +34 -0
  39. package/src/commands/capabilities.js +93 -0
  40. package/src/commands/checkout.js +1 -1
  41. package/src/commands/commit.js +1 -1
  42. package/src/commands/components.js +550 -0
  43. package/src/commands/conflict.js +1 -1
  44. package/src/commands/connect.js +18 -8
  45. package/src/commands/customService.js +20 -4
  46. package/src/commands/dataRange.js +33 -0
  47. package/src/commands/delete.js +12 -1
  48. package/src/commands/diff.js +18 -2
  49. package/src/commands/grant.js +29 -0
  50. package/src/commands/group.js +38 -0
  51. package/src/commands/help.js +64 -20
  52. package/src/commands/init.js +3 -3
  53. package/src/commands/map.js +145 -17
  54. package/src/commands/mcp.js +2 -2
  55. package/src/commands/reconcile.js +1 -1
  56. package/src/commands/role.js +32 -0
  57. package/src/commands/space.js +41 -0
  58. package/src/commands/status.js +110 -7
  59. package/src/commands/update.js +23 -11
  60. package/src/commands/verify.js +75 -0
  61. package/src/commands/worklog.js +6 -2
  62. package/src/consoleEncoding.js +34 -0
  63. package/src/contractCompatibility.js +57 -0
  64. package/src/customServices.js +138 -18
  65. package/src/diffReport.js +106 -0
  66. package/src/index.js +2 -0
  67. package/src/localRuntime/compose.js +14 -17
  68. package/src/localRuntime/index.js +22 -23
  69. package/src/localRuntime/services.js +27 -36
  70. package/src/mcp/client.js +11 -2
  71. package/src/mcp/protocol.js +22 -2
  72. package/src/mcp/tools.js +14 -1
  73. package/src/platforms.js +9 -0
  74. package/src/projectConfig.js +6 -4
  75. package/src/releaseInstall.js +105 -0
  76. package/src/updateCheck.js +48 -28
  77. package/src/worklog.js +2 -1
  78. package/src/worktree/backend.js +1 -1
  79. 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;
@@ -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 版本:${getPackageVersion()}`);
11
- log.info(`已装 skill 版本:${readInstalledVersion(projectDir) || '(未安装)'}`);
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 t of all) {
16
- const st = t.status(projectDir);
17
- const mark = st.installed ? log.c.green('✓') : log.c.gray('·');
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;
@@ -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 npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
34
+ const marker = readInstallMarker();
35
+ let command = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
34
36
  log.step(`更新 draftgo-cli:${current} → ${latest}`);
35
- const install = spawnSync(npm, ['install', '-g', `draftgo-cli@${latest}`], { stdio: 'inherit', shell: false });
36
- if (install.error) {
37
- log.err(`CLI 更新启动失败:${install.error.message}`);
38
- return 1;
39
- }
40
- if (install.status !== 0) {
41
- log.err(`npm install 返回码 ${install.status}`);
42
- return Number.isInteger(install.status) ? install.status : 1;
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,
@@ -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;
@@ -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
- output(flags, { path: result.path, item: resultFor(result.blocks, result.reference) });
42
- if (flags.output !== 'json') log.ok(`Worklog item added: ${title}`);
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
+ };