draftgo-cli 3.0.56 → 4.0.22

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 (96) hide show
  1. package/README.md +169 -297
  2. package/package.json +12 -7
  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/billing.go +596 -0
  6. package/resources/custom-service-sdk/billing_test.go +150 -0
  7. package/resources/custom-service-sdk/go.mod +3 -0
  8. package/resources/custom-service-sdk/manifest.json +72 -0
  9. package/resources/custom-service-sdk/platform.go +360 -0
  10. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  11. package/resources/custom-service-sdk/registration_test.go +39 -0
  12. package/resources/custom-service-sdk/resources.go +246 -0
  13. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  14. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  15. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  16. package/resources/custom-service-sdk/sdk.go +208 -0
  17. package/resources/skill/SKILL.md +36 -87
  18. package/resources/skill/init/SKILL.md +9 -14
  19. package/resources/skill/manifest.json +6 -2
  20. package/resources/skill/references/aihub.md +28 -5
  21. package/resources/skill/references/app-api.md +56 -6
  22. package/resources/skill/references/architecture.md +2 -2
  23. package/resources/skill/references/chat-sdk.md +4 -2
  24. package/resources/skill/references/checkout.md +21 -7
  25. package/resources/skill/references/custom-services.md +124 -222
  26. package/resources/skill/references/data.md +22 -6
  27. package/resources/skill/references/delivery.md +33 -0
  28. package/resources/skill/references/diagnostics.md +51 -0
  29. package/resources/skill/references/frontend.md +93 -499
  30. package/resources/skill/references/mcp.md +65 -101
  31. package/resources/skill/references/methods.md +189 -0
  32. package/resources/skill/references/modules.md +36 -8
  33. package/resources/skill/references/runtime.md +26 -3
  34. package/resources/skill/story/SKILL.md +1 -2
  35. package/src/apiContractCache.js +112 -0
  36. package/src/cli.js +24 -20
  37. package/src/commandRegistry.js +15 -12
  38. package/src/commands/api.js +41 -10
  39. package/src/commands/apiKey.js +34 -0
  40. package/src/commands/capabilities.js +93 -0
  41. package/src/commands/check.js +1 -10
  42. package/src/commands/checkout.js +1 -1
  43. package/src/commands/commit.js +1 -1
  44. package/src/commands/components.js +550 -0
  45. package/src/commands/conflict.js +1 -1
  46. package/src/commands/connect.js +18 -8
  47. package/src/commands/customService.js +22 -8
  48. package/src/commands/dataRange.js +33 -0
  49. package/src/commands/delete.js +34 -46
  50. package/src/commands/deploy.js +1 -1
  51. package/src/commands/diff.js +18 -2
  52. package/src/commands/grant.js +29 -0
  53. package/src/commands/group.js +38 -0
  54. package/src/commands/help.js +80 -51
  55. package/src/commands/init.js +6 -12
  56. package/src/commands/listTargets.js +1 -1
  57. package/src/commands/local.js +2 -6
  58. package/src/commands/map.js +145 -28
  59. package/src/commands/mcp.js +2 -2
  60. package/src/commands/reconcile.js +1 -1
  61. package/src/commands/role.js +32 -0
  62. package/src/commands/space.js +41 -0
  63. package/src/commands/status.js +111 -8
  64. package/src/commands/uninstall.js +3 -3
  65. package/src/commands/update.js +24 -12
  66. package/src/commands/verify.js +118 -21
  67. package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
  68. package/src/commands/worklog.js +90 -0
  69. package/src/consoleEncoding.js +34 -0
  70. package/src/contractCompatibility.js +57 -0
  71. package/src/customServices.js +278 -41
  72. package/src/diffReport.js +106 -0
  73. package/src/index.js +2 -0
  74. package/src/{localdev → localRuntime}/compose.js +14 -17
  75. package/src/{localdev → localRuntime}/detect.js +1 -1
  76. package/src/{localdev → localRuntime}/index.js +22 -23
  77. package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
  78. package/src/{localdev → localRuntime}/services.js +28 -37
  79. package/src/mcp/client.js +11 -2
  80. package/src/mcp/protocol.js +2 -2
  81. package/src/mcp/tools.js +14 -1
  82. package/src/platforms.js +9 -0
  83. package/src/projectConfig.js +8 -4
  84. package/src/releaseInstall.js +105 -0
  85. package/src/{installers/index.js → targets.js} +3 -5
  86. package/src/updateCheck.js +48 -28
  87. package/src/worklog.js +275 -0
  88. package/src/workspaceHealth.js +1 -1
  89. package/src/worktree/backend.js +1 -1
  90. package/src/worktree/index.js +86 -51
  91. package/src/changelog.js +0 -276
  92. package/src/commands/changelog.js +0 -24
  93. package/src/commands/localDev.js +0 -9
  94. package/src/commands/sync.js +0 -46
  95. package/src/commands/task.js +0 -408
  96. package/src/commands/verifyUiCompat.js +0 -16
