draftgo-cli 3.0.39 → 3.0.43

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.
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { dgDir } = require('./paths');
7
+
8
+ const DEFAULT_LOCK_TIMEOUT_MS = 10_000;
9
+ const DEFAULT_LOCK_STALE_MS = 60_000;
10
+ const DEFAULT_LOCK_RETRY_MS = 10;
11
+ const LOCK_WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
12
+
13
+ class ChangelogFormatError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'ChangelogFormatError';
17
+ this.code = 'INVALID_CHANGELOG';
18
+ }
19
+ }
20
+
21
+ function changelogPath(projectDir) {
22
+ return path.join(dgDir(projectDir), 'changelog.md');
23
+ }
24
+
25
+ function formatLocalDate(value = new Date()) {
26
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
27
+ throw new TypeError('A valid Date is required.');
28
+ }
29
+ const year = String(value.getFullYear()).padStart(4, '0');
30
+ const month = String(value.getMonth() + 1).padStart(2, '0');
31
+ const day = String(value.getDate()).padStart(2, '0');
32
+ return `${year}-${month}-${day}`;
33
+ }
34
+
35
+ function isLeapYear(year) {
36
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
37
+ }
38
+
39
+ function isCalendarDate(value) {
40
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
41
+ if (!match) return false;
42
+ const year = Number(match[1]);
43
+ const month = Number(match[2]);
44
+ const day = Number(match[3]);
45
+ if (year < 1 || month < 1 || month > 12 || day < 1) return false;
46
+ const days = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
47
+ return day <= days[month - 1];
48
+ }
49
+
50
+ function normalizeMessage(value) {
51
+ const message = value == null ? '' : String(value).trim();
52
+ if (!message) throw new TypeError('Changelog result must not be empty.');
53
+ if (/[\r\n\u2028\u2029]/.test(message)) {
54
+ throw new TypeError('Changelog result must be a single line.');
55
+ }
56
+ return message;
57
+ }
58
+
59
+ function invalidFormat(detail) {
60
+ return new ChangelogFormatError(`Invalid .draftgo/changelog.md: ${detail}`);
61
+ }
62
+
63
+ function parseChangelog(source) {
64
+ if (typeof source !== 'string') throw new TypeError('Changelog source must be a string.');
65
+ if (source === '') return [];
66
+ if (/\r(?!\n)/.test(source)) throw invalidFormat('unsupported line ending.');
67
+
68
+ const normalized = source.replace(/\r\n/g, '\n');
69
+ const body = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
70
+ const lines = body.split('\n');
71
+ const blocks = [];
72
+ let cursor = 0;
73
+
74
+ while (cursor < lines.length) {
75
+ const date = lines[cursor];
76
+ if (!isCalendarDate(date)) throw invalidFormat(`expected a YYYY-MM-DD header at line ${cursor + 1}.`);
77
+ if (blocks.length && date <= blocks[blocks.length - 1].date) {
78
+ throw invalidFormat(`date blocks must be strictly increasing (line ${cursor + 1}).`);
79
+ }
80
+ cursor += 1;
81
+ if (lines[cursor] !== '===') throw invalidFormat(`expected === at line ${cursor + 1}.`);
82
+ cursor += 1;
83
+
84
+ const entries = [];
85
+ while (cursor < lines.length && lines[cursor] !== '') {
86
+ const match = /^([1-9]\d*)\. (.+)$/.exec(lines[cursor]);
87
+ if (!match) throw invalidFormat(`invalid entry at line ${cursor + 1}.`);
88
+ const number = Number(match[1]);
89
+ if (!Number.isSafeInteger(number) || number !== entries.length + 1) {
90
+ throw invalidFormat(`entries must be consecutively numbered from 1 (line ${cursor + 1}).`);
91
+ }
92
+ let message;
93
+ try {
94
+ message = normalizeMessage(match[2]);
95
+ } catch (error) {
96
+ throw invalidFormat(`${error.message} (line ${cursor + 1}).`);
97
+ }
98
+ if (message !== match[2]) throw invalidFormat(`entry has surrounding whitespace at line ${cursor + 1}.`);
99
+ entries.push({ number, message });
100
+ cursor += 1;
101
+ }
102
+ if (!entries.length) throw invalidFormat(`date block ${date} has no entries.`);
103
+ blocks.push({ date, entries });
104
+
105
+ if (cursor >= lines.length) break;
106
+ cursor += 1;
107
+ if (cursor >= lines.length || lines[cursor] === '') {
108
+ throw invalidFormat(`date blocks must be separated by exactly one blank line (line ${cursor + 1}).`);
109
+ }
110
+ }
111
+
112
+ return blocks;
113
+ }
114
+
115
+ function renderChangelog(blocks) {
116
+ if (!Array.isArray(blocks)) throw new TypeError('Changelog blocks must be an array.');
117
+ if (!blocks.length) return '';
118
+ return `${blocks.map((block) => [
119
+ block.date,
120
+ '===',
121
+ ...block.entries.map((entry) => `${entry.number}. ${entry.message}`),
122
+ ].join('\n')).join('\n\n')}\n`;
123
+ }
124
+
125
+ function appendChangelogEntry(source, value, now = new Date()) {
126
+ const message = normalizeMessage(value);
127
+ const date = formatLocalDate(now);
128
+ const blocks = parseChangelog(source);
129
+ const last = blocks[blocks.length - 1];
130
+
131
+ if (last && date < last.date) {
132
+ throw new ChangelogFormatError(
133
+ `Cannot append ${date}: the latest changelog block is ${last.date}. Check the local clock.`
134
+ );
135
+ }
136
+
137
+ let number;
138
+ if (last && last.date === date) {
139
+ number = last.entries.length + 1;
140
+ last.entries.push({ number, message });
141
+ } else {
142
+ number = 1;
143
+ blocks.push({ date, entries: [{ number, message }] });
144
+ }
145
+
146
+ return { content: renderChangelog(blocks), date, number, message };
147
+ }
148
+
149
+ function temporaryPath(destination) {
150
+ const suffix = crypto.randomBytes(8).toString('hex');
151
+ return path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}.${suffix}.tmp`);
152
+ }
153
+
154
+ function lockOption(value, fallback, name, allowZero = false) {
155
+ const number = value == null ? fallback : Number(value);
156
+ const minimum = allowZero ? 0 : 1;
157
+ if (!Number.isSafeInteger(number) || number < minimum) {
158
+ throw new TypeError(`${name} must be an integer greater than or equal to ${minimum}.`);
159
+ }
160
+ return number;
161
+ }
162
+
163
+ function removeStaleLock(lockPath, staleMs) {
164
+ try {
165
+ const stat = fs.lstatSync(lockPath);
166
+ if (Date.now() - stat.mtimeMs < staleMs) return false;
167
+ fs.rmSync(lockPath, { force: true });
168
+ return true;
169
+ } catch (error) {
170
+ if (error && error.code === 'ENOENT') return true;
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ function acquireChangelogLock(destination, options = {}) {
176
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
177
+ const lockPath = `${destination}.lock`;
178
+ const timeoutMs = lockOption(
179
+ options.lockTimeoutMs,
180
+ DEFAULT_LOCK_TIMEOUT_MS,
181
+ 'lockTimeoutMs',
182
+ true,
183
+ );
184
+ const staleMs = lockOption(options.lockStaleMs, DEFAULT_LOCK_STALE_MS, 'lockStaleMs');
185
+ const retryMs = lockOption(options.lockRetryMs, DEFAULT_LOCK_RETRY_MS, 'lockRetryMs');
186
+ const token = `${process.pid}:${crypto.randomBytes(16).toString('hex')}`;
187
+ const started = Date.now();
188
+
189
+ while (true) {
190
+ let descriptor;
191
+ let created = false;
192
+ try {
193
+ descriptor = fs.openSync(lockPath, 'wx', 0o600);
194
+ created = true;
195
+ fs.writeFileSync(descriptor, `${token}\n`, 'utf8');
196
+ fs.fsyncSync(descriptor);
197
+ fs.closeSync(descriptor);
198
+ descriptor = undefined;
199
+ return { path: lockPath, token };
200
+ } catch (error) {
201
+ if (descriptor !== undefined) {
202
+ try { fs.closeSync(descriptor); } catch { /* Preserve the original failure. */ }
203
+ }
204
+ if (created) {
205
+ try { fs.rmSync(lockPath, { force: true }); } catch { /* Best-effort cleanup. */ }
206
+ }
207
+ if (!error || error.code !== 'EEXIST') throw error;
208
+ }
209
+
210
+ if (removeStaleLock(lockPath, staleMs)) continue;
211
+ const elapsed = Date.now() - started;
212
+ if (elapsed >= timeoutMs) {
213
+ const error = new Error('Timed out waiting for .draftgo/changelog.md.lock.');
214
+ error.code = 'CHANGELOG_LOCK_TIMEOUT';
215
+ throw error;
216
+ }
217
+ Atomics.wait(LOCK_WAIT_ARRAY, 0, 0, Math.min(retryMs, timeoutMs - elapsed));
218
+ }
219
+ }
220
+
221
+ function releaseChangelogLock(lock) {
222
+ try {
223
+ const token = fs.readFileSync(lock.path, 'utf8').trim();
224
+ if (token === lock.token) fs.rmSync(lock.path, { force: true });
225
+ } catch (error) {
226
+ if (!error || error.code !== 'ENOENT') throw error;
227
+ }
228
+ }
229
+
230
+ function writeTextAtomic(destination, content) {
231
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
232
+ const temporary = temporaryPath(destination);
233
+ let descriptor;
234
+ try {
235
+ descriptor = fs.openSync(temporary, 'wx', 0o666);
236
+ fs.writeFileSync(descriptor, content, 'utf8');
237
+ fs.fsyncSync(descriptor);
238
+ fs.closeSync(descriptor);
239
+ descriptor = undefined;
240
+ fs.renameSync(temporary, destination);
241
+ } finally {
242
+ if (descriptor !== undefined) {
243
+ try { fs.closeSync(descriptor); } catch { /* The original error is more useful. */ }
244
+ }
245
+ try { fs.rmSync(temporary, { force: true }); } catch { /* Best-effort temporary cleanup. */ }
246
+ }
247
+ }
248
+
249
+ function addChangelogEntry(projectDir, value, options = {}) {
250
+ const file = changelogPath(projectDir);
251
+ const message = normalizeMessage(value);
252
+ const lock = acquireChangelogLock(file, options);
253
+ try {
254
+ const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
255
+ const result = appendChangelogEntry(source, message, options.now || new Date());
256
+ writeTextAtomic(file, result.content);
257
+ return { ...result, path: file };
258
+ } finally {
259
+ releaseChangelogLock(lock);
260
+ }
261
+ }
262
+
263
+ module.exports = {
264
+ ChangelogFormatError,
265
+ changelogPath,
266
+ formatLocalDate,
267
+ isCalendarDate,
268
+ normalizeMessage,
269
+ parseChangelog,
270
+ renderChangelog,
271
+ appendChangelogEntry,
272
+ acquireChangelogLock,
273
+ releaseChangelogLock,
274
+ writeTextAtomic,
275
+ addChangelogEntry,
276
+ };
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ const VALUE_FLAGS = new Set([
7
7
  'project',
8
8
  'target',
9
9
  'server',
10
+ 'mcp-url',
10
11
  'token',
11
12
  'output',
12
13
  'url',
@@ -25,6 +26,7 @@ const VALUE_FLAGS = new Set([
25
26
  'params',
26
27
  'input',
27
28
  'type',
29
+ 'task',
28
30
  ]);
29
31
 
30
32
  function parse(argv) {
@@ -7,6 +7,8 @@ const commands = [
7
7
  { name: 'update', run: (dir, args, flags) => require('./commands/update')(dir, args, flags) },
8
8
  { name: 'uninstall', aliases: ['remove'], run: (dir, args, flags) => require('./commands/uninstall')(dir, args, flags) },
9
9
  { name: 'status', run: (dir) => require('./commands/status')(dir) },
10
+ { name: 'context', run: (dir, args, flags) => require('./commands/context')(dir, args, flags) },
11
+ { name: 'changelog', run: (dir, args, flags) => require('./commands/changelog')(dir, args, flags) },
10
12
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
11
13
  { name: 'check', run: (dir, _args, flags) => require('./commands/check')(dir, flags) },
12
14
  { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { addChangelogEntry } = require('../changelog');
5
+
6
+ function printUsage() {
7
+ log.err('Usage: draftgo changelog add <completed-result>');
8
+ }
9
+
10
+ function changelog(projectDir, positional = []) {
11
+ const [action, ...messageParts] = positional;
12
+ if (action !== 'add' || !messageParts.length) {
13
+ printUsage();
14
+ return 1;
15
+ }
16
+
17
+ const result = addChangelogEntry(projectDir, messageParts.join(' '));
18
+ log.ok(`Changelog updated: .draftgo/changelog.md (${result.date} #${result.number})`);
19
+ return 0;
20
+ }
21
+
22
+ changelog.printUsage = printUsage;
23
+
24
+ module.exports = changelog;
@@ -2,9 +2,10 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const { ask, askPassword } = require('../prompt');
5
- const { normalizeServer, writeProjectConfig } = require('../projectConfig');
5
+ const { normalizeConnection, normalizeServer, writeProjectConfig } = require('../projectConfig');
6
6
  const { testConnection } = require('../mcp/client');
