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.
- package/README.md +66 -15
- package/package.json +4 -2
- package/resources/skill/SKILL.md +53 -15
- package/resources/skill/init/SKILL.md +5 -3
- package/resources/skill/manifest.json +4 -2
- package/resources/skill/push/SKILL.md +2 -0
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/checkout.md +3 -2
- package/resources/skill/references/frontend.md +18 -287
- package/resources/skill/references/mcp.md +6 -2
- package/resources/skill/references/parallel.md +11 -6
- package/resources/skill/references/runtime.md +1 -1
- package/resources/skill/references/ui-protocol.md +1 -1
- package/resources/skill/scripts/README.md +2 -0
- package/src/changelog.js +276 -0
- package/src/cli.js +2 -0
- package/src/commandRegistry.js +2 -0
- package/src/commands/changelog.js +24 -0
- package/src/commands/connect.js +21 -6
- package/src/commands/context.js +27 -0
- package/src/commands/help.js +16 -2
- package/src/commands/map.js +64 -15
- package/src/commands/mcp.js +3 -2
- package/src/commands/update.js +16 -1
- package/src/context/index.js +576 -0
- package/src/mcp/client.js +170 -14
- package/src/mcp/parallel.js +31 -0
- package/src/mcp/protocol.js +2 -1
- package/src/projectConfig.js +39 -5
- package/src/skill.js +65 -3
- package/src/timeout.js +18 -0
package/src/changelog.js
ADDED
|
@@ -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
package/src/commandRegistry.js
CHANGED
|
@@ -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;
|
package/src/commands/connect.js
CHANGED
|
@@ -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
|
|
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
|
|
54
|
+
const diagnostic = await testConnection({ ...connection, server, token }, {
|
|
55
|
+
timeoutMs,
|
|
42
56
|
});
|
|
43
|
-
log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.
|
|
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;
|
package/src/commands/help.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/commands/map.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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 [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
package/src/commands/mcp.js
CHANGED
|
@@ -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:
|
|
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.
|
|
97
|
+
log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
|
|
97
98
|
log.dim(` protocol: ${result.protocolVersion}`);
|
|
98
99
|
return 0;
|
|
99
100
|
} catch (error) {
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
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;
|