draftgo-cli 4.0.24 → 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 (88) hide show
  1. package/README.md +23 -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 +22 -43
  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/update.js +18 -24
  46. package/src/commands/verify.js +8 -7
  47. package/src/commands/worklog.js +11 -5
  48. package/src/contractCompatibility.js +10 -2
  49. package/src/localRuntime/compose.js +41 -27
  50. package/src/localRuntime/detect.js +6 -6
  51. package/src/localRuntime/index.js +47 -47
  52. package/src/localRuntime/services.js +2 -39
  53. package/src/mcp/client.js +99 -134
  54. package/src/mcp/parallel.js +25 -2
  55. package/src/mcp/protocol.js +38 -9
  56. package/src/mcp/tools.js +10 -19
  57. package/src/projectConfig.js +1 -4
  58. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  59. package/src/projectMap.js +1 -1
  60. package/src/runtimeFiles.js +2 -1
  61. package/src/worklog.js +3 -2
  62. package/src/worktree/backend.js +127 -15
  63. package/src/worktree/index.js +64 -22
  64. package/src/worktree/locks.js +52 -0
  65. package/src/worktree/manifest.js +18 -4
  66. package/src/worktree/status.js +4 -2
  67. package/resources/custom-service-sdk/ai.go +0 -520
  68. package/resources/custom-service-sdk/ai_test.go +0 -156
  69. package/resources/custom-service-sdk/auth_test.go +0 -56
  70. package/resources/custom-service-sdk/billing.go +0 -596
  71. package/resources/custom-service-sdk/billing_test.go +0 -150
  72. package/resources/custom-service-sdk/go.mod +0 -3
  73. package/resources/custom-service-sdk/manifest.json +0 -77
  74. package/resources/custom-service-sdk/platform.go +0 -352
  75. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  76. package/resources/custom-service-sdk/registration_test.go +0 -39
  77. package/resources/custom-service-sdk/resources.go +0 -247
  78. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  79. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  80. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  81. package/resources/custom-service-sdk/sdk.go +0 -209
  82. package/resources/skill/references/aihub.md +0 -116
  83. package/resources/skill/references/custom-services.md +0 -201
  84. package/src/commands/customService.js +0 -95
  85. package/src/commands/dataRange.js +0 -33
  86. package/src/commands/grant.js +0 -29
  87. package/src/commands/space.js +0 -41
  88. package/src/customServices.js +0 -484
@@ -7,15 +7,18 @@ const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
7
7
  const { allWithAbort } = require('../mcp/parallel');
8
8
  const { inspectRemoteCheckouts } = require('../worktree/status');
9
9
  const { loadManifest } = require('../worktree/manifest');
10
- const customServices = require('../customServices');
11
10
 
12
11
  const REMOTE_RESOURCE_TYPES = Object.freeze({
13
12
  pages: 'pages',
14
13
  navigations: 'navigations',
15
14
  docs: 'docs/articles',
16
- custom_services: 'custom_services',
17
15
  });
18
16
  const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
17
+ const LIST_OPERATIONS = Object.freeze({
18
+ pages: 'listPage',
19
+ navigations: 'listNavigation',
20
+ 'docs/articles': 'listDocAdminArticle',
21
+ });
19
22
 
