draftgo-cli 1.0.4

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/bin/draftgo.js +9 -0
  4. package/package.json +70 -0
  5. package/resources/project-design/README.md +42 -0
  6. package/resources/skill/SKILL.md +62 -0
  7. package/resources/skill/init/SKILL.md +41 -0
  8. package/resources/skill/manifest.json +35 -0
  9. package/resources/skill/references/ai.md +41 -0
  10. package/resources/skill/references/app-api.md +97 -0
  11. package/resources/skill/references/architecture.md +13 -0
  12. package/resources/skill/references/chat-sdk.md +205 -0
  13. package/resources/skill/references/checkout.md +140 -0
  14. package/resources/skill/references/data.md +49 -0
  15. package/resources/skill/references/db-relations.md +29 -0
  16. package/resources/skill/references/delivery.md +33 -0
  17. package/resources/skill/references/development.md +41 -0
  18. package/resources/skill/references/diagnostics.md +50 -0
  19. package/resources/skill/references/frontend.md +158 -0
  20. package/resources/skill/references/mcp.md +110 -0
  21. package/resources/skill/references/methods.md +143 -0
  22. package/resources/skill/references/modules.md +75 -0
  23. package/resources/skill/references/runtime.md +109 -0
  24. package/resources/skill/references/services.md +32 -0
  25. package/src/apiContractCache.js +120 -0
  26. package/src/cli.js +100 -0
  27. package/src/commandRegistry.js +46 -0
  28. package/src/commands/api.js +244 -0
  29. package/src/commands/apiKey.js +30 -0
  30. package/src/commands/autoPush.js +36 -0
  31. package/src/commands/capabilities.js +100 -0
  32. package/src/commands/check.js +82 -0
  33. package/src/commands/checkout.js +18 -0
  34. package/src/commands/clean.js +72 -0
  35. package/src/commands/commit.js +47 -0
  36. package/src/commands/components.js +554 -0
  37. package/src/commands/conflict.js +30 -0
  38. package/src/commands/conflicts.js +16 -0
  39. package/src/commands/connect.js +91 -0
  40. package/src/commands/delete.js +95 -0
  41. package/src/commands/deploy.js +77 -0
  42. package/src/commands/diff.js +39 -0
  43. package/src/commands/group.js +37 -0
  44. package/src/commands/help.js +190 -0
  45. package/src/commands/init.js +126 -0
  46. package/src/commands/listTargets.js +13 -0
  47. package/src/commands/local.js +79 -0
  48. package/src/commands/map.js +395 -0
  49. package/src/commands/mcp.js +150 -0
  50. package/src/commands/reconcile.js +20 -0
  51. package/src/commands/role.js +31 -0
  52. package/src/commands/status.js +98 -0
  53. package/src/commands/uninstall.js +52 -0
  54. package/src/commands/update.js +79 -0
  55. package/src/commands/verify.js +188 -0
  56. package/src/commands/visualVerify.js +281 -0
  57. package/src/commands/worklog.js +117 -0
  58. package/src/consoleEncoding.js +34 -0
  59. package/src/contractCompatibility.js +65 -0
  60. package/src/detect.js +25 -0
  61. package/src/diffReport.js +106 -0
  62. package/src/fsx.js +67 -0
  63. package/src/index.js +46 -0
  64. package/src/localRuntime/compose.js +119 -0
  65. package/src/localRuntime/detect.js +77 -0
  66. package/src/localRuntime/index.js +211 -0
  67. package/src/localRuntime/mysqlClient.js +155 -0
  68. package/src/localRuntime/services.js +117 -0
  69. package/src/logger.js +37 -0
  70. package/src/mcp/client.js +558 -0
  71. package/src/mcp/hosts.js +520 -0
  72. package/src/mcp/parallel.js +54 -0
  73. package/src/mcp/protocol.js +223 -0
  74. package/src/mcp/stdio.js +300 -0
  75. package/src/mcp/tools.js +51 -0
  76. package/src/paths.js +32 -0
  77. package/src/platforms.js +110 -0
  78. package/src/projectConfig.js +139 -0
  79. package/src/projectDesign.js +19 -0
  80. package/src/projectHealth.js +33 -0
  81. package/src/projectMap.js +220 -0
  82. package/src/prompt.js +94 -0
  83. package/src/releaseInstall.js +105 -0
  84. package/src/runtimeFiles.js +45 -0
  85. package/src/skill.js +295 -0
  86. package/src/targets.js +43 -0
  87. package/src/timeout.js +18 -0
  88. package/src/updateCheck.js +100 -0
  89. package/src/worklog.js +276 -0
  90. package/src/worktree/backend.js +438 -0
  91. package/src/worktree/errors.js +28 -0
  92. package/src/worktree/index.js +751 -0
  93. package/src/worktree/inlineScripts.js +99 -0
  94. package/src/worktree/locks.js +52 -0
  95. package/src/worktree/manifest.js +89 -0
  96. package/src/worktree/status.js +124 -0
  97. package/src/worktree/streams.js +200 -0
  98. package/src/worktree/types.js +103 -0
  99. package/src/worktree/validate.js +37 -0
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const log = require('../logger');
6
+ const { exists } = require('../fsx');
7
+ const { detectDocker } = require('../localRuntime/detect');
8
+ const { runWizard } = require('../localRuntime');
9
+
10
+ function composeFile(projectDir) {
11
+ return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
12
+ }
13
+
14
+ function runCompose(projectDir, args, stdio = 'inherit') {
15
+ const docker = detectDocker();
16
+ if (!docker.ok) {
17
+ log.err(docker.reason === 'compose-missing'
18
+ ? '检测到 Docker,但缺少 compose 插件。'
19
+ : '未检测到可用 Docker。');
20
+ return 1;
21
+ }
22
+
23
+ const file = composeFile(projectDir);
24
+ if (!exists(file)) {
25
+ log.err('未找到 .draftgo/docker/docker-compose.yaml。');
26
+ log.dim(' 请先运行 `draftgo local setup` 生成本地 DraftGo stack。');
27
+ return 1;
28
+ }
29
+
30
+ let result;
31
+ try {
32
+ result = spawnSync(docker.composeCmd, [...docker.composeArgs, '-f', file, ...args], {
33
+ cwd: path.dirname(file),
34
+ stdio,
35
+ shell: false,
36
+ });
37
+ } catch (error) {
38
+ log.err(`Failed to start Docker Compose: ${error.message}`);
39
+ return 1;
40
+ }
41
+
42
+ if (result.error) {
43
+ log.err(`Failed to start Docker Compose: ${result.error.message}`);
44
+ return 1;
45
+ }
46
+ if (!Number.isInteger(result.status)) {
47
+ log.err(result.signal
48
+ ? `Docker Compose terminated by signal ${result.signal}.`
49
+ : 'Docker Compose did not report an exit status.');
50
+ return 1;
51
+ }
52
+ return result.status;
53
+ }
54
+
55
+ function local(projectDir, positional, flags = {}) {
56
+ const action = positional[0] || 'status';
57
+
58
+ switch (action) {
59
+ case 'setup':
60
+ return runWizard(projectDir, {
61
+ yes: !!(flags.yes || flags.y),
62
+ ...(flags.params ? { params: flags.params } : {}),
63
+ });
64
+ case 'start':
65
+ return runCompose(projectDir, ['up', '-d']);
66
+ case 'stop':
67
+ return runCompose(projectDir, ['down']);
68
+ case 'logs':
69
+ return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
70
+ case 'status':
71
+ return runCompose(projectDir, ['ps']);
72
+ default:
73
+ log.err(`未知 local 子命令:${action}`);
74
+ log.dim(' 可用:draftgo local setup | start | stop | logs | status');
75
+ return 1;
76
+ }
77
+ }
78
+
79
+ module.exports = local;
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { loadProjectConfig } = require('../projectConfig');
5
+ const { canonicalResourceType } = require('../worktree/types');
6
+ const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
7
+ const { allWithAbort } = require('../mcp/parallel');
8
+ const { inspectRemoteCheckouts } = require('../worktree/status');
9
+ const { loadManifest } = require('../worktree/manifest');
10
+
11
+ const REMOTE_RESOURCE_TYPES = Object.freeze({
12
+ pages: 'pages',
13
+ navigations: 'navigations',
14
+ docs: 'docs/articles',
15
+ });
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
+ });
22
+
23
+ function itemsFrom(value) {
24
+ if (Array.isArray(value)) return value;
25
+ if (!value || typeof value !== 'object') return [];
26
+ if (Array.isArray(value.items)) return value.items;
27
+ if (Array.isArray(value.resources)) return value.resources;
28
+ if (value.data) return itemsFrom(value.data);
29
+ return [];
30
+ }
31
+
32
+ function nextCursor(value) {
33
+ if (!value || typeof value !== 'object') return null;
34
+ if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) return value.next_cursor;
35
+ if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) return value.nextCursor;
36
+ return (value.has_more === true || value.hasMore === true)
37
+ && Object.prototype.hasOwnProperty.call(value, 'cursor')
38
+ ? value.cursor
39
+ : null;
40
+ }
41
+
42
+ function claimsMorePages(value) {
43
+ return !!value && typeof value === 'object'
44
+ && (value.has_more === true || value.hasMore === true);
45
+ }
46
+
47
+ function reportedTotal(value) {
48
+ const total = value && typeof value === 'object' ? value.total : null;
49
+ return Number.isInteger(total) && total >= 0 ? total : null;
50
+ }
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
+
124
+ function normalizeMapResourceType(value) {
125
+ return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
126
+ }
127
+
128
+ function requestedResourceTypes(flags = {}) {
129
+ return flags.type == null
130
+ ? DEFAULT_REMOTE_RESOURCE_TYPES
131
+ : [normalizeMapResourceType(flags.type)];
132
+ }
133
+
134
+ function normalizeRoute(value) {
135
+ // DraftGo stores page routes without surrounding slashes. Accept either the
136
+ // stored form or the browser form users naturally paste into the CLI.
137
+ return String(value == null ? '' : value).trim().replace(/^\/+|\/+$/g, '');
138
+ }
139
+
140
+ function positiveLimit(value, fallback = 20) {
141
+ if (value == null) return fallback;
142
+ if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > 100) {
143
+ throw new Error('Map --limit must be an integer between 1 and 100.');
144
+ }
145
+ return Number(value);
146
+ }
147
+
148
+ function resourceType(resource) {
149
+ return resource.resource_type || resource.type || 'unknown';
150
+ }
151
+
152
+ function resourceMatches(resource, flags = {}) {
153
+ const route = flags.route == null ? null : normalizeRoute(flags.route);
154
+ const title = flags.title == null ? null : String(flags.title);
155
+ return (route == null || normalizeRoute(resource.route) === route)
156
+ && (title == null || String(resource.title == null ? '' : resource.title) === title);
157
+ }
158
+
159
+ function resourceKey(resource) {
160
+ const type = resourceType(resource);
161
+ return `${canonicalResourceType(type)}:${resource.resource_id}`;
162
+ }
163
+
164
+ function paginationFrom(payload, resourceTypeName) {
165
+ const cursor = nextCursor(payload);
166
+ if ((cursor == null || cursor === '') && claimsMorePages(payload)) {
167
+ throw new Error(`DraftGo resource_list (${resourceTypeName}) reported more pages without a cursor.`);
168
+ }
169
+ return {
170
+ next_cursor: cursor == null || cursor === '' ? null : String(cursor),
171
+ has_more: claimsMorePages(payload) || (cursor != null && cursor !== ''),
172
+ total: reportedTotal(payload),
173
+ };
174
+ }
175
+
176
+ async function listRemoteResourcePage(session, resourceTypeName, flags = {}, options = {}) {
177
+ const filtered = flags.route != null || flags.title != null;
178
+ const args = { resource_type: resourceTypeName, limit: positiveLimit(flags.limit) };
179
+ if (flags.cursor != null && flags.cursor !== '') args.cursor = String(flags.cursor);
180
+ if (filtered) {
181
+ // resource_search narrows the remote candidate set; the strict equality
182
+ // checks below keep --route/--title deterministic even when search is fuzzy.
183
+ args.query = flags.route != null ? normalizeRoute(flags.route) : String(flags.title);
184
+ }
185
+ const payload = await callStructured(
186
+ session,
187
+ filtered ? TOOL_NAMES.resourceSearch : TOOL_NAMES.resourceList,
188
+ args,
189
+ options,
190
+ );
191
+ return { resources: itemsFrom(payload).filter((resource) => resourceMatches(resource, flags)), pagination: paginationFrom(payload, resourceTypeName) };
192
+ }
193
+
194
+ async function listMapResources(session, resourceTypes, flags, options = {}) {
195
+ if (flags.cursor != null && flags.cursor !== '' && resourceTypes.length !== 1) {
196
+ throw new Error('Map --cursor requires exactly one --type.');
197
+ }
198
+ const groups = await allWithAbort(resourceTypes.map((resourceTypeName) =>
199
+ (queryOptions) => listRemoteResourcePage(session, resourceTypeName, flags, queryOptions)), options);
200
+ return {
201
+ resources: groups.flatMap((group) => group.resources),
202
+ pagination: Object.fromEntries(groups.map((group, index) => [resourceTypes[index], group.pagination])),
203
+ };
204
+ }
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
+
241
+ async function listRemoteResourceType(session, resourceType, options = {}) {
242
+ const resources = [];
243
+ const seen = new Set();
244
+ let cursor = null;
245
+ for (let page = 0; page < 100; page += 1) {
246
+ const args = { resource_type: resourceType, limit: 100 };
247
+ if (cursor != null && cursor !== '') args.cursor = cursor;
248
+ const payload = await callStructured(session, TOOL_NAMES.resourceList, args, options);
249
+ resources.push(...itemsFrom(payload));
250
+ cursor = nextCursor(payload);
251
+ if (cursor == null || cursor === '') {
252
+ if (claimsMorePages(payload)) {
253
+ throw new Error(`DraftGo resource_list (${resourceType}) reported more pages without a cursor.`);
254
+ }
255
+ return resources;
256
+ }
257
+ const cursorKey = String(cursor);
258
+ if (seen.has(cursorKey)) {
259
+ throw new Error(`DraftGo resource_list (${resourceType}) repeated a cursor.`);
260
+ }
261
+ seen.add(cursorKey);
262
+ }
263
+ throw new Error(`DraftGo resource_list (${resourceType}) exceeded 100 pages.`);
264
+ }
265
+
266
+ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOURCE_TYPES, options = {}) {
267
+ const groups = await allWithAbort(resourceTypes.map((resourceType) =>
268
+ (queryOptions) => listRemoteResourceType(session, resourceType, queryOptions)), options);
269
+ return groups.flat();
270
+ }
271
+
272
+ function checkoutChanged(entry) { return entry.local_hash !== null && entry.local_hash !== entry.base_hash; }
273
+
274
+ async function mapCommand(projectDir, flags = {}) {
275
+ const resourceTypes = requestedResourceTypes(flags);
276
+ const config = loadProjectConfig(projectDir);
277
+ const searching = flags.route != null || flags.title != null;
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
+ }
288
+ const manifestEntries = Object.values(loadManifest(projectDir).entries);
289
+ const selected = new Set(remote.resources.map(resourceKey));
290
+ // A filtered map is a focused inspection, so do not make unrelated local
291
+ // worktree state dominate its output. The unfiltered command retains the
292
+ // established full-worktree status behavior.
293
+ const statusEntries = searching
294
+ ? manifestEntries.filter((entry) => selected.has(`${canonicalResourceType(entry.resource_type)}:${entry.resource_id}`))
295
+ : manifestEntries;
296
+ const checkouts = (await allWithAbort([
297
+ async (options) => {
298
+ const contentStatus = await inspectRemoteCheckouts(projectDir, {
299
+ config,
300
+ client: session.client,
301
+ tools: session.tools,
302
+ signal: options.signal,
303
+ entries: statusEntries,
304
+ });
305
+ return contentStatus.map((entry) => ({
306
+ ...entry,
307
+ exists: entry.local_hash !== null,
308
+ current_hash: entry.local_hash,
309
+ changed: checkoutChanged(entry),
310
+ }));
311
+ },
312
+ ]))[0];
313
+ const resources = remote.resources;
314
+ const counts = new Map();
315
+ for (const resource of resources) {
316
+ const type = resourceType(resource);
317
+ counts.set(type, (counts.get(type) || 0) + 1);
318
+ }
319
+ const checkoutStates = new Map();
320
+ for (const entry of checkouts) {
321
+ const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
322
+ checkoutStates.set(state, (checkoutStates.get(state) || 0) + 1);
323
+ }
324
+ const result = {
325
+ server: config.server,
326
+ resources,
327
+ checkouts,
328
+ pagination: remote.pagination,
329
+ };
330
+
331
+ const summary = {
332
+ server: config.server,
333
+ summary: {
334
+ resources: {
335
+ returned: resources.length,
336
+ matched: resources.length,
337
+ returned_by_type: Object.fromEntries([...counts.entries()].sort()),
338
+ // This is the server's fuzzy-search/list candidate count before the
339
+ // client-side exact route/title check. It is null when not reported.
340
+ candidate_total_by_type: Object.fromEntries(Object.entries(remote.pagination)
341
+ .map(([type, page]) => [type, page.total])),
342
+ },
343
+ checkouts: { total: checkouts.length, by_state: Object.fromEntries([...checkoutStates.entries()].sort()) },
344
+ truncated: Object.values(remote.pagination).some((page) => page.has_more),
345
+ pagination: remote.pagination,
346
+ },
347
+ };
348
+
349
+ if (flags.output === 'json') {
350
+ console.log(JSON.stringify(flags.summary ? summary : result, null, 2));
351
+ return 0;
352
+ }
353
+ log.title('draftgo map');
354
+ log.info(`Remote resources returned: ${resources.length}`);
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
+ }
361
+ log.info(`Local checkouts: ${checkouts.length}`);
362
+ if (flags.summary) {
363
+ for (const [state, count] of [...checkoutStates.entries()].sort()) log.plain(` ${state}: ${count}`);
364
+ for (const [type, page] of Object.entries(remote.pagination)) {
365
+ if (page.has_more) log.plain(` ${type}: more results; use --type ${type} --cursor ${page.next_cursor}`);
366
+ }
367
+ return 0;
368
+ }
369
+ for (const entry of checkouts) {
370
+ const state = entry.state || (!entry.exists ? 'missing' : entry.changed ? 'modified' : 'clean');
371
+ log.plain(` ${entry.resource_type} ${entry.resource_id}: ${state}`
372
+ + ` local=${entry.local_hash || entry.current_hash || '-'} base=${entry.manifest_hash || entry.base_hash || '-'} remote=${entry.remote_hash || '-'} `
373
+ + `version=${entry.manifest_version ?? '-'}->${entry.remote_version ?? '-'} `
374
+ + `(${entry.local_path})`);
375
+ }
376
+ return 0;
377
+ }
378
+
379
+ module.exports = mapCommand;
380
+ module.exports.itemsFrom = itemsFrom;
381
+ module.exports.normalizeMapResourceType = normalizeMapResourceType;
382
+ module.exports.requestedResourceTypes = requestedResourceTypes;
383
+ module.exports.listRemoteResources = listRemoteResources;
384
+ module.exports.listRemoteResourcePage = listRemoteResourcePage;
385
+ module.exports.listMapResources = listMapResources;
386
+ module.exports.listRegistryResources = listRegistryResources;
387
+ module.exports.normalizeRoute = normalizeRoute;
388
+ module.exports.resourceMatches = resourceMatches;
389
+ module.exports.positiveLimit = positiveLimit;
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;
395
+ module.exports.checkoutChanged = checkoutChanged;
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { loadProjectConfig } = require('../projectConfig');
5
+ const { testConnection } = require('../mcp/client');
6
+ const { redactText } = require('../mcp/protocol');
7
+ const {
8
+ HOSTS,
9
+ detectHostTargets,
10
+ setupHosts,
11
+ statusHosts,
12
+ } = require('../mcp/hosts');
13
+ const { serveStdio } = require('../mcp/stdio');
14
+ const { parseTimeout } = require('../timeout');
15
+
16
+ function classifyMcpFailure(error) {
17
+ const message = String(error && error.message || error || '');
18
+ const code = error && error.code;
19
+ const status = error && error.status;
20
+ if (status === 401 || status === 403) return 'API Key authentication or authorization failed.';
21
+ if (/session|uninitialized|initialize first|not initialized/i.test(message)
22
+ || ['SESSION_EXPIRED', 'MCP_SESSION_EXPIRED', -32002].includes(code)) {
23
+ return 'The MCP session was lost or the server rejected the initialized state; check sticky sessions and service restarts.';
24
+ }
25
+ if (status >= 500) return 'The MCP service returned a server error; inspect the service request ID and logs.';
26
+ if (/tools\/call|tool/i.test(message)) return 'Tool invocation failed after protocol setup; inspect the tool contract and server handler.';
27
+ return 'Connection or MCP protocol negotiation failed.';
28
+ }
29
+
30
+ function targetArgs(positional, flags) {
31
+ const values = positional.slice();
32
+ if (flags.target) values.push(...String(flags.target).split(','));
33
+ return values.map((value) => String(value).trim()).filter(Boolean);
34
+ }
35
+
36
+ function printUsage() {
37
+ log.plain('Usage:');
38
+ log.plain(' draftgo mcp setup [target...]');
39
+ log.plain(' draftgo mcp status [target...]');
40
+ log.plain(' draftgo mcp test');
41
+ log.plain(' draftgo mcp serve');
42
+ log.dim(` targets: ${HOSTS.map((host) => host.name).join(', ')}`);
43
+ }
44
+
45
+ function setup(projectDir, positional = [], flags = {}) {
46
+ const targets = targetArgs(positional, flags);
47
+ const detected = targets.length ? null : detectHostTargets(projectDir);
48
+ if (!targets.length && detected.length === 0) {
49
+ log.err('No supported AI host was detected in this project.');
50
+ log.dim(' Run `draftgo mcp setup <target>` to select one explicitly.');
51
+ return 1;
52
+ }
53
+
54
+ const results = setupHosts(projectDir, targets.length ? targets : detected.map((host) => host.name));
55
+ let configured = 0;
56
+ let unsupported = 0;
57
+ for (const result of results) {
58
+ if (!result.supported) {
59
+ unsupported += 1;
60
+ log.warn(`${result.host.displayName}: ${result.reason}`);
61
+ continue;
62
+ }
63
+ configured += 1;
64
+ if (result.changed) log.ok(`${result.host.displayName}: ${result.host.path}`);
65
+ else log.info(`${result.host.displayName}: already configured`);
66
+ }
67
+ if (!configured && unsupported) return 1;
68
+ if (!configured) {
69
+ log.err('No MCP host configuration was written.');
70
+ return 1;
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ function status(projectDir, positional = [], flags = {}) {
76
+ const targets = targetArgs(positional, flags);
77
+ const results = statusHosts(projectDir, targets);
78
+ log.title('draftgo mcp status');
79
+ let exitCode = 0;
80
+ try {
81
+ const config = loadProjectConfig(projectDir);
82
+ log.plain(` project configured (${config.server}; API Key present)`);
83
+ } catch (error) {
84
+ log.plain(` project not ready (${error.message})`);
85
+ exitCode = 1;
86
+ }
87
+ for (const result of results) {
88
+ if (!result.supported) {
89
+ log.plain(` - ${result.host.name.padEnd(12)} unsupported`);
90
+ } else if (result.configured && result.secure) {
91
+ log.plain(` OK ${result.host.name.padEnd(12)} ${result.host.path}`);
92
+ } else if (result.error) {
93
+ log.plain(` x ${result.host.name.padEnd(12)} invalid config`);
94
+ exitCode = 1;
95
+ } else {
96
+ log.plain(` - ${result.host.name.padEnd(12)} not configured`);
97
+ }
98
+ }
99
+ return exitCode;
100
+ }
101
+
102
+ async function test(projectDir, _positional = [], flags = {}) {
103
+ let config;
104
+ const stages = [];
105
+ try {
106
+ config = loadProjectConfig(projectDir);
107
+ const result = await testConnection(config, {
108
+ timeoutMs: parseTimeout(flags.timeout),
109
+ onStage(stage, status) {
110
+ stages.push({ stage, status });
111
+ if (status === 'succeeded') log.ok(`MCP ${stage} succeeded.`);
112
+ else if (status === 'failed') log.err(`MCP ${stage} failed.`);
113
+ else log.step(`MCP ${stage}...`);
114
+ },
115
+ });
116
+ log.ok(`MCP diagnostic completed: ${result.testedCalls.length} tools/call checks passed.`);
117
+ log.dim(` protocol: ${result.protocolVersion}`);
118
+ return 0;
119
+ } catch (error) {
120
+ const token = config && String(config.token || config.sat || '');
121
+ log.err(redactText(error && error.message ? error.message : error, [token]));
122
+ const last = stages[stages.length - 1];
123
+ if (last) log.info(`MCP diagnostic stopped at ${last.stage} (${last.status}).`);
124
+ log.info(classifyMcpFailure(error));
125
+ return 1;
126
+ }
127
+ }
128
+
129
+ async function serve(projectDir, _positional = [], _flags = {}) {
130
+ return serveStdio(projectDir);
131
+ }
132
+
133
+ async function mcp(projectDir, positional = [], flags = {}) {
134
+ const [action, ...rest] = positional;
135
+ if (action === 'setup') return setup(projectDir, rest, flags);
136
+ if (action === 'status') return status(projectDir, rest, flags);
137
+ if (action === 'test') return test(projectDir, rest, flags);
138
+ if (action === 'serve') return serve(projectDir, rest, flags);
139
+ printUsage();
140
+ return 1;
141
+ }
142
+
143
+ mcp.setup = setup;
144
+ mcp.status = status;
145
+ mcp.test = test;
146
+ mcp.serve = serve;
147
+ mcp.printUsage = printUsage;
148
+
149
+ module.exports = mcp;
150
+ module.exports.classifyMcpFailure = classifyMcpFailure;
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { reconcileResources } = require('../worktree');
5
+
6
+ async function reconcile(projectDir, positional, flags = {}) {
7
+ const [resourceType, ...ids] = positional;
8
+ if (!resourceType || !ids.length) {
9
+ log.err('Usage: draftgo reconcile <pages|nav|docs> <id...>');
10
+ return 1;
11
+ }
12
+ const results = await reconcileResources(projectDir, resourceType, ids);
13
+ if (flags.output === 'json') console.log(JSON.stringify(results, null, 2));
14
+ else for (const result of results) {
15
+ log.ok(`Reconciled ${result.resource_type} ${result.resource_id} -> ${result.base_version ?? result.base_revision}`);
16
+ }
17
+ return 0;
18
+ }
19
+
20
+ module.exports = reconcile;
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { callOperation } = require('./api');
5
+
6
+ const OPERATIONS = Object.freeze({
7
+ permissions: 'listPermissions',
8
+ list: 'listRoles',
9
+ create: 'createRole',
10
+ get: 'getRole',
11
+ update: 'updateRole',
12
+ delete: 'deleteRole',
13
+ });
14
+
15
+ function operationKey(value) {
16
+ const action = String(value || 'list').trim().toLowerCase();
17
+ return OPERATIONS[action] ? action : '';
18
+ }
19
+
20
+ async function roleCommand(projectDir, positional, flags = {}) {
21
+ const action = operationKey(positional[0]);
22
+ if (!action) {
23
+ log.err('Usage: draftgo role permissions|list|create|get|update|delete [--input <json-file>]');
24
+ return 1;
25
+ }
26
+ return callOperation(projectDir, OPERATIONS[action], flags);
27
+ }
28
+
29
+ module.exports = roleCommand;
30
+ module.exports.OPERATIONS = OPERATIONS;
31
+ module.exports.operationKey = operationKey;