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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/draftgo.js +9 -0
- package/package.json +70 -0
- package/resources/project-design/README.md +42 -0
- package/resources/skill/SKILL.md +62 -0
- package/resources/skill/init/SKILL.md +41 -0
- package/resources/skill/manifest.json +35 -0
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +97 -0
- package/resources/skill/references/architecture.md +13 -0
- package/resources/skill/references/chat-sdk.md +205 -0
- package/resources/skill/references/checkout.md +140 -0
- package/resources/skill/references/data.md +49 -0
- package/resources/skill/references/db-relations.md +29 -0
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/development.md +41 -0
- package/resources/skill/references/diagnostics.md +50 -0
- package/resources/skill/references/frontend.md +158 -0
- package/resources/skill/references/mcp.md +110 -0
- package/resources/skill/references/methods.md +143 -0
- package/resources/skill/references/modules.md +75 -0
- package/resources/skill/references/runtime.md +109 -0
- package/resources/skill/references/services.md +32 -0
- package/src/apiContractCache.js +120 -0
- package/src/cli.js +100 -0
- package/src/commandRegistry.js +46 -0
- package/src/commands/api.js +244 -0
- package/src/commands/apiKey.js +30 -0
- package/src/commands/autoPush.js +36 -0
- package/src/commands/capabilities.js +100 -0
- package/src/commands/check.js +82 -0
- package/src/commands/checkout.js +18 -0
- package/src/commands/clean.js +72 -0
- package/src/commands/commit.js +47 -0
- package/src/commands/components.js +554 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +91 -0
- package/src/commands/delete.js +95 -0
- package/src/commands/deploy.js +77 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/group.js +37 -0
- package/src/commands/help.js +190 -0
- package/src/commands/init.js +126 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/local.js +79 -0
- package/src/commands/map.js +395 -0
- package/src/commands/mcp.js +150 -0
- package/src/commands/reconcile.js +20 -0
- package/src/commands/role.js +31 -0
- package/src/commands/status.js +98 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +79 -0
- package/src/commands/verify.js +188 -0
- package/src/commands/visualVerify.js +281 -0
- package/src/commands/worklog.js +117 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +65 -0
- package/src/detect.js +25 -0
- package/src/diffReport.js +106 -0
- package/src/fsx.js +67 -0
- package/src/index.js +46 -0
- package/src/localRuntime/compose.js +119 -0
- package/src/localRuntime/detect.js +77 -0
- package/src/localRuntime/index.js +211 -0
- package/src/localRuntime/mysqlClient.js +155 -0
- package/src/localRuntime/services.js +117 -0
- package/src/logger.js +37 -0
- package/src/mcp/client.js +558 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/parallel.js +54 -0
- package/src/mcp/protocol.js +223 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +51 -0
- package/src/paths.js +32 -0
- package/src/platforms.js +110 -0
- package/src/projectConfig.js +139 -0
- package/src/projectDesign.js +19 -0
- package/src/projectHealth.js +33 -0
- package/src/projectMap.js +220 -0
- package/src/prompt.js +94 -0
- package/src/releaseInstall.js +105 -0
- package/src/runtimeFiles.js +45 -0
- package/src/skill.js +295 -0
- package/src/targets.js +43 -0
- package/src/timeout.js +18 -0
- package/src/updateCheck.js +100 -0
- package/src/worklog.js +276 -0
- package/src/worktree/backend.js +438 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +751 -0
- package/src/worktree/inlineScripts.js +99 -0
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +89 -0
- package/src/worktree/status.js +124 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
package/src/worklog.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { dgDir } = require('./paths');
|
|
7
|
+
|
|
8
|
+
const STATUS = Object.freeze({ pending: '', active: '●', waiting: '?', completed: '√' });
|
|
9
|
+
const LOCK_WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
10
|
+
|
|
11
|
+
function formatLocalDate(value = new Date()) {
|
|
12
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) throw new TypeError('A valid Date is required.');
|
|
13
|
+
const year = String(value.getFullYear()).padStart(4, '0');
|
|
14
|
+
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
15
|
+
const day = String(value.getDate()).padStart(2, '0');
|
|
16
|
+
return `${year}-${month}-${day}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function worklogPath(projectDir) {
|
|
20
|
+
return path.join(dgDir(projectDir), 'worklog.md');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function invalid(detail) {
|
|
24
|
+
const error = new Error(`Invalid .draftgo/worklog.md: ${detail}`);
|
|
25
|
+
error.code = 'INVALID_WORKLOG';
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeText(value, name = 'Worklog text') {
|
|
30
|
+
const text = value == null ? '' : String(value).trim();
|
|
31
|
+
if (!text) throw new TypeError(`${name} must not be empty.`);
|
|
32
|
+
if (/[\r\n\u2028\u2029]/.test(text)) throw new TypeError(`${name} must be a single line.`);
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeDate(value, now = new Date()) {
|
|
37
|
+
const date = value == null || value === '' ? formatLocalDate(now) : String(value).trim();
|
|
38
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new TypeError('Worklog date must use YYYY-MM-DD.');
|
|
39
|
+
const [year, month, day] = date.split('-').map(Number);
|
|
40
|
+
const parsed = new Date(year, month - 1, day);
|
|
41
|
+
if (parsed.getFullYear() !== year || parsed.getMonth() !== month - 1 || parsed.getDate() !== day) {
|
|
42
|
+
throw new TypeError(`Invalid worklog date: ${date}.`);
|
|
43
|
+
}
|
|
44
|
+
return date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseStatus(marker) {
|
|
48
|
+
if (marker === '●') return 'active';
|
|
49
|
+
if (marker === '√') return 'completed';
|
|
50
|
+
if (marker === '?') return 'waiting';
|
|
51
|
+
if (marker === '') return 'pending';
|
|
52
|
+
throw invalid(`unsupported status marker ${JSON.stringify(marker)}.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseWorklog(source) {
|
|
56
|
+
if (typeof source !== 'string') throw new TypeError('Worklog source must be a string.');
|
|
57
|
+
if (!source) return [];
|
|
58
|
+
if (/\r(?!\n)/.test(source)) throw invalid('unsupported line ending.');
|
|
59
|
+
const normalized = source.replace(/\r\n/g, '\n');
|
|
60
|
+
const body = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
61
|
+
if (!body) return [];
|
|
62
|
+
const lines = body.split('\n');
|
|
63
|
+
const blocks = [];
|
|
64
|
+
let cursor = 0;
|
|
65
|
+
|
|
66
|
+
while (cursor < lines.length) {
|
|
67
|
+
const header = /^\[\s*(\d{4}-\d{2}-\d{2})\s*\]$/.exec(lines[cursor]);
|
|
68
|
+
if (!header) throw invalid(`expected a [ YYYY-MM-DD ] header at line ${cursor + 1}.`);
|
|
69
|
+
const date = normalizeDate(header[1]);
|
|
70
|
+
if (blocks.length && date <= blocks[blocks.length - 1].date) {
|
|
71
|
+
throw invalid(`date blocks must be strictly chronological (line ${cursor + 1}).`);
|
|
72
|
+
}
|
|
73
|
+
cursor += 1;
|
|
74
|
+
const entries = [];
|
|
75
|
+
while (cursor < lines.length && lines[cursor] !== '') {
|
|
76
|
+
const itemMatch = /^([1-9]\d*)\.\s+\[\s*(●|√|\?)?\s*\]\s+(.+)$/.exec(lines[cursor]);
|
|
77
|
+
if (!itemMatch) throw invalid(`invalid item at line ${cursor + 1}.`);
|
|
78
|
+
const number = Number(itemMatch[1]);
|
|
79
|
+
if (!Number.isSafeInteger(number) || number !== entries.length + 1) {
|
|
80
|
+
throw invalid(`items must be consecutively numbered from 1 (line ${cursor + 1}).`);
|
|
81
|
+
}
|
|
82
|
+
const title = normalizeText(itemMatch[3], 'Worklog item');
|
|
83
|
+
const status = parseStatus(itemMatch[2] || '');
|
|
84
|
+
cursor += 1;
|
|
85
|
+
const notes = [];
|
|
86
|
+
while (cursor < lines.length && lines[cursor].startsWith('//')) {
|
|
87
|
+
const note = lines[cursor].slice(2).trim();
|
|
88
|
+
if (note) notes.push(note);
|
|
89
|
+
cursor += 1;
|
|
90
|
+
}
|
|
91
|
+
entries.push({ number, title, status, notes });
|
|
92
|
+
}
|
|
93
|
+
if (!entries.length) throw invalid(`date block ${date} has no items.`);
|
|
94
|
+
blocks.push({ date, entries });
|
|
95
|
+
if (cursor >= lines.length) break;
|
|
96
|
+
cursor += 1;
|
|
97
|
+
if (cursor >= lines.length || lines[cursor] === '') {
|
|
98
|
+
throw invalid(`date blocks must be separated by one blank line (line ${cursor + 1}).`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return blocks;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderStatus(status) {
|
|
105
|
+
const marker = STATUS[status];
|
|
106
|
+
if (marker === undefined) throw new TypeError(`Unknown worklog status: ${status}.`);
|
|
107
|
+
return marker ? `[ ${marker} ]` : '[ ]';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderWorklog(blocks) {
|
|
111
|
+
if (!Array.isArray(blocks)) throw new TypeError('Worklog blocks must be an array.');
|
|
112
|
+
if (!blocks.length) return '';
|
|
113
|
+
return `${blocks.map((block) => [
|
|
114
|
+
`[ ${block.date} ]`,
|
|
115
|
+
...block.entries.flatMap((entry) => [
|
|
116
|
+
`${entry.number}. ${renderStatus(entry.status)} ${entry.title}`,
|
|
117
|
+
...(entry.notes || []).map((note) => `// ${note}`),
|
|
118
|
+
]),
|
|
119
|
+
].join('\n')).join('\n\n')}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cloneBlocks(blocks) {
|
|
123
|
+
return blocks.map((block) => ({
|
|
124
|
+
...block,
|
|
125
|
+
entries: block.entries.map((entry) => ({ ...entry, notes: [...entry.notes] })),
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function appendItem(blocks, title, status = 'pending', notes = [], date = normalizeDate()) {
|
|
130
|
+
const next = cloneBlocks(blocks);
|
|
131
|
+
const last = next[next.length - 1];
|
|
132
|
+
if (last && date < last.date) throw new Error(`Cannot append ${date}; the latest worklog date is ${last.date}.`);
|
|
133
|
+
if (last && last.date === date) {
|
|
134
|
+
last.entries.push({ number: last.entries.length + 1, title, status, notes: [...notes] });
|
|
135
|
+
} else {
|
|
136
|
+
next.push({ date, entries: [{ number: 1, title, status, notes: [...notes] }] });
|
|
137
|
+
}
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveReference(blocks, reference) {
|
|
142
|
+
const value = String(reference || '').trim();
|
|
143
|
+
const match = /^(?:([^#]+)#)?([1-9]\d*)$/.exec(value);
|
|
144
|
+
if (!match) throw new Error('Worklog item reference must be a number or YYYY-MM-DD#number.');
|
|
145
|
+
const date = match[1];
|
|
146
|
+
const number = Number(match[2]);
|
|
147
|
+
const block = date ? blocks.find((entry) => entry.date === normalizeDate(date)) : blocks[blocks.length - 1];
|
|
148
|
+
if (!block) throw new Error(`No worklog entries exist for ${date || 'the latest date'}.`);
|
|
149
|
+
const entry = block.entries.find((item) => item.number === number);
|
|
150
|
+
if (!entry) throw new Error(`Worklog item ${value} was not found.`);
|
|
151
|
+
return { block, entry };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function updateItem(blocks, reference, status, note) {
|
|
155
|
+
const next = cloneBlocks(blocks);
|
|
156
|
+
const resolved = resolveReference(next, reference);
|
|
157
|
+
resolved.entry.status = status;
|
|
158
|
+
if (note) resolved.entry.notes.push(note);
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function temporaryPath(destination) {
|
|
163
|
+
return path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeTextAtomic(destination, content) {
|
|
167
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
168
|
+
const temporary = temporaryPath(destination);
|
|
169
|
+
let descriptor;
|
|
170
|
+
try {
|
|
171
|
+
descriptor = fs.openSync(temporary, 'wx', 0o666);
|
|
172
|
+
fs.writeFileSync(descriptor, content, 'utf8');
|
|
173
|
+
fs.fsyncSync(descriptor);
|
|
174
|
+
fs.closeSync(descriptor);
|
|
175
|
+
descriptor = undefined;
|
|
176
|
+
fs.renameSync(temporary, destination);
|
|
177
|
+
} finally {
|
|
178
|
+
if (descriptor !== undefined) {
|
|
179
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the original failure. */ }
|
|
180
|
+
}
|
|
181
|
+
try { fs.rmSync(temporary, { force: true }); } catch { /* Best-effort cleanup. */ }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function removeStaleLock(lockPath, staleMs) {
|
|
186
|
+
try {
|
|
187
|
+
if (Date.now() - fs.lstatSync(lockPath).mtimeMs < staleMs) return false;
|
|
188
|
+
fs.rmSync(lockPath, { force: true });
|
|
189
|
+
return true;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error && error.code === 'ENOENT') return true;
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function acquireLock(destination, options = {}) {
|
|
197
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
198
|
+
const lockPath = `${destination}.lock`;
|
|
199
|
+
const timeoutMs = Number(options.lockTimeoutMs ?? 10_000);
|
|
200
|
+
const staleMs = Number(options.lockStaleMs ?? 60_000);
|
|
201
|
+
const retryMs = Number(options.lockRetryMs ?? 10);
|
|
202
|
+
if (![timeoutMs, staleMs, retryMs].every(Number.isSafeInteger) || timeoutMs < 0 || staleMs < 1 || retryMs < 1) {
|
|
203
|
+
throw new TypeError('Invalid worklog lock timing options.');
|
|
204
|
+
}
|
|
205
|
+
const token = `${process.pid}:${crypto.randomBytes(16).toString('hex')}`;
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
while (true) {
|
|
208
|
+
let descriptor;
|
|
209
|
+
try {
|
|
210
|
+
descriptor = fs.openSync(lockPath, 'wx', 0o600);
|
|
211
|
+
fs.writeFileSync(descriptor, `${token}\n`, 'utf8');
|
|
212
|
+
fs.fsyncSync(descriptor);
|
|
213
|
+
fs.closeSync(descriptor);
|
|
214
|
+
return { path: lockPath, token };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (descriptor !== undefined) {
|
|
217
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the original failure. */ }
|
|
218
|
+
}
|
|
219
|
+
const retryable = error && ['EEXIST', 'EPERM', 'EACCES', 'EBUSY'].includes(error.code);
|
|
220
|
+
if (!retryable) throw error;
|
|
221
|
+
}
|
|
222
|
+
if (removeStaleLock(lockPath, staleMs)) continue;
|
|
223
|
+
const elapsed = Date.now() - started;
|
|
224
|
+
if (elapsed >= timeoutMs) {
|
|
225
|
+
const error = new Error('Timed out waiting for .draftgo/worklog.md.lock.');
|
|
226
|
+
error.code = 'WORKLOG_LOCK_TIMEOUT';
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
Atomics.wait(LOCK_WAIT_ARRAY, 0, 0, Math.min(retryMs, timeoutMs - elapsed));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function releaseLock(lock) {
|
|
234
|
+
try {
|
|
235
|
+
if (fs.readFileSync(lock.path, 'utf8').trim() === lock.token) fs.rmSync(lock.path, { force: true });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function mutateWorklog(projectDir, mutate, options = {}) {
|
|
242
|
+
const file = worklogPath(projectDir);
|
|
243
|
+
const lock = acquireLock(file, options);
|
|
244
|
+
try {
|
|
245
|
+
const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
246
|
+
const blocks = parseWorklog(source);
|
|
247
|
+
const result = mutate(blocks);
|
|
248
|
+
const next = result && result.blocks ? result.blocks : result;
|
|
249
|
+
writeTextAtomic(file, renderWorklog(next));
|
|
250
|
+
return { ...(result && result.blocks ? result : { blocks: next }), path: file };
|
|
251
|
+
} finally {
|
|
252
|
+
releaseLock(lock);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readWorklog(projectDir) {
|
|
257
|
+
const file = worklogPath(projectDir);
|
|
258
|
+
return parseWorklog(fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
STATUS,
|
|
263
|
+
formatLocalDate,
|
|
264
|
+
worklogPath,
|
|
265
|
+
normalizeDate,
|
|
266
|
+
parseWorklog,
|
|
267
|
+
renderWorklog,
|
|
268
|
+
appendItem,
|
|
269
|
+
resolveReference,
|
|
270
|
+
updateItem,
|
|
271
|
+
mutateWorklog,
|
|
272
|
+
readWorklog,
|
|
273
|
+
acquireLock,
|
|
274
|
+
releaseLock,
|
|
275
|
+
writeTextAtomic,
|
|
276
|
+
};
|