20
23
  function itemsFrom(value) {
21
24
  if (Array.isArray(value)) return value;
@@ -46,8 +49,79 @@ function reportedTotal(value) {
46
49
  return Number.isInteger(total) && total >= 0 ? total : null;
47
50
  }
48
51
 
52
+ function registryCursor(operationID, page) {
53
+ return Buffer.from(JSON.stringify({ operation_id: operationID, page }), 'utf8').toString('base64url');
54
+ }
55
+
56
+ function registryPageFromCursor(cursor, operationID) {
57
+ if (cursor == null || cursor === '') return 1;
58
+ try {
59
+ const value = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
60
+ if (value && value.operation_id === operationID && Number.isSafeInteger(value.page) && value.page > 1) {
61
+ return value.page;
62
+ }
63
+ } catch { /* Report one stable CLI error below. */ }
64
+ throw new Error(`Map --cursor is not valid for Registry operation ${operationID}.`);
65
+ }
66
+
67
+ function queryProperties(operation) {
68
+ const schema = operation && operation.input_schema;
69
+ const query = schema && schema.properties && schema.properties.query;
70
+ return query && query.properties && typeof query.properties === 'object' ? query.properties : {};
71
+ }
72
+
73
+ function registryQuery(operation, operationID, flags) {
74
+ const properties = queryProperties(operation);
75
+ const query = {};
76
+ const limit = positiveLimit(flags.limit);
77
+ const cursor = flags.cursor == null ? '' : String(flags.cursor);
78
+ if (Object.prototype.hasOwnProperty.call(properties, 'cursor')) {
79
+ if (cursor) query.cursor = cursor;
80
+ if (Object.prototype.hasOwnProperty.call(properties, 'limit')) query.limit = limit;
81
+ } else if (Object.prototype.hasOwnProperty.call(properties, 'page')
82
+ && Object.prototype.hasOwnProperty.call(properties, 'page_size')) {
83
+ query.page = registryPageFromCursor(cursor, operationID);
84
+ query.page_size = limit;
85
+ } else if (cursor) {
86
+ throw new Error(`${operationID} does not expose a paginated Registry contract.`);
87
+ }
88
+
89
+ const search = flags.route != null ? normalizeRoute(flags.route)
90
+ : flags.title != null ? String(flags.title) : '';
91
+ if (search) {
92
+ const key = flags.route != null && Object.prototype.hasOwnProperty.call(properties, 'route')
93
+ ? 'route'
94
+ : ['search', 'q', 'query'].find((candidate) => Object.prototype.hasOwnProperty.call(properties, candidate));
95
+ if (key) query[key] = search;
96
+ }
97
+ return query;
98
+ }
99
+
100
+ function registryPagination(payload, operationID, query, returned) {
101
+ const serverCursor = nextCursor(payload);
102
+ if (serverCursor != null && serverCursor !== '') {
103
+ return {
104
+ next_cursor: String(serverCursor),
105
+ has_more: true,
106
+ total: reportedTotal(payload),
107
+ };
108
+ }
109
+ if (claimsMorePages(payload)) {
110
+ throw new Error(`${operationID} reported more pages without a cursor.`);
111
+ }
112
+ const total = reportedTotal(payload);
113
+ const page = Number(query.page);
114
+ const pageSize = Number(query.page_size);
115
+ const hasMore = Number.isSafeInteger(page) && Number.isSafeInteger(pageSize)
116
+ && (total != null ? page * pageSize < total : returned === pageSize);
117
+ return {
118
+ next_cursor: hasMore ? registryCursor(operationID, page + 1) : null,
119
+ has_more: hasMore,
120
+ total,
121
+ };
122
+ }
123
+
49
124
  function normalizeMapResourceType(value) {
50
- if (require('./customService').isServiceType(value)) return 'custom_services';
51
125
  return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
52
126
  }
53
127
 
@@ -84,7 +158,6 @@ function resourceMatches(resource, flags = {}) {
84
158
 
85
159
  function resourceKey(resource) {
86
160
  const type = resourceType(resource);
87
- if (type === 'custom_services') return `custom_services:${resource.resource_id}`;
88
161
  return `${canonicalResourceType(type)}:${resource.resource_id}`;
89
162
  }
90
163
 
@@ -130,6 +203,41 @@ async function listMapResources(session, resourceTypes, flags, options = {}) {
130
203
  };
131
204
  }
132
205
 
206
+ async function listRegistryResources(projectDir, config, session, resourceTypes, flags = {}) {
207
+ const { registryRevision, descriptionForRevision } = require('./api');
208
+ if (flags.cursor != null && flags.cursor !== '' && resourceTypes.length !== 1) {
209
+ throw new Error('Map --cursor requires exactly one --type.');
210
+ }
211
+ const groups = await allWithAbort(resourceTypes.map((type) => async (options) => {
212
+ const operationID = LIST_OPERATIONS[type];
213
+ const revision = await registryRevision(session, operationID);
214
+ const description = await descriptionForRevision(projectDir, config, session, operationID, revision);
215
+ const operation = description.operation || {};
216
+ const query = registryQuery(operation, operationID, flags);
217
+ const invoke = {
218
+ operation_id: operationID,
219
+ registry_revision: revision,
220
+ };
221
+ if (Object.keys(query).length) invoke.query = query;
222
+ const payload = await callStructured(session, TOOL_NAMES.apiCall, invoke, options);
223
+ const response = payload && payload.response !== undefined ? payload.response
224
+ : payload && payload.data !== undefined ? payload.data : payload;
225
+ const responseItems = itemsFrom(response);
226
+ const resources = responseItems
227
+ .map((item) => ({ ...item, resource_type: type, resource_id: String(item.resource_id ?? item.id) }))
228
+ .filter((item) => item.resource_id && item.resource_id !== 'undefined')
229
+ .filter((item) => resourceMatches(item, flags));
230
+ return {
231
+ resources,
232
+ pagination: registryPagination(response, operationID, query, responseItems.length),
233
+ };
234
+ }));
235
+ return {
236
+ resources: groups.flatMap((group) => group.resources),
237
+ pagination: Object.fromEntries(groups.map((group, index) => [resourceTypes[index], group.pagination])),
238
+ };
239
+ }
240
+
133
241
  async function listRemoteResourceType(session, resourceType, options = {}) {
134
242
  const resources = [];
135
243
  const seen = new Set();
@@ -161,43 +269,40 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
161
269
  return groups.flat();
162
270
  }
163
271
 
164
- function checkoutChanged(entry) {
165
- if (entry.resource_type === 'custom_services') {
166
- return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
167
- }
168
- return entry.local_hash !== null && entry.local_hash !== entry.base_hash;
169
- }
272
+ function checkoutChanged(entry) { return entry.local_hash !== null && entry.local_hash !== entry.base_hash; }
170
273
 
171
274
  async function mapCommand(projectDir, flags = {}) {
172
275
  const resourceTypes = requestedResourceTypes(flags);
173
276
  const config = loadProjectConfig(projectDir);
174
277
  const searching = flags.route != null || flags.title != null;
175
- const session = await openToolSession(config, [searching ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList]);
176
- const remote = await listMapResources(session, resourceTypes, flags);
278
+ let session;
279
+ let remote;
280
+ try {
281
+ session = await openToolSession(config, [searching ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList]);
282
+ remote = await listMapResources(session, resourceTypes, flags);
283
+ } catch (error) {
284
+ if (!error || error.code !== 'MCP_TOOL_UNAVAILABLE') throw error;
285
+ session = await openToolSession(config, [TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
286
+ remote = await listRegistryResources(projectDir, config, session, resourceTypes, flags);
287
+ }
177
288
  const manifestEntries = Object.values(loadManifest(projectDir).entries);
178
289
  const selected = new Set(remote.resources.map(resourceKey));
179
290
  // A filtered map is a focused inspection, so do not make unrelated local
180
291
  // worktree state dominate its output. The unfiltered command retains the
181
292
  // established full-worktree status behavior.
182
293
  const statusEntries = searching
183
- ? manifestEntries.filter((entry) => entry.resource_type === 'custom_services'
184
- ? selected.has(`custom_services:${entry.resource_id}`)
185
- : selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
294
+ ? manifestEntries.filter((entry) => selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
186
295
  : manifestEntries;
187
- const contentEntries = statusEntries.filter((entry) => entry.resource_type !== 'custom_services');
188
- const serviceIds = statusEntries
189
- .filter((entry) => entry.resource_type === 'custom_services')
190
- .map((entry) => entry.resource_id);
191
296
  const checkouts = (await allWithAbort([
192
297
  async (options) => {
193
- const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
298
+ const contentStatus = await inspectRemoteCheckouts(projectDir, {
194
299
  config,
195
300
  client: session.client,
196
301
  tools: session.tools,
197
302
  signal: options.signal,
198
- entries: contentEntries,
199
- }), customServices.inspectRemote(projectDir, { config, signal: options.signal, ids: serviceIds })]);
200
- return [...contentStatus, ...serviceStatus].map((entry) => ({
303
+ entries: statusEntries,
304
+ });
305
+ return contentStatus.map((entry) => ({
201
306
  ...entry,
202
307
  exists: entry.local_hash !== null,
203
308
  current_hash: entry.local_hash,
@@ -248,6 +353,11 @@ async function mapCommand(projectDir, flags = {}) {
248
353
  log.title('draftgo map');
249
354
  log.info(`Remote resources returned: ${resources.length}`);
250
355
  for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
356
+ if (!flags.summary) {
357
+ for (const resource of resources) {
358
+ log.plain(` ${resourceType(resource)} ${resource.resource_id}: ${resource.title || '-'} route=${resource.route || '-'} code=${resource.code || '-'} slug=${resource.slug || '-'}`);
359
+ }
360
+ }
251
361
  log.info(`Local checkouts: ${checkouts.length}`);
252
362
  if (flags.summary) {
253
363
  for (const [state, count] of [...checkoutStates.entries()].sort()) log.plain(` ${state}: ${count}`);
@@ -273,8 +383,13 @@ module.exports.requestedResourceTypes = requestedResourceTypes;
273
383
  module.exports.listRemoteResources = listRemoteResources;
274
384
  module.exports.listRemoteResourcePage = listRemoteResourcePage;
275
385
  module.exports.listMapResources = listMapResources;
386
+ module.exports.listRegistryResources = listRegistryResources;
276
387
  module.exports.normalizeRoute = normalizeRoute;
277
388
  module.exports.resourceMatches = resourceMatches;
278
389
  module.exports.positiveLimit = positiveLimit;
279
390
  module.exports.reportedTotal = reportedTotal;
391
+ module.exports.registryCursor = registryCursor;
392
+ module.exports.registryPageFromCursor = registryPageFromCursor;
393
+ module.exports.registryQuery = registryQuery;
394
+ module.exports.registryPagination = registryPagination;
280
395
  module.exports.checkoutChanged = checkoutChanged;
@@ -6,23 +6,9 @@ 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|custom-services> <id...>');
9
+ log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
10
10
  return 1;
11
11
  }
12
- if (require('./customService').isServiceType(resourceType)) {
13
- const services = require('../customServices');
14
- const states = await services.inspectRemote(projectDir);
15
- const results = [];
16
- for (const id of ids) {
17
- const state = states.find((entry) => String(entry.resource_id) === String(id));
18
- if (!state || state.state !== 'committed_unrecorded') throw new Error(`custom_services ${id} cannot be reconciled from state ${state?.state || 'missing'}.`);
19
- await services.checkout(projectDir, [id], { force: true });
20
- results.push({ resource_type: 'custom_services', resource_id: String(id), base_revision: state.remote_revision });
21
- }
22
- if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
23
- else results.forEach((result) => log.ok(`Reconciled custom_services ${result.resource_id} -> ${result.base_revision}`));
24
- return 0;
25
- }
26
12
  const results = await reconcileResources(projectDir, resourceType, ids);
27
13
  if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
28
14
  else for (const result of results) {
@@ -3,9 +3,8 @@
3
3
  const log = require('../logger');
4
4
  const { callOperation } = require('./api');
5
5
 
6
- // Role is a permission template. Use `draftgo grant` to bind it to a subject.
7
6
  const OPERATIONS = Object.freeze({
8
- permissions: 'listPermissionCatalog',
7
+ permissions: 'listPermissions',
9
8
  list: 'listRoles',
10
9
  create: 'createRole',
11
10
  get: 'getRole',
@@ -5,30 +5,19 @@ const { all } = require('../targets');
5
5
  const { readInstalledVersion, getPackageVersion } = require('../skill');
6
6
  const { loadProjectConfig } = require('../projectConfig');
7
7
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
8
- const { normalizeDescription } = require('../apiContractCache');
9
8
  const { parseTimeout } = require('../timeout');
10
9
 
11
- const API_KEY_STATUS_OPERATION = 'getCurrentUserAPIKey';
12
-
13
10
  function asObject(value) {
14
11
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
15
12
  }
16
13
 
17
- function scopeFrom(value) {
14
+ function findField(value, keys) {
18
15
  let candidates = [asObject(value)];
19
16
  for (let depth = 0; depth < 4; depth += 1) {
20
17
  const next = [];
21
18
  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']) {
19
+ for (const key of keys) if (candidate[key] != null) return candidate[key];
20
+ for (const key of ['operation_registry', 'registry', 'response', 'data', 'value']) {
32
21
  const nested = asObject(candidate[key]);
33
22
  if (Object.keys(nested).length) next.push(nested);
34
23
  }
@@ -48,30 +37,15 @@ async function connectionDiagnostic(projectDir, options = {}) {
48
37
 
49
38
  const diagnostic = { health: 'unhealthy', connected: false, server: config.server };
50
39
  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);
40
+ const session = await openToolSession(config, [TOOL_NAMES.apiSearch], { timeoutMs: options.timeoutMs });
41
+ const catalog = await callStructured(session, TOOL_NAMES.apiSearch, {
42
+ query: 'system', limit: 1,
43
+ }, { timeoutMs: options.timeoutMs });
56
44
  diagnostic.connected = true;
57
- diagnostic.server_version = project.version || null;
45
+ diagnostic.server_version = asObject(session.initialized.serverInfo).version || null;
58
46
  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' });
47
+ diagnostic.registry_revision = findField(catalog, ['registry_revision']) || null;
48
+ diagnostic.health = 'healthy';
75
49
  return diagnostic;
76
50
  } catch (error) {
77
51
  diagnostic.error = error && error.message || String(error);
@@ -104,9 +78,7 @@ async function status(projectDir, _positional = [], flags = {}) {
104
78
  log.info(`服务端:${connection.server || '(未连接)'}`);
105
79
  if (connection.health === 'healthy') {
106
80
  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}`);
81
+ log.info(`Registry revision:${connection.registry_revision || '-'}`);
110
82
  } else {
111
83
  log.warn(`连接健康:${connection.health}`);
112
84
  if (connection.error) log.dim(` ${connection.error}`);
@@ -122,5 +94,5 @@ async function status(projectDir, _positional = [], flags = {}) {
122
94
  }
123
95
 
124
96
  module.exports = status;
125
- module.exports.scopeFrom = scopeFrom;
97
+ module.exports.findField = findField;
126
98
  module.exports.connectionDiagnostic = connectionDiagnostic;
@@ -2,10 +2,9 @@
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
4
  const log = require('../logger');
5
- const { all, byName, resolveTargets } = require('../targets');
5
+ const { all, resolveTargets } = require('../targets');
6
6
  const { ensureRuntime, writeInstalledVersion, readInstalledVersion, getPackageVersion } = require('../skill');
7
7
  const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
8
- const { detectTargets } = require('../detect');
9
8
  const { readInstallMarker, installRelease } = require('../releaseInstall');
10
9
 
11
10
  const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
@@ -16,6 +15,10 @@ function installedTargetsNotRefreshed(projectDir, refreshedTargets) {
16
15
  !refreshed.has(target.name) && target.status(projectDir).installed);
17
16
  }
18
17
 
18
+ function targetInstalled(projectDir, target) {
19
+ return typeof target.status === 'function' && target.status(projectDir).installed === true;
20
+ }
21
+
19
22
  async function updateCliIfNeeded(projectDir, positional, flags) {
20
23
  if (process.env[REENTRY_FLAG] === '1' || flags['skip-update-check'] || process.env.DRAFTGO_NO_UPDATE_CHECK === '1') {
21
24
  return null;
@@ -76,36 +79,27 @@ async function update(projectDir, positional, flags) {
76
79
  const prev = readInstalledVersion(projectDir);
77
80
  log.info(`当前已装 skill:${prev || '未安装'},CLI 版本:${getPackageVersion()}`);
78
81
 
79
- // Determine which entry files to refresh:
80
- // - no positional refresh installed targets plus project-level tool signals
81
- // - "all" → refresh every known target
82
- // - names → refresh the specified targets
82
+ // Implicit update only refreshes existing Skills. An explicit target is a
83
+ // deliberate request and may install that target when it is missing.
83
84
  let resolved = [];
84
- let unknown = [];
85
-
86
- if (positional.length === 0) {
87
- const byTarget = new Map();
88
- for (const t of all) {
89
- if (t.status(projectDir).installed) byTarget.set(t.name, t);
90
- }
91
- for (const name of detectTargets(projectDir)) {
92
- if (byName[name]) byTarget.set(name, byName[name]);
93
- }
94
- resolved = Array.from(byTarget.values());
85
+ let unknown = [];
86
+
87
+ if (positional.length === 0 || positional.includes('all')) {
88
+ resolved = all.filter((target) => targetInstalled(projectDir, target));
95
89
  if (resolved.length === 0) {
96
- log.warn('未检测到任何已安装或可识别的 AI 工具目标,无可刷新内容。');
97
- log.dim(' 使用 `draftgo update all` 或 `draftgo update <target>` 强制刷新。');
90
+ log.warn('未检测到已安装的 DraftGo Skill,无可刷新内容。');
91
+ log.dim(' 需要新增 AI 工具时,请使用 `draftgo init <target>`。');
98
92
  return 0;
99
93
  } else {
100
94
  log.info(`刷新目标:${resolved.map((r) => r.displayName).join(', ')}`);
101
95
  }
102
96
  } else {
103
97
  ({ resolved, unknown } = resolveTargets(positional));
104
- if (unknown.length) {
105
- log.err(`未知 target:${unknown.join(', ')}`);
106
- return 1;
107
- }
108
- }
98
+ if (unknown.length) {
99
+ log.err(`未知 target:${unknown.join(', ')}`);
100
+ return 1;
101
+ }
102
+ }
109
103
 
110
104
  ensureRuntime(projectDir);
111
105
  log.step('用 CLI 内置版本覆盖各 AI 工具 skill 目录');
@@ -128,18 +128,23 @@ async function verifyPageComponents(projectDir, selected) {
128
128
  async function verify(projectDir, positional = [], flags = {}) {
129
129
  let selected;
130
130
  let remoteMode;
131
+ let visual;
132
+ const url = String(flags.url || '').trim();
131
133
  try {
132
134
  selected = resources(positional);
133
135
  remoteMode = flags.remote ? 'always' : 'never';
136
+ visual = visualRequest(flags, url);
137
+ if (flags.output === 'json' && visual.run) throw new Error('Visual verification does not support --output json; run the explicit visual check separately.');
134
138
  } catch (error) {
135
139
  log.err(error.message);
136
140
  return 1;
137
141
  }
138
142
 
139
- log.title('draftgo verify');
140
- try { await verifyPageComponents(projectDir, selected); }
143
+ if (flags.output !== 'json') log.title('draftgo verify');
144
+ try { if (flags.remote) await verifyPageComponents(projectDir, selected); }
141
145
  catch (error) { log.err(error.message); return 1; }
142
146
  const checkCode = await check(projectDir, [], {
147
+ output: flags.output,
143
148
  strict: flags.strict,
144
149
  remote: remoteMode === 'always',
145
150
  resourceKeys: selected.length
@@ -148,13 +153,9 @@ async function verify(projectDir, positional = [], flags = {}) {
148
153
  });
149
154
  if (checkCode !== 0) return checkCode;
150
155
 
151
- const url = String(flags.url || '').trim();
152
- let visual;
153
- try { visual = visualRequest(flags, url); }
154
- catch (error) { log.err(error.message); return 1; }
155
156
  if (!visual.run) {
156
157
  if (url) log.warn('--url was provided without --screenshot always or --ui always; visual verification was skipped.');
157
- log.ok('Unified verification passed (UI skipped).');
158
+ if (flags.output !== 'json') log.ok('Local verification passed; browser and business interactions were not tested.');
158
159
  return 0;
159
160
  }
160
161
  let targets;
@@ -13,7 +13,7 @@ const {
13
13
  } = require('../worklog');
14
14
 
15
15
  function printUsage() {
16
- log.err('Usage: draftgo work start|add|start-item|complete|show|list <value>');
16
+ log.err('Usage: draftgo work start|add|start-item|wait|complete|show|list <value>');
17
17
  }
18
18
 
19
19
  function output(flags, value) {
@@ -27,7 +27,12 @@ function latestReference(blocks) {
27
27
 
28
28
  function resultFor(blocks, reference) {
29
29
  const resolved = resolveReference(blocks, reference);
30
- return { date: resolved.block.date, ...resolved.entry };
30
+ return { date: resolved.block.date, ...resolved.entry, status: visibleStatus(resolved.entry.status) };
31
+ }
32
+
33
+ function visibleStatus(status) { return status === 'pending' ? 'waiting' : status; }
34
+ function visibleBlocks(blocks) {
35
+ return blocks.map(block => ({ ...block, entries: block.entries.map(entry => ({ ...entry, status: visibleStatus(entry.status) })) }));
31
36
  }
32
37
 
33
38
  function add(projectDir, positional, flags, status) {
@@ -61,14 +66,14 @@ function change(projectDir, positional, flags, status) {
61
66
  }
62
67
 
63
68
  function show(projectDir, flags) {
64
- const blocks = readWorklog(projectDir);
69
+ const blocks = visibleBlocks(readWorklog(projectDir));
65
70
  output(flags, { path: worklogPath(projectDir), blocks });
66
71
  if (flags.output !== 'json') console.log(renderWorklog(blocks) || 'Worklog is empty.');
67
72
  return 0;
68
73
  }
69
74
 
70
75
  function list(projectDir, flags) {
71
- const blocks = readWorklog(projectDir);
76
+ const blocks = visibleBlocks(readWorklog(projectDir));
72
77
  const items = blocks.flatMap((block) => block.entries.map((entry) => ({ date: block.date, ...entry })));
73
78
  output(flags, { path: worklogPath(projectDir), items });
74
79
  if (flags.output !== 'json') items.forEach((item) => log.info(`${item.date}#${item.number} [${item.status}] ${item.title}`));
@@ -78,7 +83,8 @@ function list(projectDir, flags) {
78
83
  async function worklog(projectDir, positional = [], flags = {}) {
79
84
  const operation = positional[0] || 'show';
80
85
  if (operation === 'start') return add(projectDir, positional.slice(1), flags, 'active');
81
- if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'pending');
86
+ if (operation === 'add') return add(projectDir, positional.slice(1), flags, 'waiting');
87
+ if (operation === 'wait') return change(projectDir, positional.slice(1), flags, 'waiting');
82
88
  if (operation === 'start-item' || operation === 'start_item') return change(projectDir, positional.slice(1), flags, 'active');
83
89
  if (operation === 'complete' || operation === 'done') return change(projectDir, positional.slice(1), flags, 'completed');
84
90
  if (operation === 'show') return show(projectDir, flags);
@@ -21,7 +21,8 @@ function isHardContractIncompatibility(error) {
21
21
 
22
22
  function isDangerousOperation(contract) {
23
23
  const operation = contract && contract.operation || contract || {};
24
- return operation.destructive === true || String(operation.risk || '').toLowerCase() === 'high';
24
+ const risk = String(operation.risk || '').toLowerCase();
25
+ return operation.destructive === true || risk === 'high' || risk === 'critical';
25
26
  }
26
27
 
27
28
  function canonicalValue(value) {
@@ -36,9 +37,15 @@ function sameOperationContract(left, right) {
36
37
  return JSON.stringify(canonicalValue(leftOperation)) === JSON.stringify(canonicalValue(rightOperation));
37
38
  }
38
39
 
40
+ function canRetryAfterContractChange(left, right) {
41
+ const operation = right && right.operation || right || {};
42
+ const method = String(operation.method || '').toUpperCase();
43
+ return method === 'GET' || method === 'HEAD' || sameOperationContract(left, right);
44
+ }
45
+
39
46
  function incompatibleOperationError(operationId, error) {
40
47
  const wrapped = new Error(
41
- `DraftGo CLI and server contracts are incompatible for dangerous operation ${operationId}; `
48
+ `DraftGo CLI and server contracts are incompatible for operation ${operationId}; `
42
49
  + 'the call was blocked before retry. Upgrade DraftGo CLI, then describe and confirm the operation again.',
43
50
  );
44
51
  wrapped.code = 'CLI_CONTRACT_INCOMPATIBLE';
@@ -53,5 +60,6 @@ module.exports = {
53
60
  isHardContractIncompatibility,
54
61
  isDangerousOperation,
55
62
  sameOperationContract,
63
+ canRetryAfterContractChange,
56
64
  incompatibleOperationError,
57
65
  };