@@ -4,8 +4,8 @@ const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
5
  const log = require('../logger');
6
6
  const { exists } = require('../fsx');
7
- const { detectDocker } = require('../localdev/detect');
8
- const { runWizard } = require('../localdev');
7
+ const { detectDocker } = require('../localRuntime/detect');
8
+ const { runWizard } = require('../localRuntime');
9
9
 
10
10
  function composeFile(projectDir) {
11
11
  return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
@@ -58,21 +58,17 @@ function local(projectDir, positional, flags = {}) {
58
58
  switch (action) {
59
59
  case 'setup':
60
60
  return runWizard(projectDir, { yes: !!(flags.yes || flags.y) });
61
- case 'up':
62
61
  case 'start':
63
62
  return runCompose(projectDir, ['up', '-d']);
64
- case 'down':
65
63
  case 'stop':
66
64
  return runCompose(projectDir, ['down']);
67
65
  case 'logs':
68
66
  return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
69
67
  case 'status':
70
- case 'ps':
71
68
  return runCompose(projectDir, ['ps']);
72
69
  default:
73
70
  log.err(`未知 local 子命令:${action}`);
74
71
  log.dim(' 可用:draftgo local setup | start | stop | logs | status');
75
- log.dim(' 兼容别名:up | down');
76
72
  return 1;
77
73
  }
78
74
  }
@@ -1,13 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
3
  const log = require('../logger');
6
4
  const { loadProjectConfig } = require('../projectConfig');
7
5
  const { canonicalResourceType } = require('../worktree/types');
8
6
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
7
  const { allWithAbort } = require('../mcp/parallel');
10
8
  const { inspectRemoteCheckouts } = require('../worktree/status');
9
+ const { loadManifest } = require('../worktree/manifest');
11
10
  const customServices = require('../customServices');
12
11
 
13
12
  const REMOTE_RESOURCE_TYPES = Object.freeze({
@@ -42,6 +41,11 @@ function claimsMorePages(value) {
42
41
  && (value.has_more === true || value.hasMore === true);
43
42
  }
44
43
 
44
+ function reportedTotal(value) {
45
+ const total = value && typeof value === 'object' ? value.total : null;
46
+ return Number.isInteger(total) && total >= 0 ? total : null;
47
+ }
48
+
45
49
  function normalizeMapResourceType(value) {
46
50
  if (require('./customService').isServiceType(value)) return 'custom_services';
47
51
  return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
@@ -53,6 +57,79 @@ function requestedResourceTypes(flags = {}) {
53
57
  : [normalizeMapResourceType(flags.type)];
54
58
  }
55
59
 
60
+ function normalizeRoute(value) {
61
+ // DraftGo stores page routes without surrounding slashes. Accept either the
62
+ // stored form or the browser form users naturally paste into the CLI.
63
+ return String(value == null ? '' : value).trim().replace(/^\/+|\/+$/g, '');
64
+ }
65
+
66
+ function positiveLimit(value, fallback = 20) {
67
+ if (value == null) return fallback;
68
+ if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > 100) {
69
+ throw new Error('Map --limit must be an integer between 1 and 100.');
70
+ }
71
+ return Number(value);
72
+ }
73
+
74
+ function resourceType(resource) {
75
+ return resource.resource_type || resource.type || 'unknown';
76
+ }
77
+
78
+ function resourceMatches(resource, flags = {}) {
79
+ const route = flags.route == null ? null : normalizeRoute(flags.route);
80
+ const title = flags.title == null ? null : String(flags.title);
81
+ return (route == null || normalizeRoute(resource.route) === route)
82
+ && (title == null || String(resource.title == null ? '' : resource.title) === title);
83
+ }
84
+
85
+ function resourceKey(resource) {
86
+ const type = resourceType(resource);
87
+ if (type === 'custom_services') return `custom_services:${resource.resource_id}`;
88
+ return `${canonicalResourceType(type)}:${resource.resource_id}`;
89
+ }
90
+
91
+ function paginationFrom(payload, resourceTypeName) {
92
+ const cursor = nextCursor(payload);
93
+ if ((cursor == null || cursor === '') && claimsMorePages(payload)) {
94
+ throw new Error(`DraftGo resource_list (${resourceTypeName}) reported more pages without a cursor.`);
95
+ }
96
+ return {
97
+ next_cursor: cursor == null || cursor === '' ? null : String(cursor),
98
+ has_more: claimsMorePages(payload) || (cursor != null && cursor !== ''),
99
+ total: reportedTotal(payload),
100
+ };
101
+ }
102
+
103
+ async function listRemoteResourcePage(session, resourceTypeName, flags = {}, options = {}) {
104
+ const filtered = flags.route != null || flags.title != null;
105
+ const args = { resource_type: resourceTypeName, limit: positiveLimit(flags.limit) };
106
+ if (flags.cursor != null && flags.cursor !== '') args.cursor = String(flags.cursor);
107
+ if (filtered) {
108
+ // resource_search narrows the remote candidate set; the strict equality
109
+ // checks below keep --route/--title deterministic even when search is fuzzy.
110
+ args.query = flags.route != null ? normalizeRoute(flags.route) : String(flags.title);
111
+ }
112
+ const payload = await callStructured(
113
+ session,
114
+ filtered ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList,
115
+ args,
116
+ options,
117
+ );
118
+ return { resources: itemsFrom(payload).filter((resource) => resourceMatches(resource, flags)), pagination: paginationFrom(payload, resourceTypeName) };
119
+ }
120
+
121
+ async function listMapResources(session, resourceTypes, flags, options = {}) {
122
+ if (flags.cursor != null && flags.cursor !== '' && resourceTypes.length !== 1) {
123
+ throw new Error('Map --cursor requires exactly one --type.');
124
+ }
125
+ const groups = await allWithAbort(resourceTypes.map((resourceTypeName) =>
126
+ (queryOptions) => listRemoteResourcePage(session, resourceTypeName, flags, queryOptions)), options);
127
+ return {
128
+ resources: groups.flatMap((group) => group.resources),
129
+ pagination: Object.fromEntries(groups.map((group, index) => [resourceTypes[index], group.pagination])),
130
+ };
131
+ }
132
+
56
133
  async function listRemoteResourceType(session, resourceType, options = {}) {
57
134
  const resources = [];
58
135
  const seen = new Set();
@@ -84,11 +161,6 @@ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOU
84
161
  return groups.flat();
85
162
  }
86
163
 
87
- function legacyCaches(projectDir) {
88
- return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
89
- .filter((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
90
- }
91
-
92
164
  function checkoutChanged(entry) {
93
165
  if (entry.resource_type === 'custom_services') {
94
166
  return ['local_modified', 'diverged', 'committed_unrecorded'].includes(entry.state);
@@ -99,19 +171,32 @@ function checkoutChanged(entry) {
99
171
  async function mapCommand(projectDir, flags = {}) {
100
172
  const resourceTypes = requestedResourceTypes(flags);
101
173
  const config = loadProjectConfig(projectDir);
102
- const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList]);
103
- const [remote, checkouts] = await allWithAbort([
104
- (options) => allWithAbort([
105
- (options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
106
- (options) => listRemoteResources(session, resourceTypes, options),
107
- ], options),
174
+ 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);
177
+ const manifestEntries = Object.values(loadManifest(projectDir).entries);
178
+ const selected = new Set(remote.resources.map(resourceKey));
179
+ // A filtered map is a focused inspection, so do not make unrelated local
180
+ // worktree state dominate its output. The unfiltered command retains the
181
+ // established full-worktree status behavior.
182
+ 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}`))
186
+ : 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
+ const checkouts = (await allWithAbort([
108
192
  async (options) => {
109
193
  const [contentStatus, serviceStatus] = await Promise.all([inspectRemoteCheckouts(projectDir, {
110
194
  config,
111
195
  client: session.client,
112
196
  tools: session.tools,
113
197
  signal: options.signal,
114
- }), customServices.inspectRemote(projectDir, { config, signal: options.signal })]);
198
+ entries: contentEntries,
199
+ }), customServices.inspectRemote(projectDir, { config, signal: options.signal, ids: serviceIds })]);
115
200
  return [...contentStatus, ...serviceStatus].map((entry) => ({
116
201
  ...entry,
117
202
  exists: entry.local_hash !== null,
@@ -119,29 +204,58 @@ async function mapCommand(projectDir, flags = {}) {
119
204
  changed: checkoutChanged(entry),
120
205
  }));
121
206
  },
122
- ]);
123
- const [overview, resources] = remote;
207
+ ]))[0];
208
+ const resources = remote.resources;
209
+ const counts = new Map();
210
+ for (const resource of resources) {
211
+ const type = resourceType(resource);
212
+ counts.set(type, (counts.get(type) || 0) + 1);
213
+ }
214
+ const checkoutStates = new Map();
215
+ for (const entry of checkouts) {
216
+ const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
217
+ checkoutStates.set(state, (checkoutStates.get(state) || 0) + 1);
218
+ }
124
219
  const result = {
125
220
  server: config.server,
126
- overview,
127
221
  resources,
128
222
  checkouts,
129
- legacy_cache: { ignored: true, detected: legacyCaches(projectDir) },
223
+ pagination: remote.pagination,
224
+ };
225
+
226
+ const summary = {
227
+ server: config.server,
228
+ summary: {
229
+ resources: {
230
+ returned: resources.length,
231
+ matched: resources.length,
232
+ returned_by_type: Object.fromEntries([...counts.entries()].sort()),
233
+ // This is the server's fuzzy-search/list candidate count before the
234
+ // client-side exact route/title check. It is null when not reported.
235
+ candidate_total_by_type: Object.fromEntries(Object.entries(remote.pagination)
236
+ .map(([type, page]) => [type, page.total])),
237
+ },
238
+ checkouts: { total: checkouts.length, by_state: Object.fromEntries([...checkoutStates.entries()].sort()) },
239
+ truncated: Object.values(remote.pagination).some((page) => page.has_more),
240
+ pagination: remote.pagination,
241
+ },
130
242
  };
131
243
 
132
244
  if (flags.output === 'json') {
133
- console.log(JSON.stringify(result, null, 2));
245
+ console.log(JSON.stringify(flags.summary ? summary : result, null, 2));
134
246
  return 0;
135
247
  }
136
248
  log.title('draftgo map');
137
- log.info(`Remote resources: ${resources.length}`);
138
- const counts = new Map();
139
- for (const resource of resources) {
140
- const type = resource.resource_type || resource.type || 'unknown';
141
- counts.set(type, (counts.get(type) || 0) + 1);
142
- }
249
+ log.info(`Remote resources returned: ${resources.length}`);
143
250
  for (const [type, count] of [...counts.entries()].sort()) log.plain(` ${type}: ${count}`);
144
251
  log.info(`Local checkouts: ${checkouts.length}`);
252
+ if (flags.summary) {
253
+ for (const [state, count] of [...checkoutStates.entries()].sort()) log.plain(` ${state}: ${count}`);
254
+ for (const [type, page] of Object.entries(remote.pagination)) {
255
+ if (page.has_more) log.plain(` ${type}: more results; use --type ${type} --cursor ${page.next_cursor}`);
256
+ }
257
+ return 0;
258
+ }
145
259
  for (const entry of checkouts) {
146
260
  const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
147
261
  log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
@@ -149,9 +263,6 @@ async function mapCommand(projectDir, flags = {}) {
149
263
  + `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
150
264
  + `(${entry.local_path})`);
151
265
  }
152
- if (result.legacy_cache.detected.length) {
153
- log.warn(`Ignored legacy cache indexes: ${result.legacy_cache.detected.join(', ')}`);
154
- }
155
266
  return 0;
156
267
  }
157
268
 
@@ -160,4 +271,10 @@ module.exports.itemsFrom = itemsFrom;
160
271
  module.exports.normalizeMapResourceType = normalizeMapResourceType;
161
272
  module.exports.requestedResourceTypes = requestedResourceTypes;
162
273
  module.exports.listRemoteResources = listRemoteResources;
274
+ module.exports.listRemoteResourcePage = listRemoteResourcePage;
275
+ module.exports.listMapResources = listMapResources;
276
+ module.exports.normalizeRoute = normalizeRoute;
277
+ module.exports.resourceMatches = resourceMatches;
278
+ module.exports.positiveLimit = positiveLimit;
279
+ module.exports.reportedTotal = reportedTotal;
163
280
  module.exports.checkoutChanged = checkoutChanged;
@@ -17,7 +17,7 @@ function classifyMcpFailure(error) {
17
17
  const message = String(error && error.message || error || '');
18
18
  const code = error && error.code;
19
19
  const status = error && error.status;
20
- if (status === 401 || status === 403) return 'SAT authentication or authorization failed.';
20
+ if (status === 401 || status === 403) return 'API Key authentication or authorization failed.';
21
21
  if (/session|uninitialized|initialize first|not initialized/i.test(message)
22
22
  || ['SESSION_EXPIRED', 'MCP_SESSION_EXPIRED', -32002].includes(code)) {
23
23
  return 'The MCP session was lost or the server rejected the initialized state; check sticky sessions and service restarts.';
@@ -79,7 +79,7 @@ function status(projectDir, positional = [], flags = {}) {
79
79
  let exitCode = 0;
80
80
  try {
81
81
  const config = loadProjectConfig(projectDir);
82
- log.plain(` project configured (${config.server}; SAT present)`);
82
+ log.plain(` project configured (${config.server}; API Key present)`);
83
83
  } catch (error) {
84
84
  log.plain(` project not ready (${error.message})`);
85
85
  exitCode = 1;
@@ -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;
@@ -1,23 +1,126 @@
1
1
  'use strict';
2
2
 
3
3
  const log = require('../logger');
4
- const { all } = require('../installers');
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;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const log = require('../logger');
4
- const { all, resolveTargets } = require('../installers');
4
+ const { all, resolveTargets } = require('../targets');
5
5
  const { removePath } = require('../fsx');
6
6
  const { dgDir } = require('../paths');
7
7
 
@@ -38,10 +38,10 @@ async function uninstall(projectDir, positional, flags) {
38
38
 
39
39
  // Each AI-tool skill directory is self-contained, so per-target uninstall
40
40
  // already wiped its content above. Only --purge nukes runtime data under
41
- // .draftgo/ (config/changelog/Task/lessons/cache etc.).
41
+ // .draftgo/ (config/worklog/lessons/cache etc.).
42
42
  if (flags.purge) {
43
43
  if (removePath(dgDir(projectDir))) {
44
- log.warn('已使用 --purge:.draftgo/(含 config/changelog/lessons/Task 等运行时数据)已删除。');
44
+ log.warn('已使用 --purge:.draftgo/(含 config/worklog/lessons 等运行时数据)已删除。');
45
45
  }
46
46
  }
47
47
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
4
  const log = require('../logger');
5
- const { all, byName, resolveTargets } = require('../installers');
5
+ 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,