draftgo-cli 4.0.25 → 4.0.26

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 (87) hide show
  1. package/README.md +21 -37
  2. package/package.json +3 -5
  3. package/resources/skill/SKILL.md +9 -5
  4. package/resources/skill/manifest.json +2 -5
  5. package/resources/skill/references/ai.md +41 -0
  6. package/resources/skill/references/app-api.md +2 -50
  7. package/resources/skill/references/architecture.md +1 -1
  8. package/resources/skill/references/chat-sdk.md +29 -37
  9. package/resources/skill/references/checkout.md +4 -4
  10. package/resources/skill/references/data.md +0 -46
  11. package/resources/skill/references/delivery.md +3 -3
  12. package/resources/skill/references/diagnostics.md +10 -11
  13. package/resources/skill/references/frontend.md +23 -20
  14. package/resources/skill/references/mcp.md +4 -14
  15. package/resources/skill/references/methods.md +15 -68
  16. package/resources/skill/references/modules.md +23 -44
  17. package/resources/skill/references/runtime.md +3 -20
  18. package/resources/skill/story/SKILL.md +2 -2
  19. package/src/apiContractCache.js +14 -6
  20. package/src/cli.js +0 -7
  21. package/src/commandRegistry.js +0 -6
  22. package/src/commands/api.js +87 -17
  23. package/src/commands/apiKey.js +2 -6
  24. package/src/commands/autoPush.js +15 -51
  25. package/src/commands/capabilities.js +22 -15
  26. package/src/commands/check.js +19 -53
  27. package/src/commands/checkout.js +1 -4
  28. package/src/commands/clean.js +1 -1
  29. package/src/commands/commit.js +1 -4
  30. package/src/commands/components.js +12 -8
  31. package/src/commands/conflict.js +4 -6
  32. package/src/commands/conflicts.js +1 -2
  33. package/src/commands/connect.js +0 -8
  34. package/src/commands/delete.js +15 -11
  35. package/src/commands/deploy.js +64 -26
  36. package/src/commands/diff.js +1 -4
  37. package/src/commands/group.js +2 -3
  38. package/src/commands/help.js +19 -41
  39. package/src/commands/init.js +13 -6
  40. package/src/commands/local.js +4 -1
  41. package/src/commands/map.js +138 -23
  42. package/src/commands/reconcile.js +1 -15
  43. package/src/commands/role.js +1 -2
  44. package/src/commands/status.js +12 -40
  45. package/src/commands/verify.js +8 -7
  46. package/src/commands/worklog.js +11 -5
  47. package/src/contractCompatibility.js +10 -2
  48. package/src/localRuntime/compose.js +41 -27
  49. package/src/localRuntime/detect.js +6 -6
  50. package/src/localRuntime/index.js +47 -47
  51. package/src/localRuntime/services.js +2 -39
  52. package/src/mcp/client.js +99 -134
  53. package/src/mcp/parallel.js +25 -2
  54. package/src/mcp/protocol.js +38 -9
  55. package/src/mcp/tools.js +10 -19
  56. package/src/projectConfig.js +1 -4
  57. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  58. package/src/projectMap.js +1 -1
  59. package/src/runtimeFiles.js +2 -1
  60. package/src/worklog.js +3 -2
  61. package/src/worktree/backend.js +127 -15
  62. package/src/worktree/index.js +64 -22
  63. package/src/worktree/locks.js +52 -0
  64. package/src/worktree/manifest.js +18 -4
  65. package/src/worktree/status.js +4 -2
  66. package/resources/custom-service-sdk/ai.go +0 -520
  67. package/resources/custom-service-sdk/ai_test.go +0 -156
  68. package/resources/custom-service-sdk/auth_test.go +0 -56
  69. package/resources/custom-service-sdk/billing.go +0 -596
  70. package/resources/custom-service-sdk/billing_test.go +0 -150
  71. package/resources/custom-service-sdk/go.mod +0 -3
  72. package/resources/custom-service-sdk/manifest.json +0 -77
  73. package/resources/custom-service-sdk/platform.go +0 -352
  74. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  75. package/resources/custom-service-sdk/registration_test.go +0 -39
  76. package/resources/custom-service-sdk/resources.go +0 -247
  77. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  78. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  79. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  80. package/resources/custom-service-sdk/sdk.go +0 -209
  81. package/resources/skill/references/aihub.md +0 -116
  82. package/resources/skill/references/custom-services.md +0 -201
  83. package/src/commands/customService.js +0 -95
  84. package/src/commands/dataRange.js +0 -33
  85. package/src/commands/grant.js +0 -29
  86. package/src/commands/space.js +0 -41
  87. package/src/customServices.js +0 -484
