sdd-toolkit 1.9.2 → 2.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/definitions/sdd-coder.yaml +161 -71
- package/definitions/sdd-feature.yaml +139 -73
- package/definitions/sdd-log.yaml +70 -61
- package/definitions/sdd-project.yaml +163 -51
- package/definitions/sdd-requirements.yaml +70 -70
- package/definitions/sdd-review.yaml +106 -88
- package/definitions/sdd.yaml +12 -12
- package/package.json +1 -1
- package/src/commands/view.js +18 -18
- package/src/index.js +98 -98
- package/src/lib/dashboard.js +187 -187
- package/src/lib/docs.js +71 -69
- package/src/lib/i18n.js +65 -65
- package/src/lib/messages.js +234 -234
- package/src/lib/profiles.js +186 -186
- package/src/lib/schema.js +13 -13
- package/src/lib/transformers.js +326 -332
- package/src/scripts/archive.js +55 -56
- package/src/scripts/reset.js +19 -19
- package/src/scripts/status.js +50 -58
- package/templates/context.md +22 -0
- package/templates/guidelines.md +9 -9
- package/templates/project.md +28 -28
- package/templates/requirements.md +15 -15
- package/templates/system.md +20 -0
- package/templates/task.md +11 -11
- package/README.pt.md +0 -334
package/src/commands/view.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
const { renderDashboard } = require('../lib/dashboard');
|
|
2
|
-
const { spinner } = require('@clack/prompts');
|
|
3
|
-
const pc = require('picocolors');
|
|
4
|
-
|
|
5
|
-
async function view() {
|
|
6
|
-
const s = spinner();
|
|
7
|
-
s.start('Carregando Dashboard...');
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
await renderDashboard();
|
|
11
|
-
s.stop('Dashboard atualizado.');
|
|
12
|
-
} catch (e) {
|
|
13
|
-
s.stop(pc.red('Erro ao carregar dashboard.'));
|
|
14
|
-
console.error(e);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
module.exports = { view };
|
|
1
|
+
const { renderDashboard } = require('../lib/dashboard');
|
|
2
|
+
const { spinner } = require('@clack/prompts');
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
|
|
5
|
+
async function view() {
|
|
6
|
+
const s = spinner();
|
|
7
|
+
s.start('Carregando Dashboard...');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await renderDashboard();
|
|
11
|
+
s.stop('Dashboard atualizado.');
|
|
12
|
+
} catch (e) {
|
|
13
|
+
s.stop(pc.red('Erro ao carregar dashboard.'));
|
|
14
|
+
console.error(e);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { view };
|
package/src/index.js
CHANGED
|
@@ -72,7 +72,7 @@ async function main() {
|
|
|
72
72
|
if (fs.existsSync(path.join(process.cwd(), '.claude'))) tools.push('claude');
|
|
73
73
|
if (fs.existsSync(path.join(process.cwd(), '.trae'))) tools.push('trae');
|
|
74
74
|
if (fs.existsSync(path.join(process.cwd(), '.kilocode'))) tools.push('kilo');
|
|
75
|
-
if (fs.existsSync(path.join(process.cwd(), '.github'))) tools.push('copilot');
|
|
75
|
+
if (fs.existsSync(path.join(process.cwd(), '.github'))) tools.push('copilot');
|
|
76
76
|
if (fs.existsSync(path.join(process.cwd(), '.roo'))) tools.push('roo');
|
|
77
77
|
if (fs.existsSync(path.join(process.cwd(), '.opencode'))) tools.push('opencode');
|
|
78
78
|
if (fs.existsSync(path.join(process.cwd(), 'prompts'))) tools.push('web');
|
|
@@ -193,17 +193,17 @@ async function processAgentsInstallation(tools, options) {
|
|
|
193
193
|
})
|
|
194
194
|
);
|
|
195
195
|
},
|
|
196
|
-
roo: async (validAgents, options) => {
|
|
197
|
-
const targetDir = path.join(process.cwd(), '.roo', 'commands');
|
|
198
|
-
await fsp.mkdir(targetDir, { recursive: true });
|
|
199
|
-
|
|
200
|
-
await Promise.all(
|
|
201
|
-
validAgents.map((agent) => {
|
|
202
|
-
const md = toOpenCodeAgent(agent, options);
|
|
203
|
-
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
204
|
-
})
|
|
205
|
-
);
|
|
206
|
-
},
|
|
196
|
+
roo: async (validAgents, options) => {
|
|
197
|
+
const targetDir = path.join(process.cwd(), '.roo', 'commands');
|
|
198
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
await Promise.all(
|
|
201
|
+
validAgents.map((agent) => {
|
|
202
|
+
const md = toOpenCodeAgent(agent, options);
|
|
203
|
+
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
207
|
cline: async (validAgents, options) => {
|
|
208
208
|
const targetDir = path.join(process.cwd(), '.cline');
|
|
209
209
|
await fsp.mkdir(targetDir, { recursive: true });
|
|
@@ -230,31 +230,31 @@ async function processAgentsInstallation(tools, options) {
|
|
|
230
230
|
})
|
|
231
231
|
);
|
|
232
232
|
},
|
|
233
|
-
claude: async (validAgents, options) => {
|
|
234
|
-
const targetDir = path.join(process.cwd(), '.claude', 'commands', 'agents');
|
|
235
|
-
await fsp.mkdir(targetDir, { recursive: true });
|
|
236
|
-
|
|
237
|
-
await Promise.all(
|
|
238
|
-
validAgents.map((agent) => {
|
|
239
|
-
const md = toClaudeCommand(agent, options);
|
|
240
|
-
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
241
|
-
})
|
|
242
|
-
);
|
|
243
|
-
},
|
|
244
|
-
cursor: async (validAgents, options) => {
|
|
245
|
-
const commandsDir = path.join(process.cwd(), '.cursor', 'commands');
|
|
246
|
-
await fsp.mkdir(commandsDir, { recursive: true });
|
|
247
|
-
|
|
248
|
-
await Promise.all(
|
|
249
|
-
validAgents.map((agent) => {
|
|
250
|
-
const mdc = toCursorMDC(agent, options);
|
|
251
|
-
return fsp.writeFile(path.join(commandsDir, `${agent.slug}.mdc`), mdc);
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
},
|
|
255
|
-
kilo: async (validAgents, options) => {
|
|
256
|
-
const targetDir = path.join(process.cwd(), '.kilocode', 'workflows');
|
|
257
|
-
await fsp.mkdir(targetDir, { recursive: true });
|
|
233
|
+
claude: async (validAgents, options) => {
|
|
234
|
+
const targetDir = path.join(process.cwd(), '.claude', 'commands', 'agents');
|
|
235
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
236
|
+
|
|
237
|
+
await Promise.all(
|
|
238
|
+
validAgents.map((agent) => {
|
|
239
|
+
const md = toClaudeCommand(agent, options);
|
|
240
|
+
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
cursor: async (validAgents, options) => {
|
|
245
|
+
const commandsDir = path.join(process.cwd(), '.cursor', 'commands');
|
|
246
|
+
await fsp.mkdir(commandsDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
await Promise.all(
|
|
249
|
+
validAgents.map((agent) => {
|
|
250
|
+
const mdc = toCursorMDC(agent, options);
|
|
251
|
+
return fsp.writeFile(path.join(commandsDir, `${agent.slug}.mdc`), mdc);
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
kilo: async (validAgents, options) => {
|
|
256
|
+
const targetDir = path.join(process.cwd(), '.kilocode', 'workflows');
|
|
257
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
258
258
|
|
|
259
259
|
await Promise.all(
|
|
260
260
|
validAgents.map((agent) => {
|
|
@@ -263,22 +263,22 @@ async function processAgentsInstallation(tools, options) {
|
|
|
263
263
|
})
|
|
264
264
|
);
|
|
265
265
|
},
|
|
266
|
-
copilot: async (validAgents, options) => {
|
|
267
|
-
const githubDir = path.join(process.cwd(), '.github');
|
|
268
|
-
const promptsDir = path.join(githubDir, 'prompts');
|
|
269
|
-
await fsp.mkdir(promptsDir, { recursive: true });
|
|
270
|
-
|
|
271
|
-
await Promise.all(
|
|
272
|
-
validAgents.map((agent) => {
|
|
273
|
-
const md = toCopilotInstructions(agent, options);
|
|
274
|
-
return fsp.writeFile(path.join(promptsDir, `${agent.slug}.md`), md);
|
|
275
|
-
})
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const mainAgent = validAgents.find((a) => a.slug.includes('coder')) || validAgents[0];
|
|
279
|
-
const mainInstructions = toCopilotInstructions(mainAgent, options);
|
|
280
|
-
await fsp.writeFile(path.join(githubDir, 'prompts.md'), mainInstructions);
|
|
281
|
-
},
|
|
266
|
+
copilot: async (validAgents, options) => {
|
|
267
|
+
const githubDir = path.join(process.cwd(), '.github');
|
|
268
|
+
const promptsDir = path.join(githubDir, 'prompts');
|
|
269
|
+
await fsp.mkdir(promptsDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
await Promise.all(
|
|
272
|
+
validAgents.map((agent) => {
|
|
273
|
+
const md = toCopilotInstructions(agent, options);
|
|
274
|
+
return fsp.writeFile(path.join(promptsDir, `${agent.slug}.md`), md);
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const mainAgent = validAgents.find((a) => a.slug.includes('coder')) || validAgents[0];
|
|
279
|
+
const mainInstructions = toCopilotInstructions(mainAgent, options);
|
|
280
|
+
await fsp.writeFile(path.join(githubDir, 'prompts.md'), mainInstructions);
|
|
281
|
+
},
|
|
282
282
|
trae: async (validAgents, options) => {
|
|
283
283
|
const traeDir = path.join(process.cwd(), '.trae');
|
|
284
284
|
await fsp.mkdir(traeDir, { recursive: true });
|
|
@@ -298,51 +298,51 @@ async function processAgentsInstallation(tools, options) {
|
|
|
298
298
|
})
|
|
299
299
|
);
|
|
300
300
|
},
|
|
301
|
-
opencode: async (validAgents, options) => {
|
|
302
|
-
const targetDir = path.join(process.cwd(), '.opencode', 'commands');
|
|
303
|
-
await fsp.mkdir(targetDir, { recursive: true });
|
|
304
|
-
|
|
305
|
-
await Promise.all(
|
|
306
|
-
validAgents.map((agent) => {
|
|
307
|
-
const md = toOpenCodeAgent(agent, options);
|
|
308
|
-
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
309
|
-
})
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// Generate AGENTS.md with interaction rules and agent location
|
|
313
|
-
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
314
|
-
let agentsMdContent = `# Interaction Rules
|
|
315
|
-
|
|
316
|
-
- Always respond to the user in the language they initially interact in; if they interact in English, respond in English, if they interact in Portuguese, respond in Portuguese.
|
|
317
|
-
- If possible, display reasoning in the user's language as well.
|
|
318
|
-
- Be didactic when explaining things, focus on providing complete responses and not just summaries.
|
|
319
|
-
- Whenever possible, provide examples to illustrate concepts.
|
|
320
|
-
|
|
321
|
-
# Allowed Commands
|
|
322
|
-
|
|
323
|
-
- Never execute rm or rm -rf commands without confirming with the user.
|
|
324
|
-
- Whenever possible, use more specific commands instead of generic ones.
|
|
325
|
-
- Be cautious when using commands that may affect critical systems, such as shutdown or reboot.
|
|
326
|
-
- For commands that may affect files or directories, always confirm with the user before executing.
|
|
327
|
-
- Never execute commands that require administrative privileges (sudo, admin) without explicit permission from the user.
|
|
328
|
-
- Avoid running background processes or daemons unless explicitly requested.
|
|
329
|
-
- Be cautious when using commands that alter network settings, firewall configurations, or external connections.
|
|
330
|
-
- Always quote file paths that contain spaces to avoid interpretation errors.
|
|
331
|
-
- For package installation commands (npm install, pip install, etc.), confirm that the user has control over dependencies and versions.
|
|
332
|
-
- Avoid irreversible git operations (such as force push or reset --hard) without confirmation.
|
|
333
|
-
|
|
334
|
-
# Agent Location
|
|
335
|
-
|
|
336
|
-
Custom agents are located in .opencode/commands/`;
|
|
337
|
-
|
|
338
|
-
let userRules = '';
|
|
339
|
-
if (options.globalRules && options.globalRules.trim()) {
|
|
340
|
-
userRules = '\n\n# User Specified Rules\n\n' + options.globalRules.split('\n').filter(line => line.trim()).map(line => '- ' + line.trim()).join('\n');
|
|
341
|
-
}
|
|
342
|
-
agentsMdContent += userRules;
|
|
343
|
-
|
|
344
|
-
await fsp.writeFile(agentsMdPath, agentsMdContent);
|
|
345
|
-
}
|
|
301
|
+
opencode: async (validAgents, options) => {
|
|
302
|
+
const targetDir = path.join(process.cwd(), '.opencode', 'commands');
|
|
303
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
304
|
+
|
|
305
|
+
await Promise.all(
|
|
306
|
+
validAgents.map((agent) => {
|
|
307
|
+
const md = toOpenCodeAgent(agent, options);
|
|
308
|
+
return fsp.writeFile(path.join(targetDir, `${agent.slug}.md`), md);
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Generate AGENTS.md with interaction rules and agent location
|
|
313
|
+
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
314
|
+
let agentsMdContent = `# Interaction Rules
|
|
315
|
+
|
|
316
|
+
- Always respond to the user in the language they initially interact in; if they interact in English, respond in English, if they interact in Portuguese, respond in Portuguese.
|
|
317
|
+
- If possible, display reasoning in the user's language as well.
|
|
318
|
+
- Be didactic when explaining things, focus on providing complete responses and not just summaries.
|
|
319
|
+
- Whenever possible, provide examples to illustrate concepts.
|
|
320
|
+
|
|
321
|
+
# Allowed Commands
|
|
322
|
+
|
|
323
|
+
- Never execute rm or rm -rf commands without confirming with the user.
|
|
324
|
+
- Whenever possible, use more specific commands instead of generic ones.
|
|
325
|
+
- Be cautious when using commands that may affect critical systems, such as shutdown or reboot.
|
|
326
|
+
- For commands that may affect files or directories, always confirm with the user before executing.
|
|
327
|
+
- Never execute commands that require administrative privileges (sudo, admin) without explicit permission from the user.
|
|
328
|
+
- Avoid running background processes or daemons unless explicitly requested.
|
|
329
|
+
- Be cautious when using commands that alter network settings, firewall configurations, or external connections.
|
|
330
|
+
- Always quote file paths that contain spaces to avoid interpretation errors.
|
|
331
|
+
- For package installation commands (npm install, pip install, etc.), confirm that the user has control over dependencies and versions.
|
|
332
|
+
- Avoid irreversible git operations (such as force push or reset --hard) without confirmation.
|
|
333
|
+
|
|
334
|
+
# Agent Location
|
|
335
|
+
|
|
336
|
+
Custom agents are located in .opencode/commands/`;
|
|
337
|
+
|
|
338
|
+
let userRules = '';
|
|
339
|
+
if (options.globalRules && options.globalRules.trim()) {
|
|
340
|
+
userRules = '\n\n# User Specified Rules\n\n' + options.globalRules.split('\n').filter(line => line.trim()).map(line => '- ' + line.trim()).join('\n');
|
|
341
|
+
}
|
|
342
|
+
agentsMdContent += userRules;
|
|
343
|
+
|
|
344
|
+
await fsp.writeFile(agentsMdPath, agentsMdContent);
|
|
345
|
+
}
|
|
346
346
|
};
|
|
347
347
|
|
|
348
348
|
for (const tool of tools) {
|
package/src/lib/dashboard.js
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
const fs = require('fs/promises');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const pc = require('picocolors');
|
|
4
|
-
const { t } = require('./i18n');
|
|
5
|
-
|
|
6
|
-
const DOCS_DIR = '.sdd-toolkit';
|
|
7
|
-
const PROJECT_FILE = path.join(DOCS_DIR, 'project.md');
|
|
8
|
-
const TASKS_FILE = path.join(DOCS_DIR, 'task.md');
|
|
9
|
-
const LOGS_DIR = path.join(DOCS_DIR, 'logs', 'executions');
|
|
10
|
-
const PACKAGE_FILE = 'package.json';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extracts metadata from package.json (priority) or project.md
|
|
14
|
-
*/
|
|
15
|
-
async function getProjectMetadata() {
|
|
16
|
-
let meta = { title: 'Project', version: '0.0.0', status: 'Active' };
|
|
17
|
-
|
|
18
|
-
// Try reading package.json first (Single Source of Truth for Version)
|
|
19
|
-
try {
|
|
20
|
-
const pkgContent = await fs.readFile(PACKAGE_FILE, 'utf8');
|
|
21
|
-
const pkg = JSON.parse(pkgContent);
|
|
22
|
-
if (pkg.name) meta.title = pkg.name;
|
|
23
|
-
if (pkg.version) meta.version = pkg.version;
|
|
24
|
-
} catch (e) {
|
|
25
|
-
// Ignore if no package.json
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Try reading project.md for status or title fallback
|
|
29
|
-
try {
|
|
30
|
-
const content = await fs.readFile(PROJECT_FILE, 'utf8');
|
|
31
|
-
|
|
32
|
-
// Only override title if package.json didn't provide one
|
|
33
|
-
if (meta.title === 'Project') {
|
|
34
|
-
const titleMatch = content.match(/title:\s*(.+)/);
|
|
35
|
-
if (titleMatch) meta.title = titleMatch[1].trim();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const statusMatch = content.match(/status:\s*(.+)/);
|
|
39
|
-
if (statusMatch) meta.status = statusMatch[1].trim();
|
|
40
|
-
|
|
41
|
-
} catch (e) {
|
|
42
|
-
// Ignore errors
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return meta;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Parses task.md to calculate progress metrics
|
|
50
|
-
*/
|
|
51
|
-
async function getTaskProgress() {
|
|
52
|
-
try {
|
|
53
|
-
const content = await fs.readFile(TASKS_FILE, 'utf8');
|
|
54
|
-
|
|
55
|
-
// Extract Milestone Name
|
|
56
|
-
const milestoneMatch = content.match(/# Execution Backlog:\s*(.+)/);
|
|
57
|
-
const milestone = milestoneMatch ? milestoneMatch[1].trim() : 'General Tasks';
|
|
58
|
-
|
|
59
|
-
// Count Checkboxes
|
|
60
|
-
const total = (content.match(/- \[ \]/g) || []).length + (content.match(/- \[x\]/g) || []).length;
|
|
61
|
-
const done = (content.match(/- \[x\]/g) || []).length;
|
|
62
|
-
const pending = total - done;
|
|
63
|
-
const percent = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
64
|
-
|
|
65
|
-
return { milestone, total, done, pending, percent };
|
|
66
|
-
} catch (e) {
|
|
67
|
-
return { milestone: 'No active plan', total: 0, done: 0, pending: 0, percent: 0 };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Retrieves the N most recent execution logs
|
|
73
|
-
*/
|
|
74
|
-
async function getRecentLogs(limit = 5) {
|
|
75
|
-
try {
|
|
76
|
-
// Ensure dir exists to avoid crash
|
|
77
|
-
await fs.access(LOGS_DIR);
|
|
78
|
-
|
|
79
|
-
const files = await fs.readdir(LOGS_DIR);
|
|
80
|
-
|
|
81
|
-
// Read stats for sorting
|
|
82
|
-
const fileStats = await Promise.all(
|
|
83
|
-
files
|
|
84
|
-
.filter(f => f.endsWith('.md'))
|
|
85
|
-
.map(async (file) => {
|
|
86
|
-
const filePath = path.join(LOGS_DIR, file);
|
|
87
|
-
const stats = await fs.stat(filePath);
|
|
88
|
-
return { file, mtime: stats.mtime, filePath };
|
|
89
|
-
})
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
// Sort by time desc
|
|
93
|
-
const sorted = fileStats.sort((a, b) => b.mtime - a.mtime).slice(0, limit);
|
|
94
|
-
|
|
95
|
-
// Read content to get status/task ID if possible (simplified for now)
|
|
96
|
-
const logs = sorted.map(item => {
|
|
97
|
-
const name = item.file.replace('.md', '');
|
|
98
|
-
return {
|
|
99
|
-
name,
|
|
100
|
-
date: item.mtime.toLocaleDateString(),
|
|
101
|
-
status: 'Completed'
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return logs;
|
|
106
|
-
} catch (e) {
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* UI Component: Progress Bar
|
|
113
|
-
* [██████░░░░] 60%
|
|
114
|
-
*/
|
|
115
|
-
function drawProgressBar(percent, width = 20) {
|
|
116
|
-
const filled = Math.round((percent / 100) * width);
|
|
117
|
-
const empty = width - filled;
|
|
118
|
-
|
|
119
|
-
const bar = pc.green('█'.repeat(filled)) + pc.gray('░'.repeat(empty));
|
|
120
|
-
return `[${bar}] ${pc.bold(percent + '%')}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* UI Component: Box
|
|
125
|
-
*/
|
|
126
|
-
function drawBox(lines) {
|
|
127
|
-
const maxWidth = Math.max(60, ...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
|
|
128
|
-
const borderTop = '╭' + '─'.repeat(maxWidth + 2) + '╮';
|
|
129
|
-
const borderBottom = '╰' + '─'.repeat(maxWidth + 2) + '╯';
|
|
130
|
-
|
|
131
|
-
console.log(pc.gray(borderTop));
|
|
132
|
-
lines.forEach(line => {
|
|
133
|
-
const len = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
134
|
-
const padding = ' '.repeat(maxWidth - len);
|
|
135
|
-
console.log(pc.gray('│ ') + line + padding + pc.gray(' │'));
|
|
136
|
-
});
|
|
137
|
-
console.log(pc.gray(borderBottom));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Main Render Function
|
|
142
|
-
*/
|
|
143
|
-
async function renderDashboard() {
|
|
144
|
-
console.clear();
|
|
145
|
-
|
|
146
|
-
// Fetch Data Parallel
|
|
147
|
-
const [meta, progress, logs] = await Promise.all([
|
|
148
|
-
getProjectMetadata(),
|
|
149
|
-
getTaskProgress(),
|
|
150
|
-
getRecentLogs(5)
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
// Header (using i18n keys)
|
|
154
|
-
drawBox([
|
|
155
|
-
`${pc.magenta(t('DASHBOARD.TITLE'))} ${pc.bold(meta.title)} (v${meta.version})`,
|
|
156
|
-
`${pc.cyan(t('DASHBOARD.PHASE'))} ${progress.milestone}`,
|
|
157
|
-
`${pc.yellow(t('DASHBOARD.STATUS'))} ${meta.status}`
|
|
158
|
-
]);
|
|
159
|
-
|
|
160
|
-
console.log(''); // Spacing
|
|
161
|
-
|
|
162
|
-
// Progress Section
|
|
163
|
-
console.log(pc.bold(t('DASHBOARD.OVERALL')));
|
|
164
|
-
console.log(drawProgressBar(progress.percent, 40));
|
|
165
|
-
console.log(`${pc.green('✅ ' + progress.done + ' ' + t('DASHBOARD.COMPLETED'))} | ${pc.red('⭕ ' + progress.pending + ' ' + t('DASHBOARD.PENDING'))}`);
|
|
166
|
-
|
|
167
|
-
console.log(''); // Spacing
|
|
168
|
-
|
|
169
|
-
// Recent Logs Section
|
|
170
|
-
if (logs.length > 0) {
|
|
171
|
-
console.log(pc.bold(t('DASHBOARD.RECENT')));
|
|
172
|
-
logs.forEach(log => {
|
|
173
|
-
console.log(`${pc.gray('•')} ${pc.dim(`[${log.date}]`)} ${log.name}`);
|
|
174
|
-
});
|
|
175
|
-
} else {
|
|
176
|
-
console.log(pc.gray(t('DASHBOARD.NO_ACTIVITY')));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
console.log(''); // Spacing
|
|
180
|
-
console.log(pc.bgMagenta(pc.black(t('DASHBOARD.ACTION'))) + ' ' + t('DASHBOARD.HINT') + ' ' + pc.cyan('/dev:coder <Task_ID>') + ' ' + t('DASHBOARD.HINT_SUFFIX'));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
module.exports = {
|
|
184
|
-
getProjectMetadata,
|
|
185
|
-
getTaskProgress,
|
|
186
|
-
getRecentLogs,
|
|
187
|
-
renderDashboard
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
const { t } = require('./i18n');
|
|
5
|
+
|
|
6
|
+
const DOCS_DIR = '.sdd-toolkit';
|
|
7
|
+
const PROJECT_FILE = path.join(DOCS_DIR, 'project.md');
|
|
8
|
+
const TASKS_FILE = path.join(DOCS_DIR, 'task.md');
|
|
9
|
+
const LOGS_DIR = path.join(DOCS_DIR, 'logs', 'executions');
|
|
10
|
+
const PACKAGE_FILE = 'package.json';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts metadata from package.json (priority) or project.md
|
|
14
|
+
*/
|
|
15
|
+
async function getProjectMetadata() {
|
|
16
|
+
let meta = { title: 'Project', version: '0.0.0', status: 'Active' };
|
|
17
|
+
|
|
18
|
+
// Try reading package.json first (Single Source of Truth for Version)
|
|
19
|
+
try {
|
|
20
|
+
const pkgContent = await fs.readFile(PACKAGE_FILE, 'utf8');
|
|
21
|
+
const pkg = JSON.parse(pkgContent);
|
|
22
|
+
if (pkg.name) meta.title = pkg.name;
|
|
23
|
+
if (pkg.version) meta.version = pkg.version;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// Ignore if no package.json
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Try reading project.md for status or title fallback
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(PROJECT_FILE, 'utf8');
|
|
31
|
+
|
|
32
|
+
// Only override title if package.json didn't provide one
|
|
33
|
+
if (meta.title === 'Project') {
|
|
34
|
+
const titleMatch = content.match(/title:\s*(.+)/);
|
|
35
|
+
if (titleMatch) meta.title = titleMatch[1].trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const statusMatch = content.match(/status:\s*(.+)/);
|
|
39
|
+
if (statusMatch) meta.status = statusMatch[1].trim();
|
|
40
|
+
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Ignore errors
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return meta;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses task.md to calculate progress metrics
|
|
50
|
+
*/
|
|
51
|
+
async function getTaskProgress() {
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(TASKS_FILE, 'utf8');
|
|
54
|
+
|
|
55
|
+
// Extract Milestone Name
|
|
56
|
+
const milestoneMatch = content.match(/# Execution Backlog:\s*(.+)/);
|
|
57
|
+
const milestone = milestoneMatch ? milestoneMatch[1].trim() : 'General Tasks';
|
|
58
|
+
|
|
59
|
+
// Count Checkboxes
|
|
60
|
+
const total = (content.match(/- \[ \]/g) || []).length + (content.match(/- \[x\]/g) || []).length;
|
|
61
|
+
const done = (content.match(/- \[x\]/g) || []).length;
|
|
62
|
+
const pending = total - done;
|
|
63
|
+
const percent = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
64
|
+
|
|
65
|
+
return { milestone, total, done, pending, percent };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { milestone: 'No active plan', total: 0, done: 0, pending: 0, percent: 0 };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Retrieves the N most recent execution logs
|
|
73
|
+
*/
|
|
74
|
+
async function getRecentLogs(limit = 5) {
|
|
75
|
+
try {
|
|
76
|
+
// Ensure dir exists to avoid crash
|
|
77
|
+
await fs.access(LOGS_DIR);
|
|
78
|
+
|
|
79
|
+
const files = await fs.readdir(LOGS_DIR);
|
|
80
|
+
|
|
81
|
+
// Read stats for sorting
|
|
82
|
+
const fileStats = await Promise.all(
|
|
83
|
+
files
|
|
84
|
+
.filter(f => f.endsWith('.md'))
|
|
85
|
+
.map(async (file) => {
|
|
86
|
+
const filePath = path.join(LOGS_DIR, file);
|
|
87
|
+
const stats = await fs.stat(filePath);
|
|
88
|
+
return { file, mtime: stats.mtime, filePath };
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Sort by time desc
|
|
93
|
+
const sorted = fileStats.sort((a, b) => b.mtime - a.mtime).slice(0, limit);
|
|
94
|
+
|
|
95
|
+
// Read content to get status/task ID if possible (simplified for now)
|
|
96
|
+
const logs = sorted.map(item => {
|
|
97
|
+
const name = item.file.replace('.md', '');
|
|
98
|
+
return {
|
|
99
|
+
name,
|
|
100
|
+
date: item.mtime.toLocaleDateString(),
|
|
101
|
+
status: 'Completed'
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return logs;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* UI Component: Progress Bar
|
|
113
|
+
* [██████░░░░] 60%
|
|
114
|
+
*/
|
|
115
|
+
function drawProgressBar(percent, width = 20) {
|
|
116
|
+
const filled = Math.round((percent / 100) * width);
|
|
117
|
+
const empty = width - filled;
|
|
118
|
+
|
|
119
|
+
const bar = pc.green('█'.repeat(filled)) + pc.gray('░'.repeat(empty));
|
|
120
|
+
return `[${bar}] ${pc.bold(percent + '%')}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* UI Component: Box
|
|
125
|
+
*/
|
|
126
|
+
function drawBox(lines) {
|
|
127
|
+
const maxWidth = Math.max(60, ...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
|
|
128
|
+
const borderTop = '╭' + '─'.repeat(maxWidth + 2) + '╮';
|
|
129
|
+
const borderBottom = '╰' + '─'.repeat(maxWidth + 2) + '╯';
|
|
130
|
+
|
|
131
|
+
console.log(pc.gray(borderTop));
|
|
132
|
+
lines.forEach(line => {
|
|
133
|
+
const len = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
134
|
+
const padding = ' '.repeat(maxWidth - len);
|
|
135
|
+
console.log(pc.gray('│ ') + line + padding + pc.gray(' │'));
|
|
136
|
+
});
|
|
137
|
+
console.log(pc.gray(borderBottom));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main Render Function
|
|
142
|
+
*/
|
|
143
|
+
async function renderDashboard() {
|
|
144
|
+
console.clear();
|
|
145
|
+
|
|
146
|
+
// Fetch Data Parallel
|
|
147
|
+
const [meta, progress, logs] = await Promise.all([
|
|
148
|
+
getProjectMetadata(),
|
|
149
|
+
getTaskProgress(),
|
|
150
|
+
getRecentLogs(5)
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// Header (using i18n keys)
|
|
154
|
+
drawBox([
|
|
155
|
+
`${pc.magenta(t('DASHBOARD.TITLE'))} ${pc.bold(meta.title)} (v${meta.version})`,
|
|
156
|
+
`${pc.cyan(t('DASHBOARD.PHASE'))} ${progress.milestone}`,
|
|
157
|
+
`${pc.yellow(t('DASHBOARD.STATUS'))} ${meta.status}`
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
console.log(''); // Spacing
|
|
161
|
+
|
|
162
|
+
// Progress Section
|
|
163
|
+
console.log(pc.bold(t('DASHBOARD.OVERALL')));
|
|
164
|
+
console.log(drawProgressBar(progress.percent, 40));
|
|
165
|
+
console.log(`${pc.green('✅ ' + progress.done + ' ' + t('DASHBOARD.COMPLETED'))} | ${pc.red('⭕ ' + progress.pending + ' ' + t('DASHBOARD.PENDING'))}`);
|
|
166
|
+
|
|
167
|
+
console.log(''); // Spacing
|
|
168
|
+
|
|
169
|
+
// Recent Logs Section
|
|
170
|
+
if (logs.length > 0) {
|
|
171
|
+
console.log(pc.bold(t('DASHBOARD.RECENT')));
|
|
172
|
+
logs.forEach(log => {
|
|
173
|
+
console.log(`${pc.gray('•')} ${pc.dim(`[${log.date}]`)} ${log.name}`);
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
console.log(pc.gray(t('DASHBOARD.NO_ACTIVITY')));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(''); // Spacing
|
|
180
|
+
console.log(pc.bgMagenta(pc.black(t('DASHBOARD.ACTION'))) + ' ' + t('DASHBOARD.HINT') + ' ' + pc.cyan('/dev:coder <Task_ID>') + ' ' + t('DASHBOARD.HINT_SUFFIX'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
getProjectMetadata,
|
|
185
|
+
getTaskProgress,
|
|
186
|
+
getRecentLogs,
|
|
187
|
+
renderDashboard
|
|
188
188
|
};
|