draftgo-cli 3.0.39 → 3.0.41

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
@@ -25,6 +25,7 @@ const VALUE_FLAGS = new Set([
25
25
  'params',
26
26
  'input',
27
27
  'type',
28
+ 'task',
28
29
  ]);
29
30
 
30
31
  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;
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { collectContext, TASK_PROFILES } = require('../context');
5
+
6
+ const MAX_TIMEOUT_MS = 2_147_483_647;
7
+
8
+ function parseTimeout(value) {
9
+ if (value == null) return undefined;
10
+ const text = String(value).trim();
11
+ if (!/^\d+$/.test(text)) {
12
+ throw new TypeError('--timeout must be a positive integer in milliseconds.');
13
+ }
14
+ const timeoutMs = Number(text);
15
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
16
+ throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
17
+ }
18
+ return timeoutMs;
19
+ }
20
+
21
+ function printUsage() {
22
+ log.err('Usage: draftgo context --task <task> --output json');
23
+ log.dim(` tasks: ${Object.keys(TASK_PROFILES).join(', ')}`);
24
+ }
25
+
26
+ async function contextCommand(projectDir, positional = [], flags = {}) {
27
+ if (positional.length || !flags.task || (flags.output && flags.output !== 'json')) {
28
+ printUsage();
29
+ return 1;
30
+ }
31
+ const result = await collectContext(projectDir, flags.task, {
32
+ timeoutMs: parseTimeout(flags.timeout),
33
+ });
34
+ console.log(JSON.stringify(result, null, 2));
35
+ return 0;
36
+ }
37
+
38
+ contextCommand.printUsage = printUsage;
39
+ contextCommand.parseTimeout = parseTimeout;
40
+
41
+ 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.
@@ -35,6 +37,9 @@ Usage:
35
37
  draftgo mcp test Test initialize, tools/list, and tools/call.
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.
@@ -82,7 +89,7 @@ Important flags:
82
89
  --target <name,...> Select one or more MCP setup/status targets.
83
90
  --server <url> (connect) DraftGo base URL.
84
91
  --token <sat> (connect) SAT for non-interactive use.
85
- --timeout <ms> (connect/mcp test) Network timeout.
92
+ --timeout <ms> (connect/mcp test/context) Network timeout.
86
93
  --allow-offline (connect) Save only after explicitly accepting a
87
94
  failed MCP validation.
88
95
  --no-mcp-setup (connect) Do not write host MCP configuration.
@@ -93,6 +100,8 @@ Important flags:
93
100
  --local-dev (init) Continue into local Docker setup.
94
101
  --no-setup (init) Install the Skill without either setup flow.
95
102
  --output json Print machine-readable output where supported.
103
+ --task <task> (context) frontend | data | custom-service | aihub |
104
+ content | project.
96
105
  --strict Treat check warnings as failures.
97
106
  --yes Skip supported confirmation prompts.
98
107
  --operation-id <id> (delete) Select an operation explicitly.
@@ -120,11 +129,13 @@ Examples:
120
129
  draftgo init codex
121
130
  draftgo connect codex --server https://draftgo.example --token <sat>
122
131
  draftgo mcp test
132
+ draftgo context --task frontend --output json
123
133
  draftgo map --output json
124
134
  draftgo checkout pages 42
125
135
  draftgo check --strict
126
136
  draftgo diff pages 42
127
137
  draftgo commit pages 42
138
+ draftgo changelog add "Complete document management and role permissions"
128
139
  draftgo deploy docs 7 --delivery preview
129
140
  `);
130
141
  }
@@ -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;