@@ -16,20 +16,14 @@ const commands = [
16
16
  { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
17
17
  { name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
18
18
  { name: 'commit', run: (dir, args, flags) => require('./commands/commit')(dir, args, flags) },
19
- { name: 'validate', run: (dir, args, flags) => require('./commands/customService').validate(dir, args, flags) },
20
- { name: 'test', run: (dir, args, flags) => require('./commands/customService').test(dir, args, flags) },
21
- { name: 'publish', run: (dir, args, flags) => require('./commands/customService').publish(dir, args, flags) },
22
19
  { name: 'reconcile', run: (dir, args, flags) => require('./commands/reconcile')(dir, args, flags) },
23
20
  { name: 'diff', run: (dir, args, flags) => require('./commands/diff')(dir, args, flags) },
24
21
  { name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
25
22
  { name: 'conflict', run: (dir, args, flags) => require('./commands/conflict')(dir, args, flags) },
26
23
  { name: 'api', run: (dir, args, flags) => require('./commands/api')(dir, args, flags) },
27
- { name: 'space', run: (dir, args, flags) => require('./commands/space')(dir, args, flags) },
28
24
  { name: 'group', run: (dir, args, flags) => require('./commands/group')(dir, args, flags) },
29
- { name: 'grant', run: (dir, args, flags) => require('./commands/grant')(dir, args, flags) },
30
25
  { name: 'api-key', run: (dir, args, flags) => require('./commands/apiKey')(dir, args, flags) },
31
26
  { name: 'role', run: (dir, args, flags) => require('./commands/role')(dir, args, flags) },
32
- { name: 'data-range', run: (dir, args, flags) => require('./commands/dataRange')(dir, args, flags) },
33
27
  { name: 'delete', run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
34
28
  { name: 'deploy', run: (dir, args, flags) => require('./commands/deploy')(dir, args, flags) },
35
29
  { name: 'auto-push', run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
@@ -5,17 +5,51 @@ const path = require('path');
5
5
  const log = require('../logger');
6
6
  const { confirm } = require('../prompt');
7
7
  const { loadProjectConfig } = require('../projectConfig');
8
- const { TOOL_NAMES, openToolSession, callStructured, configuredScope } = require('../mcp/tools');
8
+ const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
9
  const { cachedDescription, normalizeDescription, storeDescription } = require('../apiContractCache');
10
10
  const {
11
11
  isContractCompatibilityError,
12
12
  isHardContractIncompatibility,
13
13
  isDangerousOperation,
14
14
  sameOperationContract,
15
+ canRetryAfterContractChange,
15
16
  incompatibleOperationError,
16
17
  } = require('../contractCompatibility');
17
18
 
19
+ async function registryRevision(session, operationId) {
20
+ const result = await callStructured(session, TOOL_NAMES.apiSearch, {
21
+ query: operationId,
22
+ limit: 100,
23
+ });
24
+ const items = result && Array.isArray(result.items) ? result.items : [];
25
+ const operation = items.find((item) => item && item.operation_id === operationId);
26
+ const revision = String(result && result.registry_revision || '').trim();
27
+ if (!operation || !revision) {
28
+ throw new Error(`DraftGo Registry could not resolve operation ${operationId} with a revision.`);
29
+ }
30
+ return revision;
31
+ }
32
+
33
+ async function describeOperation(projectDir, config, session, operationId, knownRevision = '') {
34
+ let revision = String(knownRevision || '').trim();
35
+ if (!revision) revision = await registryRevision(session, operationId);
36
+ const described = await callStructured(session, TOOL_NAMES.apiDescribe, {
37
+ operation_id: operationId,
38
+ registry_revision: revision,
39
+ });
40
+ return storeDescription(projectDir, config.server, described, revision)
41
+ || normalizeDescription(described, revision);
42
+ }
43
+
44
+ async function descriptionForRevision(projectDir, config, session, operationId, knownRevision = '') {
45
+ const revision = String(knownRevision || '').trim() || await registryRevision(session, operationId);
46
+ const cached = cachedDescription(projectDir, config.server, operationId);
47
+ if (cached && cached.registry_revision === revision) return cached;
48
+ return describeOperation(projectDir, config, session, operationId, revision);
49
+ }
50
+
18
51
  function readCallInput(projectDir, flags) {
52
+ if (flags.input && flags.params) throw new Error('Use either --input or --params, not both.');
19
53
  const raw = flags.input
20
54
  ? fs.readFileSync(path.resolve(projectDir, String(flags.input)))
21
55
  : flags.params ? Buffer.from(String(flags.params), 'utf8') : Buffer.from('{}', 'utf8');
@@ -32,17 +66,38 @@ function diagnosticResult(operationId, result) {
32
66
  const statusCode = source.status_code ?? source.statusCode ?? source.status ?? null;
33
67
  const headerRequestId = source.headers
34
68
  && (source.headers['x-request-id'] || source.headers['X-Request-Id']);
35
- const requestId = source.request_id ?? source.requestId ?? headerRequestId ?? null;
36
- const error = source.error && typeof source.error === 'object' ? source.error : null;
69
+ const rawRequestId = source.request_id ?? source.requestId ?? headerRequestId ?? null;
70
+ const requestId = Array.isArray(rawRequestId) ? rawRequestId[0] || null : rawRequestId;
71
+ const data = source.data && typeof source.data === 'object' ? source.data : {};
72
+ const error = source.error || data.error || {};
37
73
  return {
38
74
  operation_id: operationId,
39
75
  status_code: statusCode,
40
- server_code: error && error.code || source.code || null,
76
+ server_code: error.code || data.code || source.code || null,
41
77
  request_id: requestId,
42
78
  result,
43
79
  };
44
80
  }
45
81
 
82
+ function assertInvocationResult(operationId, result) {
83
+ const diagnostic = diagnosticResult(operationId, result);
84
+ if (result && result.workflow && result.workflow.required) {
85
+ const error = new Error(`Operation ${operationId} requires the ${result.workflow.workflow || result.workflow.mode} workflow; it has not executed.`);
86
+ error.code = 'WORKFLOW_REQUIRED';
87
+ error.details = { ...diagnostic, workflow: result.workflow };
88
+ throw error;
89
+ }
90
+ if (Number(diagnostic.status_code) >= 400) {
91
+ const data = result.data || {};
92
+ const error = new Error(data.error && data.error.message || data.message || `DraftGo returned HTTP ${diagnostic.status_code}.`);
93
+ error.code = diagnostic.server_code || 'HTTP_ERROR';
94
+ error.status = diagnostic.status_code;
95
+ error.details = { ...data, request_id: diagnostic.request_id };
96
+ throw error;
97
+ }
98
+ return result;
99
+ }
100
+
46
101
  function diagnosticError(operationId, error) {
47
102
  const source = error && (error.details || error.data) || {};
48
103
  const nested = source.error && typeof source.error === 'object' ? source.error : {};
@@ -77,11 +132,12 @@ async function callOperation(projectDir, operationId, flags = {}) {
77
132
  let config;
78
133
  try {
79
134
  config = loadProjectConfig(projectDir);
80
- session = await openToolSession(config, [TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
135
+ session = await openToolSession(config, [
136
+ TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
137
+ ]);
81
138
  description = cachedDescription(projectDir, config.server, operationId);
82
139
  if (!description) {
83
- const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
84
- description = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
140
+ description = await describeOperation(projectDir, config, session, operationId);
85
141
  }
86
142
  if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
87
143
  input = readCallInput(projectDir, flags);
@@ -90,14 +146,20 @@ async function callOperation(projectDir, operationId, flags = {}) {
90
146
  }
91
147
  const dangerous = isDangerousOperation(description);
92
148
  if (dangerous && !flags.yes && !flags.y) {
149
+ if (flags.output === 'json' || !process.stdin.isTTY) {
150
+ console.log(JSON.stringify({
151
+ operation_id: operationId,
152
+ error: { code: 'CONFIRMATION_REQUIRED', message: 'Dangerous operations require --yes in JSON output mode.' },
153
+ }, null, 2));
154
+ return 1;
155
+ }
93
156
  const approved = await confirm(`Call destructive DraftGo operation ${operationId}?`, { default: false });
94
- if (!approved) return 0;
157
+ if (!approved) return 1;
95
158
  }
96
159
  let result;
97
160
  try {
98
161
  const invoke = (contract, confirmed) => callStructured(session, TOOL_NAMES.apiCall, {
99
162
  ...input, operation_id: operationId, registry_revision: contract.registry_revision,
100
- ...configuredScope(config, contract),
101
163
  ...(confirmed ? { confirm: true } : {}),
102
164
  });
103
165
  try {
@@ -108,15 +170,19 @@ async function callOperation(projectDir, operationId, flags = {}) {
108
170
  throw incompatibleOperationError(operationId, error);
109
171
  }
110
172
  const previousDescription = description;
111
- const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
112
- description = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
173
+ const revision = await registryRevision(session, operationId);
174
+ description = await describeOperation(projectDir, config, session, operationId, revision);
113
175
  if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
114
176
  const changedDangerous = isDangerousOperation(description);
115
177
  if ((dangerous || changedDangerous) && !sameOperationContract(previousDescription, description)) {
116
178
  throw incompatibleOperationError(operationId, error);
117
179
  }
180
+ if (!canRetryAfterContractChange(previousDescription, description)) {
181
+ throw incompatibleOperationError(operationId, error);
182
+ }
118
183
  result = await invoke(description, changedDangerous);
119
184
  }
185
+ assertInvocationResult(operationId, result);
120
186
  } catch (error) {
121
187
  return printCallError(operationId, error, flags);
122
188
  }
@@ -155,12 +221,12 @@ async function apiCommand(projectDir, positional, flags = {}) {
155
221
  }
156
222
 
157
223
  const config = loadProjectConfig(projectDir);
158
- const expected = describe ? TOOL_NAMES.apiDescribe : TOOL_NAMES.apiSearch;
159
- const session = await openToolSession(config, [expected]);
160
- const result = await callStructured(session, expected, describe
161
- ? { operation_id: query }
162
- : { query });
163
- if (describe) storeDescription(projectDir, config.server, result);
224
+ const expected = describe ? [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe] : [TOOL_NAMES.apiSearch];
225
+ const session = await openToolSession(config, expected);
226
+ const result = describe
227
+ ? await describeOperation(projectDir, config, session, query)
228
+ : await callStructured(session, TOOL_NAMES.apiSearch,
229
+ require('./capabilities').searchArguments(query, flags, flags.cursor));
164
230
  if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
165
231
  else printResult(describe ? `DraftGo API ${query}` : `DraftGo API search: ${query}`, result);
166
232
  return 0;
@@ -172,3 +238,7 @@ module.exports.diagnosticResult = diagnosticResult;
172
238
  module.exports.diagnosticError = diagnosticError;
173
239
  module.exports.printCallError = printCallError;
174
240
  module.exports.callOperation = callOperation;
241
+ module.exports.registryRevision = registryRevision;
242
+ module.exports.describeOperation = describeOperation;
243
+ module.exports.descriptionForRevision = descriptionForRevision;
244
+ module.exports.assertInvocationResult = assertInvocationResult;
@@ -6,12 +6,9 @@ const { callOperation } = require('./api');
6
6
  const OPERATIONS = Object.freeze({
7
7
  status: 'getCurrentUserAPIKey',
8
8
  create: 'createCurrentUserAPIKey',
9
- update: 'updateCurrentUserAPIKey',
9
+ update: 'setCurrentUserAPIKeyActive',
10
10
  delete: 'deleteCurrentUserAPIKey',
11
11
  rotate: 'rotateCurrentUserAPIKey',
12
- reset: 'resetCurrentUserAPIKey',
13
- enable: 'enableCurrentUserAPIKey',
14
- disable: 'disableCurrentUserAPIKey',
15
12
  });
16
13
 
17
14
  function operationKey(value) {
@@ -22,10 +19,9 @@ function operationKey(value) {
22
19
  async function apiKeyCommand(projectDir, positional, flags = {}) {
23
20
  const action = operationKey(positional[0]);
24
21
  if (!action) {
25
- log.err('Usage: draftgo api-key status|create|update|delete|rotate|reset|enable|disable [--input <json-file>]');
22
+ log.err('Usage: draftgo api-key status|create|update|delete|rotate [--input <json-file>]');
26
23
  return 1;
27
24
  }
28
- if (action === 'reset') log.warn('api-key reset is deprecated; prefer rotate.');
29
25
  return callOperation(projectDir, OPERATIONS[action], flags);
30
26
  }
31
27
 
@@ -1,72 +1,36 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const log = require('../logger');
5
- const check = require('./check');
6
4
  const { loadProjectConfig } = require('../projectConfig');
7
5
  const { loadManifest, absolutePath } = require('../worktree/manifest');
8
6
  const { hashFile } = require('../worktree/streams');
9
- const { listConflicts, commitResources } = require('../worktree');
7
+ const { targetsFrom, deliverTargets, outputResult } = require('./deploy');
10
8
 
11
9
  async function changedEntries(projectDir, manifest) {
12
10
  const changed = [];
13
11
  for (const entry of Object.values(manifest.entries)) {
14
12
  const local = absolutePath(projectDir, entry.local_path);
15
- if (!fs.existsSync(local)) continue;
16
- if ((await hashFile(local)).hash !== entry.base_hash) changed.push(entry);
13
+ // Missing files must be validated, not silently dropped from delivery.
14
+ if (!fs.existsSync(local) || (await hashFile(local)).hash !== entry.base_hash) changed.push(entry);
17
15
  }
18
16
  return changed;
19
17
  }
20
18
 
21
19
  async function autoPush(projectDir, positional, flags = {}) {
22
- let config;
23
20
  try {
24
- config = loadProjectConfig(projectDir);
25
- } catch {
26
- log.dim('auto-push skipped: this project is not connected to DraftGo.');
27
- return 0;
28
- }
29
-
30
- if (config.auto_push !== true) {
31
- log.dim('auto-push skipped: .draftgo/config.json does not set auto_push to true.');
32
- return 0;
33
- }
34
- const conflicts = listConflicts(projectDir);
35
- if (conflicts.length) {
36
- log.err(`auto-push stopped: ${conflicts.length} unresolved DraftGo conflict(s).`);
37
- return 1;
38
- }
39
-
40
- const checkCode = await check(projectDir, { strict: flags.strict });
41
- if (checkCode !== 0) {
42
- log.err('auto-push stopped because local validation failed.');
43
- return checkCode;
44
- }
45
-
46
- const groups = new Map();
47
- if (positional.length) {
48
- const [resourceType, ...ids] = positional;
49
- if (!resourceType || !ids.length) {
50
- log.err('Usage: draftgo auto-push [<pages|nav|docs> <id...>]');
51
- return 1;
52
- }
53
- groups.set(resourceType, ids);
54
- } else {
55
- for (const entry of await changedEntries(projectDir, loadManifest(projectDir))) {
56
- if (!groups.has(entry.resource_type)) groups.set(entry.resource_type, []);
57
- groups.get(entry.resource_type).push(entry.resource_id);
21
+ let config;
22
+ try { config = loadProjectConfig(projectDir); }
23
+ catch (error) {
24
+ if (fs.existsSync(require('path').join(projectDir, '.draftgo', 'config.json'))) throw error;
25
+ return outputResult({ skipped: 'auto-push skipped: this project is not connected to DraftGo.' }, flags);
58
26
  }
59
- }
60
- if (!groups.size) {
61
- log.dim('auto-push: no checked-out content changed.');
62
- return 0;
63
- }
64
-
65
- for (const [resourceType, ids] of groups) {
66
- await commitResources(projectDir, resourceType, ids, { config });
67
- }
68
- log.ok('auto-push committed all changed checked-out content.');
69
- return 0;
27
+ if (config.auto_push !== true) return outputResult({ skipped: 'auto-push skipped: auto_push is not enabled.' }, flags);
28
+ const targets = positional.length ? targetsFrom(positional)
29
+ : (await changedEntries(projectDir, loadManifest(projectDir))).map(({ resource_type, resource_id }) => ({ resource_type, resource_id }));
30
+ if (!targets.length) return outputResult({ skipped: 'auto-push: no checked-out content changed.' }, flags);
31
+ const value = await deliverTargets(projectDir, targets, { ...flags, delivery: 'deploy' }, { config });
32
+ return outputResult({ ...value, delivery: 'auto-push' }, flags);
33
+ } catch (error) { return outputResult({ error: { code: error.code || 'AUTO_PUSH_FAILED', message: error.message } }, flags); }
70
34
  }
71
35
 
72
36
  module.exports = autoPush;
@@ -23,10 +23,12 @@ function print(value, flags, title) {
23
23
  }
24
24
 
25
25
  function searchArguments(query, flags, cursor) {
26
- const input = { limit: Math.min(100, Math.max(1, Number(flags.limit) || 100)) };
26
+ const limit = flags.limit == null ? 20 : Number(flags.limit);
27
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error('--limit must be an integer between 1 and 100.');
28
+ const input = { limit };
27
29
  if (query) input.query = query;
28
30
  if (flags.module) input.module = String(flags.module);
29
- if (flags['resource-type'] || flags.type) input.resource_type = String(flags['resource-type'] || flags.type);
31
+ if (flags.type) input.module = String(flags.type);
30
32
  if (flags.method) input.method = String(flags.method).toUpperCase();
31
33
  if (flags.risk) input.risk = String(flags.risk);
32
34
  if (flags.permission) input.permission = String(flags.permission);
@@ -36,43 +38,48 @@ function searchArguments(query, flags, cursor) {
36
38
 
37
39
  async function collect(session, query, flags, allPages) {
38
40
  const result = [];
41
+ let registryRevision = null;
42
+ let total = null;
39
43
  let cursor = flags.cursor || null;
40
44
  const seen = new Set();
41
45
  do {
42
46
  const page = await callStructured(session, TOOL_NAMES.apiSearch, searchArguments(query, flags, cursor));
43
47
  result.push(...itemsOf(page));
48
+ registryRevision ||= page && page.registry_revision || null;
49
+ if (Number.isFinite(Number(page && page.total))) total = Number(page.total);
44
50
  cursor = nextCursorOf(page);
45
51
  if (!allPages || !cursor) break;
46
52
  if (seen.has(cursor)) throw new Error('DraftGo capability search repeated a cursor.');
47
53
  seen.add(cursor);
48
54
  } while (true);
49
- return { items: result, next_cursor: cursor };
55
+ return { items: result, next_cursor: cursor, registry_revision: registryRevision, total };
50
56
  }
51
57
 
52
58
  async function capabilities(projectDir, positional = [], flags = {}) {
59
+ if (flags['resource-type'] != null) {
60
+ throw new Error('Capabilities --resource-type is not supported; use --type or --module for a Registry module.');
61
+ }
53
62
  const action = String(positional[0] || 'list').toLowerCase();
54
63
  const config = loadProjectConfig(projectDir);
55
64
  if (action === 'show') {
56
65
  const operationID = String(positional[1] || '').trim();
57
66
  if (!operationID) throw new Error('Usage: draftgo capabilities show <operation_id>');
58
- const session = await openToolSession(config, [TOOL_NAMES.apiDescribe]);
59
- const result = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationID });
67
+ const { registryRevision, descriptionForRevision } = require('./api');
68
+ const session = await openToolSession(config, [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe]);
69
+ const revision = await registryRevision(session, operationID);
70
+ const result = await descriptionForRevision(projectDir, config, session, operationID, revision);
60
71
  print(result, flags, `DraftGo capability ${operationID}`);
61
72
  return 0;
62
73
  }
63
74
  if (action === 'audit') {
64
- const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.apiSearch]);
65
- const overview = await callStructured(session, TOOL_NAMES.projectOverview, {});
75
+ const session = await openToolSession(config, [TOOL_NAMES.apiSearch]);
66
76
  const catalog = await collect(session, '', flags, true);
67
- const registry = overview && (overview.operation_registry || overview.registry) || {};
68
77
  const result = {
69
78
  server: config.server,
70
- registry_revision: registry.registry_revision || overview.registry_revision || null,
71
- mapped_operations: registry.mapped_operations ?? registry.mapped ?? catalog.items.length,
72
- excluded_operations: registry.excluded_operations ?? registry.excluded ?? null,
73
- total_operations: registry.total_operations ?? registry.total ?? null,
79
+ registry_revision: catalog.registry_revision,
80
+ total_operations: catalog.total ?? catalog.items.length,
74
81
  discovered_operations: catalog.items.length,
75
- complete: (registry.mapped_operations ?? registry.mapped ?? catalog.items.length) === catalog.items.length,
82
+ complete: catalog.next_cursor == null && (catalog.total == null || catalog.total === catalog.items.length),
76
83
  };
77
84
  print(result, flags, 'DraftGo capability audit');
78
85
  return result.complete ? 0 : 1;
@@ -83,8 +90,8 @@ async function capabilities(projectDir, positional = [], flags = {}) {
83
90
  throw new Error('Capability search needs a query or filter.');
84
91
  }
85
92
  const session = await openToolSession(config, [TOOL_NAMES.apiSearch]);
86
- const result = await collect(session, query, flags, action === 'list' && !flags.cursor);
87
- print(flags.output === 'json' ? result : result.items, flags, action === 'search' ? `DraftGo capabilities: ${query || 'filtered'}` : 'DraftGo capabilities');
93
+ const result = await collect(session, query, flags, false);
94
+ print(result, flags, action === 'search' ? `DraftGo capabilities: ${query || 'filtered'}` : 'DraftGo capabilities');
88
95
  return 0;
89
96
  }
90
97
 
@@ -3,24 +3,19 @@
3
3
  const log = require('../logger');
4
4
  const { analyzeProject } = require('../projectMap');
5
5
  const { inspectRemoteCheckouts } = require('../worktree/status');
6
- const customServices = require('../customServices');
7
- const { workspaceHealth } = require('../workspaceHealth');
6
+ const { projectHealth } = require('../projectHealth');
8
7
 
9
8
  async function check(projectDir, positional = [], flags = {}) {
10
9
  if (!Array.isArray(positional)) {
11
10
  flags = positional || {};
12
11
  positional = [];
13
12
  }
14
- const [requestedType, ...requestedIds] = positional;
15
- if (requestedType && !customServicesCommand().isServiceType(requestedType)) {
16
- log.err('Usage: draftgo check [custom-services <id...>] [--remote]');
17
- return 1;
18
- }
19
- if (requestedType && !requestedIds.length) {
20
- log.err('Usage: draftgo check custom-services <id...> [--remote]');
13
+ if (positional.length) {
14
+ if (!flags.quiet) log.err('Usage: draftgo check [--remote]');
21
15
  return 1;
22
16
  }
23
17
  const resourceKeys = flags.resourceKeys || null;
18
+ const quiet = Boolean(flags.quiet);
24
19
  const result = analyzeProject(projectDir, { resourceKeys });
25
20
  if (resourceKeys) {
26
21
  const manifestKeys = new Set(Object.keys(result.map.checkouts.reduce((entries, entry) => {
@@ -29,44 +24,17 @@ async function check(projectDir, positional = [], flags = {}) {
29
24
  }, {})));
30
25
  for (const key of resourceKeys) if (!manifestKeys.has(key)) result.errors.push(`${key}: resource is not checked out`);
31
26
  }
32
- const hygiene = workspaceHealth(projectDir);
33
- hygiene.warnings.forEach((warning) => result.warningDetails.push({ code: warning.code, confidence: 'high', message: `${warning.path}${warning.size ? ` (${warning.size} bytes)` : ''}` }));
34
- hygiene.warnings.forEach((warning) => result.warnings.push(`${warning.code}: ${warning.path}`));
35
- const serviceManifest = customServices.loadManifest(projectDir);
36
- const customServiceChecks = [];
37
- const requested = new Set(requestedIds.map(String));
38
- const serviceEntries = Object.values(serviceManifest.entries)
39
- .filter((entry) => !resourceKeys && (!requestedType || requested.has(String(entry.resource_id))));
40
- if (requestedType) {
41
- for (const id of requested) {
42
- if (!serviceEntries.some((entry) => String(entry.resource_id) === id)) {
43
- result.errors.push(`custom_services ${id}: resource is not checked out`);
44
- }
45
- }
46
- }
47
- for (const entry of serviceEntries) {
48
- try {
49
- const state = customServices.diff(projectDir, entry.resource_id);
50
- customServiceChecks.push({ resource_type: 'custom_services', resource_id: entry.resource_id, state: state.changed ? 'local_modified' : 'clean_local', files: state.files });
51
- } catch (error) {
52
- result.errors.push(`custom_services ${entry.resource_id}: ${error.message}`);
53
- }
54
- }
27
+ const hygiene = projectHealth(projectDir);
28
+ const hygieneWarnings = resourceKeys ? [] : hygiene.warnings;
29
+ hygieneWarnings.forEach((warning) => result.warningDetails.push({ code: warning.code, confidence: 'high', message: `${warning.path}${warning.size ? ` (${warning.size} bytes)` : ''}` }));
30
+ hygieneWarnings.forEach((warning) => result.warnings.push(`${warning.code}: ${warning.path}`));
55
31
  let remote = null;
56
32
  let remoteError = null;
57
33
  if (flags.remote) {
58
34
  try {
59
- const [contentRemote, serviceRemote] = await Promise.all([
60
- inspectRemoteCheckouts(projectDir, resourceKeys ? {
35
+ remote = await inspectRemoteCheckouts(projectDir, resourceKeys ? {
61
36
  entries: result.map.checkouts.filter((entry) => resourceKeys.includes(`${entry.resource_type}:${entry.resource_id}`)),
62
- } : {}),
63
- resourceKeys
64
- ? Promise.resolve([])
65
- : customServices.inspectRemote(projectDir, requestedType ? { ids: [...requested] } : {}),
66
- ]);
67
- remote = requestedType
68
- ? serviceRemote.filter((entry) => requested.has(String(entry.resource_id)))
69
- : [...contentRemote, ...serviceRemote];
37
+ } : {});
70
38
  for (const entry of remote) {
71
39
  if (!['clean', 'clean_local', 'local_modified'].includes(entry.state)) {
72
40
  const detail = `${entry.resource_type} ${entry.resource_id}: ${entry.state}`
@@ -82,25 +50,27 @@ async function check(projectDir, positional = [], flags = {}) {
82
50
  }
83
51
  }
84
52
  const output = {
85
- content_validation: { map: result.map, errors: result.errors, warnings: result.warnings, warning_details: result.warningDetails, custom_services: customServiceChecks },
53
+ content_validation: { map: result.map, errors: result.errors, warnings: result.warnings, warning_details: result.warningDetails },
86
54
  remote_validation: { resources: remote, error: remoteError },
87
- workspace_hygiene: hygiene,
55
+ project_hygiene: hygiene,
88
56
  };
57
+ const code = result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
58
+ if (flags.returnResult) return { code, ...output };
59
+ if (quiet) return code;
89
60
  if (flags.output === 'json') {
90
- console.log(JSON.stringify(output, null, 2));
61
+ if (!quiet) console.log(JSON.stringify(output, null, 2));
91
62
  return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
92
63
  }
93
64
 
94
- log.title('draftgo check');
65
+ if (!quiet) log.title('draftgo check');
95
66
  const selectedCount = (type, entries) => resourceKeys
96
67
  ? entries.filter((entry) => resourceKeys.includes(`${type}:${entry.resource_id}`)).length
97
68
  : entries.length;
98
69
  log.info(`Checked-out pages: ${selectedCount('pages', result.map.pages)}`);
99
70
  log.info(`Checked-out navigations: ${selectedCount('navigations', result.map.navigations)}`);
100
71
  log.info(`Checked-out docs: ${selectedCount('docs', result.map.docs)}`);
101
- log.info(`Checked-out custom services: ${customServiceChecks.length}`);
102
- log.info(`Workspace: ${hygiene.managed_files} managed, ${hygiene.unknown_files} unknown, ${hygiene.temporary_files} temporary, ${hygiene.artifact_files} artifact file(s)`);
103
- log.info(`Workspace size: ${hygiene.total_bytes} bytes; reclaimable: ${hygiene.reclaimable_bytes} bytes`);
72
+ log.info(`Project: ${hygiene.managed_files} managed, ${hygiene.unknown_files} unknown, ${hygiene.temporary_files} temporary, ${hygiene.artifact_files} artifact file(s)`);
73
+ log.info(`Project size: ${hygiene.total_bytes} bytes; reclaimable: ${hygiene.reclaimable_bytes} bytes`);
104
74
  if (remote) log.info(`Remote checkout comparison: ${remote.length} resource(s)`);
105
75
  for (const message of result.errors) log.err(message);
106
76
  for (const detail of result.warningDetails) log.warn(`[${detail.code}/${detail.confidence}] ${detail.message}`);
@@ -109,8 +79,4 @@ async function check(projectDir, positional = [], flags = {}) {
109
79
  return 0;
110
80
  }
111
81
 
112
- function customServicesCommand() {
113
- return require('./customService');
114
- }
115
-
116
82
  module.exports = check;
@@ -6,12 +6,9 @@ const { checkoutResources } = require('../worktree');
6
6
  async function checkout(projectDir, positional, flags = {}) {
7
7
  const [resourceType, ...ids] = positional;
8
8
  if (!resourceType || !ids.length) {
9
- log.err('Usage: draftgo checkout <pages|nav|docs|custom-services> <id...>');
9
+ log.err('Usage: draftgo checkout <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
- if (require('./customService').isServiceType(resourceType)) {
13
- return require('./customService').checkout(projectDir, ids, flags);
14
- }
15
12
  const results = await checkoutResources(projectDir, resourceType, ids, { force: Boolean(flags.force) });
16
13
  if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
17
14
  else for (const entry of results) log.ok(`Checked out ${entry.resource_type} ${entry.resource_id} -> ${entry.local_path}`);
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const log = require('../logger');
6
6
  const runtime = require('../runtimeFiles');
7
7
 
8
- const MANAGED_PREFIXES = ['tmp/', 'artifacts/ui/', 'artifacts/api/', 'artifacts/custom-services/'];
8
+ const MANAGED_PREFIXES = ['tmp/', 'artifacts/ui/', 'artifacts/api/'];
9
9
  function fileSize(file) { try { const stat = fs.statSync(file); return stat.isFile() ? stat.size : 0; } catch { return 0; } }
10
10
  function olderThan(value) {
11
11
  if (value == null || value === '') return 0;
@@ -6,12 +6,9 @@ const { commitResources } = require('../worktree');
6
6
  async function commit(projectDir, positional, flags = {}) {
7
7
  const [resourceType, ...ids] = positional;
8
8
  if (!resourceType || !ids.length) {
9
- log.err('Usage: draftgo commit <pages|nav|docs|custom-services> <id...>');
9
+ log.err('Usage: draftgo commit <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
- if (require('./customService').isServiceType(resourceType)) {
13
- return require('./customService').commit(projectDir, ids, flags);
14
- }
15
12
  const streamed = [];
16
13
  const printStatus = (result) => {
17
14
  streamed.push(result);
@@ -46,7 +46,7 @@ function csv(value) {
46
46
 
47
47
  function normalizeDefinition(definition = {}) {
48
48
  return {
49
- html: definition.html || '', css: definition.css || '', js: definition.js || '',
49
+ html: definition.html || '', css: definition.css || '', js: definition.js || '', assets: definition.assets || [],
50
50
  props: definition.props || [], slots: definition.slots || [], css_variables: definition.css_variables || [],
51
51
  events: definition.events || [], dependencies: definition.dependencies || [], examples: definition.examples || [],
52
52
  usage: definition.usage || '', root_tag: definition.root_tag || '', meta: definition.meta || {},
@@ -66,12 +66,12 @@ function componentDir(projectDir, name) {
66
66
  }
67
67
 
68
68
  function contractFrom(definition = {}) {
69
- const { props, slots, css_variables, events, dependencies, examples, usage, root_tag, meta } = normalizeDefinition(definition);
70
- return { props, slots, css_variables, events, dependencies, examples, usage, root_tag, meta };
69
+ const { assets, props, slots, css_variables, events, dependencies, examples, usage, root_tag, meta } = normalizeDefinition(definition);
70
+ return { assets, props, slots, css_variables, events, dependencies, examples, usage, root_tag, meta };
71
71
  }
72
72
 
73
73
  function localDefinition(directory) {
74
- const contract = JSON.parse(fs.readFileSync(path.join(directory, 'contract.json'), 'utf8'));
74
+ const contract = JSON.parse(fs.readFileSync(path.join(directory, 'component.json'), 'utf8'));
75
75
  return normalizeDefinition({
76
76
  html: fs.readFileSync(path.join(directory, 'component.html'), 'utf8'),
77
77
  css: fs.readFileSync(path.join(directory, 'component.css'), 'utf8'),
@@ -85,12 +85,12 @@ function writeComponentFiles(directory, item) {
85
85
  fs.writeFileSync(path.join(directory, 'component.html'), item.draft.html || '', 'utf8');
86
86
  fs.writeFileSync(path.join(directory, 'component.css'), item.draft.css || '', 'utf8');
87
87
  fs.writeFileSync(path.join(directory, 'component.js'), item.draft.js || '', 'utf8');
88
- fs.writeFileSync(path.join(directory, 'contract.json'), `${JSON.stringify(contractFrom(item.draft), null, 2)}\n`, 'utf8');
89
88
  const metadata = {
90
89
  id: item.id, library_id: item.library_id, library_slug: item.library_slug,
91
90
  name: item.name, slug: item.slug, category: item.category || '', description: item.description || '',
92
91
  tags: item.tags || [], status: item.status || 'active', published_revision: item.published_revision || 0,
93
92
  remote_draft_hash: definitionHash(item.draft), updated_at: item.updated_at,
93
+ ...contractFrom(item.draft),
94
94
  };
95
95
  fs.writeFileSync(path.join(directory, 'component.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
96
96
  fs.writeFileSync(path.join(directory, '.base.json'), `${JSON.stringify(item, null, 2)}\n`, 'utf8');
@@ -231,7 +231,7 @@ async function diff(projectDir, name, flags) {
231
231
  const config = loadProjectConfig(projectDir);
232
232
  const state = localState(projectDir, name);
233
233
  const remote = await findComponent(config, name);
234
- const sections = ['html', 'css', 'js', 'props', 'slots', 'css_variables', 'events', 'dependencies', 'examples', 'usage', 'root_tag', 'meta'];
234
+ const sections = ['html', 'css', 'js', 'assets', 'props', 'slots', 'css_variables', 'events', 'dependencies', 'examples', 'usage', 'root_tag', 'meta'];
235
235
  const changed = (left, right) => {
236
236
  const normalizedLeft = normalizeDefinition(left);
237
237
  const normalizedRight = normalizeDefinition(right);
@@ -364,9 +364,13 @@ async function exportZIP(projectDir, librarySlug, flags) {
364
364
  }
365
365
  for (const summary of components) {
366
366
  const item = await request(config, 'GET', `components/${summary.id}`);
367
- const definition = `components/${item.slug}.json`;
367
+ const directory = `components/${item.slug}`;
368
+ const definition = `${directory}/component.json`;
368
369
  manifest.components.push({ name: item.name, slug: item.slug, category: item.category, description: item.description, tags: item.tags, definition });
369
- zip.addFile(definition, Buffer.from(`${JSON.stringify(item.draft, null, 2)}\n`, 'utf8'));
370
+ zip.addFile(definition, Buffer.from(`${JSON.stringify(contractFrom(item.draft), null, 2)}\n`, 'utf8'));
371
+ zip.addFile(`${directory}/component.html`, Buffer.from(item.draft.html || '', 'utf8'));
372
+ zip.addFile(`${directory}/component.css`, Buffer.from(item.draft.css || '', 'utf8'));
373
+ zip.addFile(`${directory}/component.js`, Buffer.from(item.draft.js || '', 'utf8'));
370
374
  }
371
375
  zip.addFile('draftgo-components.json', Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8'));
372
376
  const destination = path.resolve(projectDir, flags.file || `${library.slug}-components.zip`);