7
7
  const { redactText } = require('../mcp/protocol');
8
+ const { parseTimeout } = require('../timeout');
8
9
 
9
10
  async function promptServer(defaultValue) {
10
11
  while (true) {
@@ -28,19 +29,32 @@ async function promptToken() {
28
29
 
29
30
  async function connect(projectDir, positional, flags = {}) {
30
31
  log.title('draftgo connect');
31
- const server = flags.server ? normalizeServer(flags.server) : await promptServer('https://');
32
+ const endpointConnection = flags['mcp-url']
33
+ ? normalizeConnection(flags['mcp-url'])
34
+ : null;
35
+ if (endpointConnection && !endpointConnection.server && !flags.server) {
36
+ throw new Error('--server is required when --mcp-url does not use the standard /mcp path.');
37
+ }
38
+ const connection = {
39
+ server: flags.server
40
+ ? normalizeServer(flags.server)
41
+ : endpointConnection && endpointConnection.server || await promptServer('https://'),
42
+ mcp_url: endpointConnection && endpointConnection.mcp_url || '',
43
+ };
44
+ const server = connection.server;
32
45
  const token = flags.token ? String(flags.token).trim() : await promptToken();
33
46
  if (!token) {
34
47
  log.err('A non-empty SAT is required.');
35
48
  return 1;
36
49
  }
50
+ const timeoutMs = parseTimeout(flags.timeout);
37
51
 
38
52
  log.step('Validating SAT and DraftGo MCP capabilities...');
39
53
  try {
40
- const diagnostic = await testConnection({ server, token }, {
41
- timeoutMs: flags.timeout ? Number(flags.timeout) : undefined,
54
+ const diagnostic = await testConnection({ ...connection, server, token }, {
55
+ timeoutMs,
42
56
  });
43
- log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.testedTool}).`);
57
+ log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.testedCalls.join(', ')}).`);
44
58
  } catch (error) {
45
59
  const message = redactText(error && error.message ? error.message : error, [token]);
46
60
  if (!flags['allow-offline']) {
@@ -52,7 +66,7 @@ async function connect(projectDir, positional, flags = {}) {
52
66
  log.warn('Continuing only because --allow-offline was explicitly supplied.');
53
67
  }
54
68
 
55
- const configFile = writeProjectConfig(projectDir, server, token);
69
+ const configFile = writeProjectConfig(projectDir, server, token, { mcp_url: connection.mcp_url });
56
70
  log.ok(`Project configuration written: ${configFile}`);
57
71
 
58
72
  if (!flags['no-mcp-setup']) {
@@ -66,6 +80,7 @@ async function connect(projectDir, positional, flags = {}) {
66
80
 
67
81
  log.title('Connected');
68
82
  log.plain(` server: ${server}`);
83
+ if (connection.mcp_url) log.plain(` mcp endpoint: ${connection.mcp_url}`);
69
84
  log.plain(` config: ${configFile}`);
70
85
  log.dim(' No DraftGo business resources were downloaded. Use MCP discovery or checkout long content explicitly.');
71
86
  return 0;
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { collectContext, TASK_PROFILES } = require('../context');
5
+ const { parseTimeout } = require('../timeout');
6
+
7
+ function printUsage() {
8
+ log.err('Usage: draftgo context --task <task> --output json');
9
+ log.dim(` tasks: ${Object.keys(TASK_PROFILES).join(', ')}`);
10
+ }
11
+
12
+ async function contextCommand(projectDir, positional = [], flags = {}) {
13
+ if (positional.length || !flags.task || (flags.output && flags.output !== 'json')) {
14
+ printUsage();
15
+ return 1;
16
+ }
17
+ const result = await collectContext(projectDir, flags.task, {
18
+ timeoutMs: parseTimeout(flags.timeout),
19
+ });
20
+ console.log(JSON.stringify(result, null, 2));
21
+ return 0;
22
+ }
23
+
24
+ contextCommand.printUsage = printUsage;
25
+ contextCommand.parseTimeout = parseTimeout;
26
+
27
+ module.exports = contextCommand;
@@ -17,6 +17,8 @@ DraftGo Next frontend baseline: React + Vite.
17
17
  Database pages use standard HTML + Tailwind CSS.
18
18
  MCP handles live discovery and structured resources; complete page, navigation,
19
19
  and document bodies use checkout/commit outside MCP context.
20
+ The context command combines exact local Reference sections with live MCP data;
21
+ it is a CLI orchestration command, not an MCP tool.
20
22
 
21
23
  Usage:
22
24
  draftgo init [<target>...] Install the DraftGo Skill. No target = detect.
@@ -32,9 +34,12 @@ Usage:
32
34
  downloads DraftGo business resources.
33
35
  draftgo mcp setup [<target>...] Merge project-level stdio MCP configuration.
34
36
  draftgo mcp status [<target>...] Check host configuration and secret safety.
35
- draftgo mcp test Test initialize, tools/list, and tools/call.
37
+ draftgo mcp test Test project, resource, and API MCP calls.
36
38
  draftgo mcp serve Bridge local stdio to the remote /mcp endpoint.
37
39
 
40
+ draftgo context --task <task> Read exact task Reference sections, then reuse
41
+ one MCP session to query project, resources,
42
+ APIs, and data structures in parallel.
38
43
  draftgo map Read remote overview/resources through MCP and
39
44
  overlay local checkout state.
40
45
  draftgo checkout <type> <id...> Download pages/nav/docs body + verified base.
@@ -62,6 +67,8 @@ Usage:
62
67
  draftgo auto-push [<type> <id...>]
63
68
  With config.auto_push=true, check and commit
64
69
  changed checkouts. Any conflict stops the run.
70
+ draftgo changelog add <result> Append one completed result after the task has
71
+ passed unified verification and delivery.
65
72
  draftgo pull Migration notice only; always downloads nothing.
66
73
  draftgo push <type> <id...> Deprecated alias to commit for pages/nav/docs.
67
74
  Push-all and structured-resource push are removed.
@@ -81,8 +88,10 @@ Important flags:
81
88
  --project <dir> Operate on <dir> instead of the current directory.
82
89
  --target <name,...> Select one or more MCP setup/status targets.
83
90
  --server <url> (connect) DraftGo base URL.
91
+ --mcp-url <url> (connect) Explicit endpoint; standard /mcp derives the
92
+ base URL; custom paths also need --server.
84
93
  --token <sat> (connect) SAT for non-interactive use.
85
- --timeout <ms> (connect/mcp test) Network timeout.
94
+ --timeout <ms> (connect/mcp test/context) Network timeout.
86
95
  --allow-offline (connect) Save only after explicitly accepting a
87
96
  failed MCP validation.
88
97
  --no-mcp-setup (connect) Do not write host MCP configuration.
@@ -93,6 +102,9 @@ Important flags:
93
102
  --local-dev (init) Continue into local Docker setup.
94
103
  --no-setup (init) Install the Skill without either setup flow.
95
104
  --output json Print machine-readable output where supported.
105
+ --task <task> (context) frontend | data | custom-service | aihub |
106
+ content | project.
107
+ --type <type> (map) pages | nav/navigations | docs/articles.
96
108
  --strict Treat check warnings as failures.
97
109
  --yes Skip supported confirmation prompts.
98
110
  --operation-id <id> (delete) Select an operation explicitly.
@@ -120,11 +132,13 @@ Examples:
120
132
  draftgo init codex
121
133
  draftgo connect codex --server https://draftgo.example --token <sat>
122
134
  draftgo mcp test
135
+ draftgo context --task frontend --output json
123
136
  draftgo map --output json
124
137
  draftgo checkout pages 42
125
138
  draftgo check --strict
126
139
  draftgo diff pages 42
127
140
  draftgo commit pages 42
141
+ draftgo changelog add "Complete document management and role permissions"
128
142
  draftgo deploy docs 7 --delivery preview
129
143
  `);
130
144
  }
@@ -6,7 +6,16 @@ const log = require('../logger');
6
6
  const { loadProjectConfig } = require('../projectConfig');
7
7
  const { loadManifest, absolutePath } = require('../worktree/manifest');
8
8
  const { hashFile } = require('../worktree/streams');
9
+ const { canonicalResourceType } = require('../worktree/types');
9
10
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
11
+ const { allWithAbort } = require('../mcp/parallel');
12
+
13
+ const REMOTE_RESOURCE_TYPES = Object.freeze({
14
+ pages: 'pages',
15
+ navigations: 'navigations',
16
+ docs: 'docs/articles',
17
+ });
18
+ const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
10
19
 
11
20
  function itemsFrom(value) {
12
21
  if (Array.isArray(value)) return value;
@@ -18,26 +27,59 @@ function itemsFrom(value) {
18
27
  }
19
28
 
20
29
  function nextCursor(value) {
21
- return value && (value.next_cursor || value.nextCursor || value.cursor && value.has_more && value.cursor);
30
+ if (!value || typeof value !== 'object') return null;
31
+ if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) return value.next_cursor;
32
+ if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) return value.nextCursor;
33
+ return (value.has_more === true || value.hasMore === true)
34
+ && Object.prototype.hasOwnProperty.call(value, 'cursor')
35
+ ? value.cursor
36
+ : null;
37
+ }
38
+
39
+ function claimsMorePages(value) {
40
+ return !!value && typeof value === 'object'
41
+ && (value.has_more === true || value.hasMore === true);
42
+ }
43
+
44
+ function normalizeMapResourceType(value) {
45
+ return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
22
46
  }
23
47
 
24
- async function listRemoteResources(session, flags) {
48
+ function requestedResourceTypes(flags = {}) {
49
+ return flags.type == null
50
+ ? DEFAULT_REMOTE_RESOURCE_TYPES
51
+ : [normalizeMapResourceType(flags.type)];
52
+ }
53
+
54
+ async function listRemoteResourceType(session, resourceType, options = {}) {
25
55
  const resources = [];
26
56
  const seen = new Set();
27
57
  let cursor = null;
28
58
  for (let page = 0; page < 100; page += 1) {
29
- const args = {};
30
- if (cursor) args.cursor = cursor;
31
- if (flags.type) args.resource_type = String(flags.type);
32
- const payload = await callStructured(session, TOOL_NAMES.resourceList, args);
59
+ const args = { resource_type: resourceType, limit: 100 };
60
+ if (cursor != null && cursor !== '') args.cursor = cursor;
61
+ const payload = await callStructured(session, TOOL_NAMES.resourceList, args, options);
33
62
  resources.push(...itemsFrom(payload));
34
63
  cursor = nextCursor(payload);
35
- if (!cursor) break;
36
- if (seen.has(cursor)) throw new Error('DraftGo resource_list repeated a cursor.');
37
- seen.add(cursor);
64
+ if (cursor == null || cursor === '') {
65
+ if (claimsMorePages(payload)) {
66
+ throw new Error(`DraftGo resource_list (${resourceType}) reported more pages without a cursor.`);
67
+ }
68
+ return resources;
69
+ }
70
+ const cursorKey = String(cursor);
71
+ if (seen.has(cursorKey)) {
72
+ throw new Error(`DraftGo resource_list (${resourceType}) repeated a cursor.`);
73
+ }
74
+ seen.add(cursorKey);
38
75
  }
39
- if (cursor) throw new Error('DraftGo resource_list exceeded 100 pages.');
40
- return resources;
76
+ throw new Error(`DraftGo resource_list (${resourceType}) exceeded 100 pages.`);
77
+ }
78
+
79
+ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOURCE_TYPES, options = {}) {
80
+ const groups = await allWithAbort(resourceTypes.map((resourceType) =>
81
+ (queryOptions) => listRemoteResourceType(session, resourceType, queryOptions)), options);
82
+ return groups.flat();
41
83
  }
42
84
 
43
85
  async function localCheckouts(projectDir) {
@@ -63,13 +105,17 @@ function legacyCaches(projectDir) {
63
105
  }
64
106
 
65
107
  async function mapCommand(projectDir, flags = {}) {
108
+ const resourceTypes = requestedResourceTypes(flags);
66
109
  const config = loadProjectConfig(projectDir);
67
110
  const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList]);
68
- const [overview, resources, checkouts] = await Promise.all([
69
- callStructured(session, TOOL_NAMES.projectOverview, {}),
70
- listRemoteResources(session, flags),
71
- localCheckouts(projectDir),
111
+ const [remote, checkouts] = await allWithAbort([
112
+ (options) => allWithAbort([
113
+ (options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
114
+ (options) => listRemoteResources(session, resourceTypes, options),
115
+ ], options),
116
+ () => localCheckouts(projectDir),
72
117
  ]);
118
+ const [overview, resources] = remote;
73
119
  const result = {
74
120
  server: config.server,
75
121
  overview,
@@ -103,3 +149,6 @@ async function mapCommand(projectDir, flags = {}) {
103
149
 
104
150
  module.exports = mapCommand;
105
151
  module.exports.itemsFrom = itemsFrom;
152
+ module.exports.normalizeMapResourceType = normalizeMapResourceType;
153
+ module.exports.requestedResourceTypes = requestedResourceTypes;
154
+ module.exports.listRemoteResources = listRemoteResources;
@@ -11,6 +11,7 @@ const {
11
11
  statusHosts,
12
12
  } = require('../mcp/hosts');
13
13
  const { serveStdio } = require('../mcp/stdio');
14
+ const { parseTimeout } = require('../timeout');
14
15
 
15
16
  function targetArgs(positional, flags) {
16
17
  const values = positional.slice();
@@ -89,11 +90,11 @@ async function test(projectDir, _positional = [], flags = {}) {
89
90
  try {
90
91
  config = loadProjectConfig(projectDir);
91
92
  const result = await testConnection(config, {
92
- timeoutMs: flags.timeout ? Number(flags.timeout) : undefined,
93
+ timeoutMs: parseTimeout(flags.timeout),
93
94
  });
94
95
  log.ok('DraftGo MCP initialize succeeded.');
95
96
  log.ok(`tools/list returned ${result.tools.length} tools.`);
96
- log.ok(`tools/call succeeded: ${result.testedTool}`);
97
+ log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
97
98
  log.dim(` protocol: ${result.protocolVersion}`);
98
99
  return 0;
99
100
  } catch (error) {
@@ -9,6 +9,12 @@ const { detectTargets } = require('../detect');
9
9
 
10
10
  const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
11
11
 
12
+ function installedTargetsNotRefreshed(projectDir, refreshedTargets) {
13
+ const refreshed = new Set(refreshedTargets.map((target) => target.name));
14
+ return all.filter((target) =>
15
+ !refreshed.has(target.name) && target.status(projectDir).installed);
16
+ }
17
+
12
18
  async function updateCliIfNeeded(projectDir, positional, flags) {
13
19
  if (process.env[REENTRY_FLAG] === '1' || flags['skip-update-check'] || process.env.DRAFTGO_NO_UPDATE_CHECK === '1') {
14
20
  return null;
@@ -105,7 +111,15 @@ async function update(projectDir, positional, flags) {
105
111
  log.err(`更新未完成,失败目标:${failures.join(', ')}`);
106
112
  return 1;
107
113
  }
108
- writeInstalledVersion(projectDir);
114
+ const staleInstalled = installedTargetsNotRefreshed(projectDir, resolved);
115
+ if (staleInstalled.length) {
116
+ log.dim(
117
+ ` 未更新全局 skill 版本标记;尚有未刷新的已安装目标:${staleInstalled
118
+ .map((target) => target.displayName).join(', ')}`,
119
+ );
120
+ } else {
121
+ writeInstalledVersion(projectDir);
122
+ }
109
123
 
110
124
  log.title('完成');
111
125
  return 0;
@@ -113,3 +127,4 @@ async function update(projectDir, positional, flags) {
113
127
 
114
128
  module.exports = update;
115
129
  module.exports.updateCliIfNeeded = updateCliIfNeeded;
130
+ module.exports.installedTargetsNotRefreshed = installedTargetsNotRefreshed;