draftgo-cli 3.0.56 → 4.0.1
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 +112 -316
- package/package.json +5 -5
- package/resources/skill/SKILL.md +25 -24
- package/resources/skill/init/SKILL.md +5 -10
- package/resources/skill/manifest.json +2 -2
- package/resources/skill/references/aihub.md +3 -3
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/custom-services.md +65 -228
- package/resources/skill/references/data.md +3 -2
- package/resources/skill/references/frontend.md +96 -490
- package/resources/skill/references/mcp.md +39 -103
- package/resources/skill/references/runtime.md +3 -2
- package/resources/skill/story/SKILL.md +1 -2
- package/src/apiContractCache.js +112 -0
- package/src/cli.js +1 -21
- package/src/commandRegistry.js +6 -11
- package/src/commands/api.js +28 -8
- package/src/commands/check.js +1 -10
- package/src/commands/customService.js +2 -4
- package/src/commands/delete.js +23 -46
- package/src/commands/deploy.js +1 -1
- package/src/commands/help.js +16 -31
- package/src/commands/init.js +4 -10
- package/src/commands/listTargets.js +1 -1
- package/src/commands/local.js +2 -6
- package/src/commands/map.js +0 -11
- package/src/commands/status.js +1 -1
- package/src/commands/uninstall.js +3 -3
- package/src/commands/update.js +1 -1
- package/src/commands/verify.js +43 -21
- package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
- package/src/commands/worklog.js +86 -0
- package/src/customServices.js +150 -33
- package/src/{localdev → localRuntime}/detect.js +1 -1
- package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
- package/src/{localdev → localRuntime}/services.js +1 -1
- package/src/projectConfig.js +2 -0
- package/src/{installers/index.js → targets.js} +3 -5
- package/src/worklog.js +274 -0
- package/src/workspaceHealth.js +1 -1
- package/src/worktree/index.js +81 -51
- package/src/changelog.js +0 -276
- package/src/commands/changelog.js +0 -24
- package/src/commands/localDev.js +0 -9
- package/src/commands/sync.js +0 -46
- package/src/commands/task.js +0 -408
- package/src/commands/verifyUiCompat.js +0 -16
- /package/src/{localdev → localRuntime}/compose.js +0 -0
- /package/src/{localdev → localRuntime}/index.js +0 -0
package/src/commands/task.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const log = require('../logger');
|
|
7
|
-
const runtimeFiles = require('../runtimeFiles');
|
|
8
|
-
|
|
9
|
-
const TASK_ROOT = path.join('.draftgo', 'Task');
|
|
10
|
-
const TASK_FILENAME = 'Task.md';
|
|
11
|
-
const REQUIRED_ITEM_FIELDS = ['内容', '负责人', '验收'];
|
|
12
|
-
|
|
13
|
-
function root(projectDir) { return path.join(projectDir, TASK_ROOT); }
|
|
14
|
-
function taskDir(projectDir, id) { return path.join(root(projectDir), String(id)); }
|
|
15
|
-
function markdownFile(projectDir, id) { return path.join(taskDir(projectDir, id), TASK_FILENAME); }
|
|
16
|
-
function legacyFile(projectDir, id) { return path.join(taskDir(projectDir, id), 'task.yaml'); }
|
|
17
|
-
|
|
18
|
-
function atomicWrite(file, value, options = {}) {
|
|
19
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
20
|
-
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
21
|
-
fs.writeFileSync(temp, value, 'utf8');
|
|
22
|
-
try { runtimeFiles.replaceFile(temp, file, options.rename, options.wait); } catch (error) {
|
|
23
|
-
try { fs.rmSync(temp, { force: true }); } catch { /* retain the original rename error */ }
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function withLock(projectDir, taskId, action) {
|
|
29
|
-
const file = path.join(taskDir(projectDir, taskId), '.lock');
|
|
30
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
-
let descriptor;
|
|
32
|
-
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
33
|
-
try { descriptor = fs.openSync(file, 'wx'); break; } catch (error) {
|
|
34
|
-
if (error.code !== 'EEXIST') throw error;
|
|
35
|
-
const started = Date.now(); while (Date.now() - started < 20) { /* short lock wait */ }
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (descriptor === undefined) throw new Error(`Task ${taskId} is locked by another Agent.`);
|
|
39
|
-
try {
|
|
40
|
-
fs.writeFileSync(descriptor, `${process.pid}\n`);
|
|
41
|
-
const target = markdownFile(projectDir, taskId);
|
|
42
|
-
const before = fs.existsSync(target) ? digest(fs.readFileSync(target)) : null;
|
|
43
|
-
return action(before);
|
|
44
|
-
} finally {
|
|
45
|
-
fs.closeSync(descriptor);
|
|
46
|
-
fs.rmSync(file, { force: true });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function digest(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
|
|
51
|
-
function normalizeNewlines(value) { return String(value || '').replace(/\r\n/g, '\n'); }
|
|
52
|
-
function slugify(value) {
|
|
53
|
-
return String(value || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 48) || 'task';
|
|
54
|
-
}
|
|
55
|
-
function today(flags = {}) { return String(flags.date || new Date().toISOString().slice(0, 10)); }
|
|
56
|
-
function uniqueTaskId(projectDir, title, flags = {}) {
|
|
57
|
-
const requested = String(flags.id || '').trim();
|
|
58
|
-
let base;
|
|
59
|
-
if (/^\[\d{4}-\d{2}-\d{2}\][a-z0-9][a-z0-9-]*$/.test(requested)) base = requested;
|
|
60
|
-
else base = `[${today(flags)}]${slugify(requested || title)}`;
|
|
61
|
-
let candidate = base; let suffix = 2;
|
|
62
|
-
while (fs.existsSync(taskDir(projectDir, candidate))) { candidate = `${base}-${suffix}`; suffix += 1; }
|
|
63
|
-
return candidate;
|
|
64
|
-
}
|
|
65
|
-
function field(flags, names, fallback = '') {
|
|
66
|
-
for (const name of names) if (flags[name] !== undefined) return String(flags[name]).trim();
|
|
67
|
-
return fallback;
|
|
68
|
-
}
|
|
69
|
-
function lines(value, fallback = '- 暂无。') {
|
|
70
|
-
const text = String(value || '').trim();
|
|
71
|
-
if (!text) return fallback;
|
|
72
|
-
return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
73
|
-
.map((line) => line.startsWith('- ') ? line : `- ${line}`).join('\n');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function template(task) {
|
|
77
|
-
return `# ${task.title}\n\n`+
|
|
78
|
-
`- 状态:active\n- 创建时间:${task.created_at}\n- 更新时间:${task.updated_at}\n\n`+
|
|
79
|
-
`## 用户原始需求\n\n${task.original_requirement}\n\n`+
|
|
80
|
-
`## 明确后的需求\n\n${task.clarified_requirement}\n\n`+
|
|
81
|
-
`## 预计效果\n\n${lines(task.expected_effect)}\n\n`+
|
|
82
|
-
`## 本次范围\n\n${lines(task.scope)}\n\n`+
|
|
83
|
-
`## 不在本次范围\n\n${lines(task.out_of_scope)}\n\n`+
|
|
84
|
-
'## 开发清单\n\n' +
|
|
85
|
-
'暂无。\n\n' +
|
|
86
|
-
'## 关键决策\n\n- 暂无。\n\n' +
|
|
87
|
-
'## 阻塞问题\n\n- 暂无。\n\n' +
|
|
88
|
-
'## 最终验收\n\n' +
|
|
89
|
-
'- [ ] 所有开发项完成\n' +
|
|
90
|
-
'- [ ] 完成验证\n' +
|
|
91
|
-
'- [ ] changelog 已写入\n';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function section(body, name) {
|
|
95
|
-
const pattern = new RegExp(`^## ${escapeRegExp(name)}\\s*$`, 'm');
|
|
96
|
-
const match = pattern.exec(body);
|
|
97
|
-
if (!match) return '';
|
|
98
|
-
const start = match.index + match[0].length;
|
|
99
|
-
const rest = body.slice(start);
|
|
100
|
-
const next = /^##\s+/m.exec(rest);
|
|
101
|
-
return rest.slice(0, next ? next.index : rest.length).trim();
|
|
102
|
-
}
|
|
103
|
-
function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
104
|
-
function metadata(body, name) {
|
|
105
|
-
const match = body.match(new RegExp(`^- ${escapeRegExp(name)}[::]\\s*(.*)$`, 'm'));
|
|
106
|
-
return match ? match[1].trim() : '';
|
|
107
|
-
}
|
|
108
|
-
function parseChecklist(text, final = false) {
|
|
109
|
-
const body = normalizeNewlines(text); const entries = [];
|
|
110
|
-
const pattern = /^- \[([ xX])\]\s+(.+)$/gm; const matches = [...body.matchAll(pattern)];
|
|
111
|
-
for (let index = 0; index < matches.length; index += 1) {
|
|
112
|
-
const match = matches[index]; const start = match.index; const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
|
|
113
|
-
const block = body.slice(start, end).trimEnd();
|
|
114
|
-
if (final) {
|
|
115
|
-
entries.push({ title: match[2].trim(), completed: match[1].toLowerCase() === 'x', block });
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const heading = match[2].trim().match(/^(DG-\d{3})\s+(.+)$/);
|
|
119
|
-
if (!heading) continue;
|
|
120
|
-
const fields = {};
|
|
121
|
-
for (const line of block.split('\n').slice(1)) {
|
|
122
|
-
const detail = line.match(/^\s{2,}-\s+([^::]+)[::]\s*(.*)$/);
|
|
123
|
-
if (detail) fields[detail[1].trim()] = detail[2].trim();
|
|
124
|
-
}
|
|
125
|
-
entries.push({ id: heading[1], title: heading[2], completed: match[1].toLowerCase() === 'x', status: fields['状态'] || (match[1].toLowerCase() === 'x' ? 'completed' : 'pending'), fields, block });
|
|
126
|
-
}
|
|
127
|
-
return entries;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseMarkdown(file, id) {
|
|
131
|
-
const body = normalizeNewlines(fs.readFileSync(file, 'utf8'));
|
|
132
|
-
const title = (body.match(/^#\s+(.+)$/m) || [null, id])[1].trim();
|
|
133
|
-
const items = parseChecklist(section(body, '开发清单'));
|
|
134
|
-
const acceptance = parseChecklist(section(body, '最终验收'), true);
|
|
135
|
-
return {
|
|
136
|
-
schema_version: 2, format: 'markdown', id, title, status: metadata(body, '状态') || 'active',
|
|
137
|
-
created_at: metadata(body, '创建时间'), updated_at: metadata(body, '更新时间'),
|
|
138
|
-
original_requirement: section(body, '用户原始需求'), clarified_requirement: section(body, '明确后的需求'),
|
|
139
|
-
expected_effect: section(body, '预计效果'), scope: section(body, '本次范围'), out_of_scope: section(body, '不在本次范围'),
|
|
140
|
-
decisions: section(body, '关键决策'), blockers: section(body, '阻塞问题'), items, acceptance,
|
|
141
|
-
completed_items: items.filter((item) => item.completed).length, total_items: items.length,
|
|
142
|
-
body, file,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function unquote(value) {
|
|
147
|
-
const text = String(value || '').trim();
|
|
148
|
-
if (text === '[]') return [];
|
|
149
|
-
try { return JSON.parse(text); } catch { return text.replace(/^['"]|['"]$/g, ''); }
|
|
150
|
-
}
|
|
151
|
-
function parseLegacy(file, id) {
|
|
152
|
-
const body = normalizeNewlines(fs.readFileSync(file, 'utf8')); const result = { schema_version: 1, format: 'legacy', id, items: [], body, file };
|
|
153
|
-
let current = null;
|
|
154
|
-
for (const line of body.split('\n')) {
|
|
155
|
-
const item = line.match(/^\s*-\s+id:\s*(.+)$/);
|
|
156
|
-
if (item) { current = { id: unquote(item[1]) }; result.items.push(current); continue; }
|
|
157
|
-
const value = line.match(/^(\w+):\s*(.*)$/) || line.match(/^\s{4}(\w+):\s*(.*)$/);
|
|
158
|
-
if (!value) continue;
|
|
159
|
-
if (!line.startsWith(' ') && value[1] === 'items') continue;
|
|
160
|
-
const target = line.startsWith(' ') && current ? current : result;
|
|
161
|
-
target[value[1]] = unquote(value[2]);
|
|
162
|
-
}
|
|
163
|
-
result.completed_items = result.items.filter((item) => item.status === 'completed').length;
|
|
164
|
-
result.total_items = result.items.length;
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function resolveTaskId(projectDir, value) {
|
|
169
|
-
const requested = String(value || '').trim();
|
|
170
|
-
if (!requested) throw new Error('Task id is required.');
|
|
171
|
-
if (fs.existsSync(taskDir(projectDir, requested))) return requested;
|
|
172
|
-
const directory = root(projectDir);
|
|
173
|
-
if (!fs.existsSync(directory)) throw new Error(`Task ${requested} does not exist.`);
|
|
174
|
-
const shortName = new RegExp(`^\\[\\d{4}-\\d{2}-\\d{2}\\]${escapeRegExp(requested)}(?:-\\d+)?$`);
|
|
175
|
-
const matches = fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === requested || shortName.test(entry.name)));
|
|
176
|
-
if (matches.length === 1) return matches[0].name;
|
|
177
|
-
if (matches.length > 1) throw new Error(`Task ${requested} is ambiguous; use its full directory name.`);
|
|
178
|
-
throw new Error(`Task ${requested} does not exist.`);
|
|
179
|
-
}
|
|
180
|
-
function loadTask(projectDir, value) {
|
|
181
|
-
const id = resolveTaskId(projectDir, value);
|
|
182
|
-
const modern = markdownFile(projectDir, id); const legacy = legacyFile(projectDir, id);
|
|
183
|
-
if (fs.existsSync(modern)) return parseMarkdown(modern, id);
|
|
184
|
-
if (fs.existsSync(legacy)) return parseLegacy(legacy, id);
|
|
185
|
-
throw new Error(`Task ${id} has no ${TASK_FILENAME} or task.yaml.`);
|
|
186
|
-
}
|
|
187
|
-
function publicTask(task) {
|
|
188
|
-
const result = { ...task }; delete result.body; delete result.file; return result;
|
|
189
|
-
}
|
|
190
|
-
function print(flags, value) {
|
|
191
|
-
if (flags.output === 'json') console.log(JSON.stringify(Array.isArray(value) ? value.map(publicTask) : publicTask(value), null, 2));
|
|
192
|
-
}
|
|
193
|
-
function taskActor(flags) { return String(flags.owner || flags.assignee || process.env.DRAFTGO_AGENT_ID || '').trim(); }
|
|
194
|
-
function assertModern(task, operation) {
|
|
195
|
-
if (task.format === 'legacy') throw new Error(`Legacy Task ${task.id} is read-only; run task migrate ${task.id} before ${operation}.`);
|
|
196
|
-
}
|
|
197
|
-
function saveBody(task, body, beforeHash) {
|
|
198
|
-
const current = fs.readFileSync(task.file);
|
|
199
|
-
if (beforeHash && digest(current) !== beforeHash) throw new Error(`Task ${task.id} changed while it was being updated; retry the command.`);
|
|
200
|
-
atomicWrite(task.file, body.endsWith('\n') ? body : `${body}\n`);
|
|
201
|
-
}
|
|
202
|
-
function replaceMetadata(body, name, value) {
|
|
203
|
-
const pattern = new RegExp(`^- ${escapeRegExp(name)}[::].*$`, 'm');
|
|
204
|
-
if (!pattern.test(body)) throw new Error(`Task.md is missing metadata field ${name}.`);
|
|
205
|
-
return body.replace(pattern, `- ${name}:${value}`);
|
|
206
|
-
}
|
|
207
|
-
function touch(task, body, status) {
|
|
208
|
-
let result = replaceMetadata(body, '更新时间', new Date().toISOString());
|
|
209
|
-
if (status) result = replaceMetadata(result, '状态', status);
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
212
|
-
function replaceSection(body, name, content) {
|
|
213
|
-
const pattern = new RegExp(`(^## ${escapeRegExp(name)}\\s*$\\n)([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, 'm');
|
|
214
|
-
if (!pattern.test(body)) throw new Error(`Task.md is missing section ${name}.`);
|
|
215
|
-
return body.replace(pattern, `$1\n${content.trim()}\n\n`);
|
|
216
|
-
}
|
|
217
|
-
function replaceItemBlock(task, itemId, replacement) {
|
|
218
|
-
const checklist = section(task.body, '开发清单'); const item = task.items.find((entry) => entry.id === itemId);
|
|
219
|
-
if (!item) throw new Error(`Task item ${itemId} does not exist.`);
|
|
220
|
-
return replaceSection(task.body, '开发清单', checklist.replace(item.block, replacement));
|
|
221
|
-
}
|
|
222
|
-
function upsertItemField(block, name, value) {
|
|
223
|
-
const pattern = new RegExp(`^ - ${escapeRegExp(name)}[::].*$`, 'm');
|
|
224
|
-
if (pattern.test(block)) return block.replace(pattern, ` - ${name}:${value}`);
|
|
225
|
-
return `${block.trimEnd()}\n - ${name}:${value}`;
|
|
226
|
-
}
|
|
227
|
-
function validateItemFields(item) {
|
|
228
|
-
const missing = REQUIRED_ITEM_FIELDS.filter((name) => !String(item.fields[name] || '').trim());
|
|
229
|
-
if (missing.length) throw new Error(`${item.id} is missing required field(s): ${missing.join(', ')}.`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function create(projectDir, positional, flags) {
|
|
233
|
-
const title = field(flags, ['title'], positional.join(' '));
|
|
234
|
-
if (!title) throw new Error('task create requires a title or --title.');
|
|
235
|
-
const original = field(flags, ['original', 'original-requirement', 'description']);
|
|
236
|
-
const clarified = field(flags, ['clarified', 'clarified-requirement'], original);
|
|
237
|
-
const expected = field(flags, ['expected-effect', 'effect']);
|
|
238
|
-
if (!original) throw new Error('task create requires --original with the user requirement.');
|
|
239
|
-
if (!clarified) throw new Error('task create requires --clarified with the executable requirement.');
|
|
240
|
-
if (!expected) throw new Error('task create requires --expected-effect.');
|
|
241
|
-
const id = uniqueTaskId(projectDir, title, flags); const now = new Date().toISOString();
|
|
242
|
-
const task = { id, title, created_at: now, updated_at: now, original_requirement: original, clarified_requirement: clarified,
|
|
243
|
-
expected_effect: expected, scope: field(flags, ['scope']), out_of_scope: field(flags, ['out-of-scope']) };
|
|
244
|
-
fs.mkdirSync(taskDir(projectDir, id), { recursive: true }); atomicWrite(markdownFile(projectDir, id), template(task));
|
|
245
|
-
const loaded = loadTask(projectDir, id); print(flags, loaded); if (flags.output !== 'json') log.ok(`Created task ${id}`); return 0;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function list(projectDir, flags) {
|
|
249
|
-
const tasks = []; const directory = root(projectDir);
|
|
250
|
-
if (fs.existsSync(directory)) for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
251
|
-
if (!entry.isDirectory()) continue;
|
|
252
|
-
try { tasks.push(loadTask(projectDir, entry.name)); } catch { /* ignore non-task directories */ }
|
|
253
|
-
}
|
|
254
|
-
tasks.sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')));
|
|
255
|
-
print(flags, tasks);
|
|
256
|
-
if (flags.output !== 'json') tasks.forEach((task) => log.info(`${task.id} [${task.status}] ${task.title} (${task.completed_items}/${task.total_items})${task.format === 'legacy' ? ' [legacy]' : ''}`));
|
|
257
|
-
return 0;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function show(projectDir, taskId, flags) {
|
|
261
|
-
const task = loadTask(projectDir, taskId); print(flags, task);
|
|
262
|
-
if (flags.output !== 'json') {
|
|
263
|
-
const next = task.items.find((item) => !item.completed);
|
|
264
|
-
log.info(`${task.id} [${task.status}] ${task.title}\nProgress: ${task.completed_items}/${task.total_items}\n${next ? `Next: ${next.id} ${next.title}` : 'Next: final acceptance'}`);
|
|
265
|
-
}
|
|
266
|
-
return 0;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function nextItemId(items) {
|
|
270
|
-
const highest = items.reduce((max, item) => Math.max(max, Number(item.id.match(/^DG-(\d+)$/)?.[1] || 0)), 0);
|
|
271
|
-
return `DG-${String(highest + 1).padStart(3, '0')}`;
|
|
272
|
-
}
|
|
273
|
-
function add(projectDir, taskId, positional, flags) {
|
|
274
|
-
const resolved = resolveTaskId(projectDir, taskId);
|
|
275
|
-
return withLock(projectDir, resolved, (before) => {
|
|
276
|
-
const task = loadTask(projectDir, resolved); assertModern(task, 'add');
|
|
277
|
-
const title = field(flags, ['title'], positional.join(' ')); const content = field(flags, ['content']); const acceptance = field(flags, ['acceptance']);
|
|
278
|
-
const owner = field(flags, ['assignee'], taskActor(flags));
|
|
279
|
-
if (!title) throw new Error('task add requires a title or --title.');
|
|
280
|
-
if (!content) throw new Error('task add requires --content.');
|
|
281
|
-
if (!owner) throw new Error('task add requires --assignee or owner identity.');
|
|
282
|
-
if (!acceptance) throw new Error('task add requires --acceptance.');
|
|
283
|
-
const id = String(flags.id || nextItemId(task.items));
|
|
284
|
-
if (!/^DG-\d{3}$/.test(id)) throw new Error('Task item id must use DG-001 format.');
|
|
285
|
-
if (task.items.some((item) => item.id === id)) throw new Error(`Task item ${id} already exists.`);
|
|
286
|
-
let block = `- [ ] ${id} ${title}\n - 内容:${content}\n - 负责人:${owner}`;
|
|
287
|
-
const moduleLogic = field(flags, ['module-logic']); const userJourney = field(flags, ['user-journey']);
|
|
288
|
-
if (moduleLogic) block += `\n - 模块逻辑:${moduleLogic}`;
|
|
289
|
-
if (userJourney) block += `\n - 用户路线:${userJourney}`;
|
|
290
|
-
block += `\n - 验收:${acceptance}`;
|
|
291
|
-
const current = section(task.body, '开发清单'); const contentSection = current === '暂无。' ? block : `${current.trimEnd()}\n\n${block}`;
|
|
292
|
-
const body = touch(task, replaceSection(task.body, '开发清单', contentSection), 'active'); saveBody(task, body, before);
|
|
293
|
-
const updated = loadTask(projectDir, resolved); const item = updated.items.find((entry) => entry.id === id); print(flags, item);
|
|
294
|
-
if (flags.output !== 'json') log.ok(`Added ${id}`); return 0;
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function mutate(projectDir, taskId, itemId, operation, flags, trailing = []) {
|
|
299
|
-
const resolved = resolveTaskId(projectDir, taskId);
|
|
300
|
-
return withLock(projectDir, resolved, (before) => {
|
|
301
|
-
const task = loadTask(projectDir, resolved); assertModern(task, operation); const item = task.items.find((entry) => entry.id === itemId);
|
|
302
|
-
if (!item) throw new Error(`Task item ${itemId} does not exist.`); validateItemFields(item);
|
|
303
|
-
const actor = taskActor(flags); if (!actor) throw new Error(`${itemId} requires an owner identity; pass --owner/--assignee or set DRAFTGO_AGENT_ID.`);
|
|
304
|
-
const assigned = item.fields['负责人']; if (assigned && assigned !== actor && operation !== 'claim') throw new Error(`${itemId} can only be updated by owner ${assigned}.`);
|
|
305
|
-
if (operation === 'claim' && assigned && assigned !== actor) throw new Error(`${itemId} is already owned by ${assigned}.`);
|
|
306
|
-
let block = item.block;
|
|
307
|
-
if (operation === 'complete') {
|
|
308
|
-
const evidence = field(flags, ['evidence'], trailing.join(' '));
|
|
309
|
-
if (!evidence) throw new Error(`${itemId} requires --evidence before completion.`);
|
|
310
|
-
block = block.replace(/^- \[[ xX]\]/, '- [x]'); block = upsertItemField(block, '状态', 'completed'); block = upsertItemField(block, '完成结果', evidence);
|
|
311
|
-
} else if (operation === 'block') {
|
|
312
|
-
const reason = field(flags, ['evidence'], trailing.join(' ')); if (!reason) throw new Error(`${itemId} block requires a reason.`);
|
|
313
|
-
block = block.replace(/^- \[[ xX]\]/, '- [ ]'); block = upsertItemField(block, '状态', 'blocked'); block = upsertItemField(block, '阻塞原因', reason);
|
|
314
|
-
} else if (operation === 'reopen') {
|
|
315
|
-
block = block.replace(/^- \[[ xX]\]/, '- [ ]'); block = upsertItemField(block, '状态', 'pending');
|
|
316
|
-
} else {
|
|
317
|
-
block = upsertItemField(block, '负责人', actor); block = upsertItemField(block, '状态', operation === 'start' ? 'in_progress' : 'claimed');
|
|
318
|
-
}
|
|
319
|
-
let body = replaceItemBlock(task, itemId, block); let reloaded = { ...task, body };
|
|
320
|
-
const items = parseChecklist(section(body, '开发清单'));
|
|
321
|
-
if (items.length && items.every((entry) => entry.completed)) body = updateAcceptanceBody(body, '所有开发项完成', true, 'CLI automatically confirmed all development items.');
|
|
322
|
-
else body = updateAcceptanceBody(body, '所有开发项完成', false, 'Development work is still open.');
|
|
323
|
-
body = touch(reloaded, body, 'active'); saveBody(task, body, before);
|
|
324
|
-
const updated = loadTask(projectDir, resolved).items.find((entry) => entry.id === itemId); print(flags, updated);
|
|
325
|
-
if (flags.output !== 'json') log.ok(`${itemId}: ${updated.status}`); return 0;
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function updateAcceptanceBody(body, title, completed, evidence) {
|
|
330
|
-
const current = section(body, '最终验收'); const entries = parseChecklist(current, true); const target = entries.find((entry) => entry.title === title);
|
|
331
|
-
if (!target) throw new Error(`Final acceptance item ${title} does not exist.`);
|
|
332
|
-
let block = target.block.replace(/^- \[[ xX]\]/, completed ? '- [x]' : '- [ ]');
|
|
333
|
-
if (evidence) block = upsertItemField(block, '验证结果', evidence);
|
|
334
|
-
return replaceSection(body, '最终验收', current.replace(target.block, block));
|
|
335
|
-
}
|
|
336
|
-
function accept(projectDir, taskId, positional, flags) {
|
|
337
|
-
const resolved = resolveTaskId(projectDir, taskId); const title = field(flags, ['item'], positional.join(' '));
|
|
338
|
-
if (!title) throw new Error('task accept requires a final acceptance item title.');
|
|
339
|
-
return withLock(projectDir, resolved, (before) => {
|
|
340
|
-
const task = loadTask(projectDir, resolved); assertModern(task, 'accept');
|
|
341
|
-
const evidence = field(flags, ['evidence']); if (!evidence) throw new Error('task accept requires --evidence.');
|
|
342
|
-
const body = touch(task, updateAcceptanceBody(task.body, title, true, evidence), 'active'); saveBody(task, body, before);
|
|
343
|
-
const updated = loadTask(projectDir, resolved); print(flags, updated.acceptance.find((entry) => entry.title === title));
|
|
344
|
-
if (flags.output !== 'json') log.ok(`Accepted: ${title}`); return 0;
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function finish(projectDir, taskId, flags) {
|
|
349
|
-
const resolved = resolveTaskId(projectDir, taskId);
|
|
350
|
-
return withLock(projectDir, resolved, (before) => {
|
|
351
|
-
const task = loadTask(projectDir, resolved); assertModern(task, 'finish');
|
|
352
|
-
if (task.items.some((item) => !item.completed)) throw new Error(`Cannot finish ${task.id}; development items remain incomplete.`);
|
|
353
|
-
const pending = task.acceptance.filter((item) => !item.completed);
|
|
354
|
-
if (pending.length) throw new Error(`Cannot finish ${task.id}; final acceptance remains incomplete: ${pending.map((item) => item.title).join(', ')}.`);
|
|
355
|
-
const body = touch(task, task.body, 'completed'); saveBody(task, body, before);
|
|
356
|
-
const updated = loadTask(projectDir, resolved); print(flags, updated); if (flags.output !== 'json') log.ok(`Finished task ${resolved}`); return 0;
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function legacyItemDetails(projectDir, task) {
|
|
361
|
-
return task.items.map((item) => {
|
|
362
|
-
const file = path.join(taskDir(projectDir, task.id), 'items', `${item.id}.md`);
|
|
363
|
-
const body = fs.existsSync(file) ? normalizeNewlines(fs.readFileSync(file, 'utf8')) : '';
|
|
364
|
-
const evidence = section(body, 'Evidence').split('\n').map((line) => line.replace(/^-\s*/, '').trim()).filter(Boolean);
|
|
365
|
-
return { ...item, title: item.title || item.id, evidence };
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
function migrate(projectDir, taskId, flags) {
|
|
369
|
-
const resolved = resolveTaskId(projectDir, taskId);
|
|
370
|
-
return withLock(projectDir, resolved, () => {
|
|
371
|
-
const task = loadTask(projectDir, resolved);
|
|
372
|
-
if (task.format !== 'legacy') throw new Error(`Task ${resolved} already uses ${TASK_FILENAME}.`);
|
|
373
|
-
const items = legacyItemDetails(projectDir, task); const now = new Date().toISOString();
|
|
374
|
-
const original = field(flags, ['original'], task.title || resolved); const clarified = field(flags, ['clarified'], original);
|
|
375
|
-
const expected = field(flags, ['expected-effect'], '保留历史任务的目标、进度和验收证据,并使用单文件继续维护。');
|
|
376
|
-
const defaultOwner = task.owner || taskActor(flags);
|
|
377
|
-
const modern = { title: task.title || resolved, created_at: task.created_at || now, updated_at: now, original_requirement: original,
|
|
378
|
-
clarified_requirement: clarified, expected_effect: expected, scope: '', out_of_scope: '' };
|
|
379
|
-
let body = template(modern); const blocks = items.map((item, index) => {
|
|
380
|
-
const id = /^DG-\d{3}$/.test(item.id) ? item.id : `DG-${String(index + 1).padStart(3, '0')}`;
|
|
381
|
-
const owner = item.owner || defaultOwner || 'unassigned'; const evidence = item.evidence.join('; ') || 'Legacy task state migrated.';
|
|
382
|
-
return `- [${item.status === 'completed' ? 'x' : ' '}] ${id} ${item.title}\n - 内容:从历史 Task 迁移。\n - 负责人:${owner}\n - 验收:保留历史工作项状态与证据。\n - 状态:${item.status || 'pending'}${item.status === 'completed' ? `\n - 完成结果:${evidence}` : ''}`;
|
|
383
|
-
});
|
|
384
|
-
body = replaceSection(body, '开发清单', blocks.length ? blocks.join('\n\n') : '暂无。');
|
|
385
|
-
if (blocks.length && items.every((item) => item.status === 'completed')) body = updateAcceptanceBody(body, '所有开发项完成', true, 'Migrated from completed legacy items.');
|
|
386
|
-
atomicWrite(markdownFile(projectDir, resolved), body);
|
|
387
|
-
const updated = loadTask(projectDir, resolved); print(flags, updated); if (flags.output !== 'json') log.ok(`Migrated task ${resolved} to ${TASK_FILENAME}; legacy files were retained.`); return 0;
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async function task(projectDir, positional, flags = {}) {
|
|
392
|
-
const operation = positional[0] || 'list';
|
|
393
|
-
if (operation === 'create') return create(projectDir, positional.slice(1), flags);
|
|
394
|
-
if (operation === 'list') return list(projectDir, flags);
|
|
395
|
-
if (operation === 'show' || operation === 'status') return show(projectDir, positional[1], flags);
|
|
396
|
-
if (operation === 'add') return add(projectDir, positional[1], positional.slice(2), flags);
|
|
397
|
-
if (operation === 'accept') return accept(projectDir, positional[1], positional.slice(2), flags);
|
|
398
|
-
if (operation === 'finish') return finish(projectDir, positional[1], flags);
|
|
399
|
-
if (operation === 'migrate') return migrate(projectDir, positional[1], flags);
|
|
400
|
-
if (['claim', 'start', 'complete', 'block', 'reopen'].includes(operation)) return mutate(projectDir, positional[1], positional[2], operation, flags, positional.slice(3));
|
|
401
|
-
throw new Error(`Unknown task operation ${operation}.`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
module.exports = task;
|
|
405
|
-
module.exports.atomicWrite = atomicWrite;
|
|
406
|
-
module.exports.loadTask = loadTask;
|
|
407
|
-
module.exports.parseMarkdown = parseMarkdown;
|
|
408
|
-
module.exports.uniqueTaskId = uniqueTaskId;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const log = require('../logger');
|
|
4
|
-
const verify = require('./verify');
|
|
5
|
-
|
|
6
|
-
async function verifyUiCompat(projectDir, positional = [], flags = {}) {
|
|
7
|
-
const url = String(flags.url || positional[0] || '').trim();
|
|
8
|
-
log.warn('`draftgo verify-ui` is deprecated; use `draftgo verify --url <url> --ui always`.');
|
|
9
|
-
return verify(projectDir, [], {
|
|
10
|
-
...flags,
|
|
11
|
-
url,
|
|
12
|
-
ui: 'always',
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
module.exports = verifyUiCompat;
|
|
File without changes
|
|
File without changes
|