rbin-task-flow 1.1.0 → 1.2.0
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/.cursor/rules/code_comments.mdc +62 -62
- package/.cursor/rules/commit_practices.mdc +75 -75
- package/.cursor/rules/git_control.mdc +63 -63
- package/.cursor/rules/task_estimate.mdc +88 -0
- package/.cursor/rules/task_execution.mdc +6 -4
- package/.cursor/rules/task_generation.mdc +16 -16
- package/.cursor/rules/task_report.mdc +108 -0
- package/.cursor/rules/task_work.mdc +13 -13
- package/.task-flow/README.md +67 -39
- package/.task-flow/screens/example.png.txt +10 -11
- package/.task-flow/tasks.input.txt +6 -8
- package/.task-flow/tasks.status.md +7 -7
- package/CLAUDE.md +2 -0
- package/GEMINI.md +2 -0
- package/README.md +8 -0
- package/bin/cli.js +24 -0
- package/lib/estimate.js +92 -0
- package/lib/install.js +21 -30
- package/lib/report.js +278 -0
- package/lib/version.js +8 -5
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ const { program } = require('commander');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { installInProject } = require('../lib/install');
|
|
6
6
|
const { checkVersionUpdates } = require('../lib/version');
|
|
7
|
+
const { estimateTask } = require('../lib/estimate');
|
|
8
|
+
const { generateReport } = require('../lib/report');
|
|
7
9
|
const chalk = require('chalk');
|
|
8
10
|
|
|
9
11
|
program
|
|
@@ -36,6 +38,26 @@ program
|
|
|
36
38
|
await checkVersionUpdates();
|
|
37
39
|
});
|
|
38
40
|
|
|
41
|
+
program
|
|
42
|
+
.command('estimate')
|
|
43
|
+
.description('Estimate time for a task based on subtasks and experience level')
|
|
44
|
+
.argument('<taskId>', 'Task ID to estimate')
|
|
45
|
+
.option('-p, --path <path>', 'Target directory (default: current directory)')
|
|
46
|
+
.action(async (taskId, options) => {
|
|
47
|
+
const targetPath = options.path || process.cwd();
|
|
48
|
+
await estimateTask(taskId, targetPath);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('report')
|
|
53
|
+
.description('Generate implementation report for a completed task')
|
|
54
|
+
.argument('<taskId>', 'Task ID to generate report for')
|
|
55
|
+
.option('-p, --path <path>', 'Target directory (default: current directory)')
|
|
56
|
+
.action(async (taskId, options) => {
|
|
57
|
+
const targetPath = options.path || process.cwd();
|
|
58
|
+
await generateReport(taskId, targetPath);
|
|
59
|
+
});
|
|
60
|
+
|
|
39
61
|
program
|
|
40
62
|
.command('info')
|
|
41
63
|
.description('Show information about RBIN Task Flow')
|
|
@@ -50,6 +72,8 @@ program
|
|
|
50
72
|
console.log(chalk.cyan(' rbin-task-flow init') + ' - Initialize in current directory');
|
|
51
73
|
console.log(chalk.cyan(' rbin-task-flow update') + ' - Update configurations');
|
|
52
74
|
console.log(chalk.cyan(' rbin-task-flow version-check') + ' - Check for model updates');
|
|
75
|
+
console.log(chalk.cyan(' rbin-task-flow estimate <id>') + ' - Estimate time for a task');
|
|
76
|
+
console.log(chalk.cyan(' rbin-task-flow report <id>') + ' - Generate implementation report');
|
|
53
77
|
console.log(chalk.cyan(' rbin-task-flow info') + ' - Show this information\n');
|
|
54
78
|
});
|
|
55
79
|
|
package/lib/estimate.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
async function estimateTask(taskId, targetPath = process.cwd()) {
|
|
6
|
+
const tasksPath = path.join(targetPath, '.task-flow/.internal/tasks.json');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(tasksPath)) {
|
|
9
|
+
console.log(chalk.red('❌ Tasks file not found. Run "task-flow: sync" first.'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const tasksData = await fs.readJSON(tasksPath);
|
|
15
|
+
const task = tasksData.tasks.find(t => t.id === parseInt(taskId));
|
|
16
|
+
|
|
17
|
+
if (!task) {
|
|
18
|
+
console.log(chalk.red(`❌ Task ${taskId} not found.`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const subtaskCount = task.subtasks ? task.subtasks.length : 0;
|
|
23
|
+
|
|
24
|
+
if (subtaskCount === 0) {
|
|
25
|
+
console.log(chalk.yellow(`⚠️ Task ${taskId} has no subtasks. Cannot estimate.`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const estimates = calculateEstimates(subtaskCount);
|
|
30
|
+
|
|
31
|
+
console.log('\n' + chalk.cyan('═'.repeat(70)));
|
|
32
|
+
console.log(chalk.magenta('📊 Task Estimation Report'));
|
|
33
|
+
console.log(chalk.cyan('═'.repeat(70)) + '\n');
|
|
34
|
+
|
|
35
|
+
console.log(chalk.blue.bold('Task:'), chalk.yellow(`#${taskId} - ${task.title}\n`));
|
|
36
|
+
console.log(chalk.blue(`Complexity: ${chalk.yellow(subtaskCount)} subtasks\n`));
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan('─'.repeat(70)));
|
|
39
|
+
console.log(chalk.magenta.bold('Time Estimates by Experience Level:\n'));
|
|
40
|
+
|
|
41
|
+
const juniorDays = Math.ceil(estimates.junior.upper / 8);
|
|
42
|
+
const midDays = Math.ceil(estimates.mid.upper / 8);
|
|
43
|
+
const seniorDays = Math.ceil(estimates.senior.upper / 8);
|
|
44
|
+
|
|
45
|
+
console.log(chalk.gray('👶 Junior Developer (0-2 years):'));
|
|
46
|
+
console.log(chalk.white(' Hours:'), chalk.yellow(`${estimates.junior.lower}-${estimates.junior.upper} hours`));
|
|
47
|
+
console.log(chalk.white(' Days: '), chalk.yellow(`~${juniorDays} business day(s)`));
|
|
48
|
+
console.log('');
|
|
49
|
+
|
|
50
|
+
console.log(chalk.gray('👨💼 Mid-level Developer (3-5 years):'));
|
|
51
|
+
console.log(chalk.white(' Hours:'), chalk.yellow(`${estimates.mid.lower}-${estimates.mid.upper} hours`));
|
|
52
|
+
console.log(chalk.white(' Days: '), chalk.yellow(`~${midDays} business day(s)`));
|
|
53
|
+
console.log('');
|
|
54
|
+
|
|
55
|
+
console.log(chalk.gray('👴 Senior Developer (6+ years):'));
|
|
56
|
+
console.log(chalk.white(' Hours:'), chalk.yellow(`${estimates.senior.lower}-${estimates.senior.upper} hours`));
|
|
57
|
+
console.log(chalk.white(' Days: '), chalk.yellow(`~${seniorDays} business day(s)`));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
console.log(chalk.cyan('─'.repeat(70)));
|
|
61
|
+
console.log(chalk.magenta.bold('Recommendation for Management:\n'));
|
|
62
|
+
console.log(chalk.white(' Recommended estimate:'), chalk.yellow(`${estimates.mid.lower}-${estimates.mid.upper} hours`), chalk.gray('(mid-level baseline)'));
|
|
63
|
+
console.log(chalk.white(' Buffer recommended:'), chalk.yellow(`+20%`), chalk.gray('for unexpected issues'));
|
|
64
|
+
console.log(chalk.white(' Total estimate:'), chalk.yellow(`${Math.round(estimates.mid.upper * 1.2)} hours`), chalk.gray(`(~${Math.ceil(estimates.mid.upper * 1.2 / 8)} business days)`));
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(chalk.red('Error reading tasks:'), error.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calculateEstimates(subtaskCount) {
|
|
73
|
+
const baseLower = subtaskCount * 2;
|
|
74
|
+
const baseUpper = subtaskCount * 3;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
junior: {
|
|
78
|
+
lower: Math.round(baseLower * 1.5),
|
|
79
|
+
upper: Math.round(baseUpper * 1.5)
|
|
80
|
+
},
|
|
81
|
+
mid: {
|
|
82
|
+
lower: baseLower,
|
|
83
|
+
upper: baseUpper
|
|
84
|
+
},
|
|
85
|
+
senior: {
|
|
86
|
+
lower: Math.round(baseLower * 0.7),
|
|
87
|
+
upper: Math.round(baseUpper * 0.7)
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { estimateTask };
|
package/lib/install.js
CHANGED
|
@@ -4,7 +4,6 @@ const chalk = require('chalk');
|
|
|
4
4
|
const ora = require('ora');
|
|
5
5
|
const { showHeader, showSuccess, showError, showWarning, showInfo, showNextSteps } = require('./utils');
|
|
6
6
|
|
|
7
|
-
// Diretório onde o pacote npm está instalado globalmente
|
|
8
7
|
const TEMPLATE_DIR = path.join(__dirname, '..');
|
|
9
8
|
|
|
10
9
|
async function installInProject(targetPath, options = {}) {
|
|
@@ -23,13 +22,11 @@ async function installInProject(targetPath, options = {}) {
|
|
|
23
22
|
const spinner = ora('Processing...').start();
|
|
24
23
|
|
|
25
24
|
try {
|
|
26
|
-
// Verificar se diretório existe
|
|
27
25
|
if (!fs.existsSync(targetPath)) {
|
|
28
26
|
spinner.fail(chalk.red(`Directory not found: ${targetPath}`));
|
|
29
27
|
process.exit(1);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
// Verificar permissões de escrita
|
|
33
30
|
try {
|
|
34
31
|
fs.accessSync(targetPath, fs.constants.W_OK);
|
|
35
32
|
} catch (error) {
|
|
@@ -39,7 +36,6 @@ async function installInProject(targetPath, options = {}) {
|
|
|
39
36
|
|
|
40
37
|
spinner.text = 'Creating directories...';
|
|
41
38
|
|
|
42
|
-
// Criar diretórios necessários
|
|
43
39
|
const dirs = [
|
|
44
40
|
'.cursor/rules',
|
|
45
41
|
'.claude',
|
|
@@ -53,19 +49,16 @@ async function installInProject(targetPath, options = {}) {
|
|
|
53
49
|
|
|
54
50
|
spinner.text = 'Copying configuration files...';
|
|
55
51
|
|
|
56
|
-
// Copiar arquivos de configuração
|
|
57
52
|
await copyConfigs(targetPath);
|
|
58
53
|
|
|
59
54
|
spinner.text = 'Updating .gitignore...';
|
|
60
55
|
|
|
61
|
-
// Atualizar .gitignore
|
|
62
56
|
await updateGitignore(targetPath);
|
|
63
57
|
|
|
64
58
|
spinner.succeed(chalk.green('Installation completed!'));
|
|
65
59
|
|
|
66
60
|
console.log('');
|
|
67
61
|
|
|
68
|
-
// Mostrar versões configuradas
|
|
69
62
|
await showModelVersions(targetPath);
|
|
70
63
|
|
|
71
64
|
showNextSteps(targetPath);
|
|
@@ -78,7 +71,6 @@ async function installInProject(targetPath, options = {}) {
|
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
async function copyConfigs(targetPath) {
|
|
81
|
-
// Copiar regras do Cursor
|
|
82
74
|
const cursorRulesPath = path.join(TEMPLATE_DIR, '.cursor/rules');
|
|
83
75
|
if (fs.existsSync(cursorRulesPath)) {
|
|
84
76
|
await fs.copy(
|
|
@@ -89,7 +81,6 @@ async function copyConfigs(targetPath) {
|
|
|
89
81
|
showSuccess('Cursor rules');
|
|
90
82
|
}
|
|
91
83
|
|
|
92
|
-
// Copiar settings do Cursor
|
|
93
84
|
const cursorSettingsPath = path.join(TEMPLATE_DIR, '.cursor/settings.json');
|
|
94
85
|
if (fs.existsSync(cursorSettingsPath)) {
|
|
95
86
|
await fs.copy(
|
|
@@ -100,7 +91,6 @@ async function copyConfigs(targetPath) {
|
|
|
100
91
|
showSuccess('Cursor settings');
|
|
101
92
|
}
|
|
102
93
|
|
|
103
|
-
// Copiar settings do Claude
|
|
104
94
|
const claudeSettingsPath = path.join(TEMPLATE_DIR, '.claude/settings.json');
|
|
105
95
|
if (fs.existsSync(claudeSettingsPath)) {
|
|
106
96
|
await fs.copy(
|
|
@@ -111,7 +101,6 @@ async function copyConfigs(targetPath) {
|
|
|
111
101
|
showSuccess('Claude settings');
|
|
112
102
|
}
|
|
113
103
|
|
|
114
|
-
// Copiar instruções do Claude
|
|
115
104
|
const claudeInstructionsPath = path.join(TEMPLATE_DIR, 'CLAUDE.md');
|
|
116
105
|
if (fs.existsSync(claudeInstructionsPath)) {
|
|
117
106
|
await fs.copy(
|
|
@@ -122,7 +111,6 @@ async function copyConfigs(targetPath) {
|
|
|
122
111
|
showSuccess('Claude instructions');
|
|
123
112
|
}
|
|
124
113
|
|
|
125
|
-
// Copiar settings do Gemini
|
|
126
114
|
const geminiSettingsPath = path.join(TEMPLATE_DIR, '.gemini/settings.json');
|
|
127
115
|
if (fs.existsSync(geminiSettingsPath)) {
|
|
128
116
|
await fs.copy(
|
|
@@ -133,7 +121,6 @@ async function copyConfigs(targetPath) {
|
|
|
133
121
|
showSuccess('Gemini settings');
|
|
134
122
|
}
|
|
135
123
|
|
|
136
|
-
// Copiar instruções do Gemini
|
|
137
124
|
const geminiInstructionsPath = path.join(TEMPLATE_DIR, 'GEMINI.md');
|
|
138
125
|
if (fs.existsSync(geminiInstructionsPath)) {
|
|
139
126
|
await fs.copy(
|
|
@@ -144,7 +131,6 @@ async function copyConfigs(targetPath) {
|
|
|
144
131
|
showSuccess('Gemini instructions');
|
|
145
132
|
}
|
|
146
133
|
|
|
147
|
-
// Copiar Task Flow (preservando dados internos)
|
|
148
134
|
await copyTaskFlow(targetPath);
|
|
149
135
|
}
|
|
150
136
|
|
|
@@ -157,7 +143,20 @@ async function copyTaskFlow(targetPath) {
|
|
|
157
143
|
showSuccess('Task Flow directory');
|
|
158
144
|
showInfo('Note: .internal/tasks.json and .internal/status.json are NOT overwritten (your data is safe)');
|
|
159
145
|
|
|
160
|
-
|
|
146
|
+
const screensDest = path.join(taskFlowDest, 'screens');
|
|
147
|
+
await fs.ensureDir(screensDest);
|
|
148
|
+
showSuccess('Screenshots directory (.task-flow/screens/)');
|
|
149
|
+
|
|
150
|
+
const docsDest = path.join(taskFlowDest, 'docs');
|
|
151
|
+
await fs.ensureDir(docsDest);
|
|
152
|
+
showSuccess('Documentation directory (.task-flow/docs/)');
|
|
153
|
+
|
|
154
|
+
const exampleSrc = path.join(taskFlowSrc, 'screens/example.png.txt');
|
|
155
|
+
const exampleDest = path.join(screensDest, 'example.png.txt');
|
|
156
|
+
if (fs.existsSync(exampleSrc) && !fs.existsSync(exampleDest)) {
|
|
157
|
+
await fs.copy(exampleSrc, exampleDest);
|
|
158
|
+
}
|
|
159
|
+
|
|
161
160
|
const files = [
|
|
162
161
|
{ name: 'README.md', overwrite: true },
|
|
163
162
|
{ name: 'tasks.input.txt', overwrite: false },
|
|
@@ -169,27 +168,22 @@ async function copyTaskFlow(targetPath) {
|
|
|
169
168
|
const dest = path.join(taskFlowDest, file.name);
|
|
170
169
|
|
|
171
170
|
if (fs.existsSync(src)) {
|
|
172
|
-
// Sempre sobrescrever README, outros arquivos só se não existirem
|
|
173
171
|
if (file.overwrite || !fs.existsSync(dest)) {
|
|
174
172
|
await fs.copy(src, dest, { overwrite: file.overwrite });
|
|
175
173
|
}
|
|
176
174
|
}
|
|
177
175
|
}
|
|
178
|
-
|
|
179
|
-
// Não copiar .internal - deixar que seja criado pela IA
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
async function updateGitignore(targetPath) {
|
|
183
179
|
const gitignorePath = path.join(targetPath, '.gitignore');
|
|
184
180
|
|
|
185
|
-
// Criar se não existe
|
|
186
181
|
if (!fs.existsSync(gitignorePath)) {
|
|
187
182
|
await fs.writeFile(gitignorePath, '');
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
let content = await fs.readFile(gitignorePath, 'utf8');
|
|
191
186
|
|
|
192
|
-
// Entradas a adicionar
|
|
193
187
|
const entries = [
|
|
194
188
|
'.claude/',
|
|
195
189
|
'.gemini/',
|
|
@@ -199,16 +193,13 @@ async function updateGitignore(targetPath) {
|
|
|
199
193
|
'GEMINI.md'
|
|
200
194
|
];
|
|
201
195
|
|
|
202
|
-
// Remover entradas antigas (caso existam)
|
|
203
196
|
for (const entry of entries) {
|
|
204
197
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm');
|
|
205
198
|
content = content.replace(regex, '');
|
|
206
199
|
}
|
|
207
200
|
|
|
208
|
-
// Remover linhas vazias consecutivas
|
|
209
201
|
content = content.replace(/\n{3,}/g, '\n\n');
|
|
210
202
|
|
|
211
|
-
// Adicionar novas entradas
|
|
212
203
|
if (!content.endsWith('\n')) {
|
|
213
204
|
content += '\n';
|
|
214
205
|
}
|
|
@@ -228,7 +219,6 @@ async function showModelVersions(targetPath) {
|
|
|
228
219
|
|
|
229
220
|
let hasModels = false;
|
|
230
221
|
|
|
231
|
-
// Claude
|
|
232
222
|
const claudeSettingsPath = path.join(targetPath, '.claude/settings.json');
|
|
233
223
|
if (fs.existsSync(claudeSettingsPath)) {
|
|
234
224
|
try {
|
|
@@ -241,11 +231,9 @@ async function showModelVersions(targetPath) {
|
|
|
241
231
|
hasModels = true;
|
|
242
232
|
}
|
|
243
233
|
} catch (error) {
|
|
244
|
-
// Ignorar erros de parsing
|
|
245
234
|
}
|
|
246
235
|
}
|
|
247
236
|
|
|
248
|
-
// Cursor
|
|
249
237
|
const cursorSettingsPath = path.join(targetPath, '.cursor/settings.json');
|
|
250
238
|
if (fs.existsSync(cursorSettingsPath)) {
|
|
251
239
|
try {
|
|
@@ -255,21 +243,24 @@ async function showModelVersions(targetPath) {
|
|
|
255
243
|
hasModels = true;
|
|
256
244
|
}
|
|
257
245
|
} catch (error) {
|
|
258
|
-
// Ignorar erros de parsing
|
|
259
246
|
}
|
|
260
247
|
}
|
|
261
248
|
|
|
262
|
-
// Gemini
|
|
263
249
|
const geminiSettingsPath = path.join(targetPath, '.gemini/settings.json');
|
|
264
250
|
if (fs.existsSync(geminiSettingsPath)) {
|
|
265
251
|
try {
|
|
266
252
|
const settings = await fs.readJSON(geminiSettingsPath);
|
|
267
253
|
if (settings.model) {
|
|
268
|
-
|
|
254
|
+
const modelName = typeof settings.model === 'string'
|
|
255
|
+
? settings.model
|
|
256
|
+
: settings.model.name || 'Default (recommended)';
|
|
257
|
+
console.log(chalk.blue('Gemini:'), chalk.yellow(modelName));
|
|
258
|
+
hasModels = true;
|
|
259
|
+
} else {
|
|
260
|
+
console.log(chalk.blue('Gemini:'), chalk.yellow('Default (recommended)'));
|
|
269
261
|
hasModels = true;
|
|
270
262
|
}
|
|
271
263
|
} catch (error) {
|
|
272
|
-
// Ignorar erros de parsing
|
|
273
264
|
}
|
|
274
265
|
}
|
|
275
266
|
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
async function generateReport(taskId, targetPath = process.cwd()) {
|
|
7
|
+
const tasksPath = path.join(targetPath, '.task-flow/.internal/tasks.json');
|
|
8
|
+
const statusPath = path.join(targetPath, '.task-flow/.internal/status.json');
|
|
9
|
+
const docsDir = path.join(targetPath, '.task-flow/docs');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(tasksPath)) {
|
|
12
|
+
console.log(chalk.red('❌ Tasks file not found. Run "task-flow: sync" first.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(statusPath)) {
|
|
17
|
+
console.log(chalk.red('❌ Status file not found. Run "task-flow: sync" first.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const tasksData = await fs.readJSON(tasksPath);
|
|
23
|
+
const statusData = await fs.readJSON(statusPath);
|
|
24
|
+
|
|
25
|
+
const task = tasksData.tasks.find(t => t.id === parseInt(taskId));
|
|
26
|
+
|
|
27
|
+
if (!task) {
|
|
28
|
+
console.log(chalk.red(`❌ Task ${taskId} not found.`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const taskStatus = statusData.tasks[taskId.toString()];
|
|
33
|
+
|
|
34
|
+
if (!taskStatus) {
|
|
35
|
+
console.log(chalk.yellow(`⚠️ Task ${taskId} has no status information.`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isCompleted = taskStatus && taskStatus.status === 'done';
|
|
39
|
+
const allSubtasksDone = taskStatus && taskStatus.subtasks
|
|
40
|
+
? Object.values(taskStatus.subtasks).every(status => status === 'done')
|
|
41
|
+
: false;
|
|
42
|
+
|
|
43
|
+
if (!isCompleted && !allSubtasksDone) {
|
|
44
|
+
console.log(chalk.yellow(`⚠️ Task ${taskId} is not completed. Generating partial report...`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await fs.ensureDir(docsDir);
|
|
48
|
+
|
|
49
|
+
const reportContent = await buildReport(task, taskStatus, targetPath);
|
|
50
|
+
const reportPath = path.join(docsDir, `task-${taskId}-implementation.md`);
|
|
51
|
+
|
|
52
|
+
await fs.writeFile(reportPath, reportContent, 'utf8');
|
|
53
|
+
|
|
54
|
+
console.log(chalk.green(`✅ Report generated: ${reportPath}`));
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(chalk.red('Error generating report:'), error.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function buildReport(task, taskStatus, targetPath) {
|
|
62
|
+
const completionDate = new Date().toISOString().split('T')[0];
|
|
63
|
+
|
|
64
|
+
let content = `# Task ${task.id}: ${task.title}\n\n`;
|
|
65
|
+
content += `**Status**: ${taskStatus?.status === 'done' ? '✅ Completed' : '⏳ In Progress'}\n`;
|
|
66
|
+
content += `**Report Date**: ${completionDate}\n\n`;
|
|
67
|
+
|
|
68
|
+
content += `## Overview\n\n`;
|
|
69
|
+
content += `${task.description || task.originalRequest || 'No description available.'}\n\n`;
|
|
70
|
+
|
|
71
|
+
const completedSubtasks = task.subtasks?.filter(st =>
|
|
72
|
+
taskStatus?.subtasks?.[st.id.toString()] === 'done'
|
|
73
|
+
) || [];
|
|
74
|
+
|
|
75
|
+
content += `## Implementation Summary\n\n`;
|
|
76
|
+
content += `This task was completed through ${completedSubtasks.length} of ${task.subtasks?.length || 0} subtasks. `;
|
|
77
|
+
content += `The implementation involved creating and modifying code files to achieve the task objectives.\n\n`;
|
|
78
|
+
|
|
79
|
+
const fileChanges = await getFileChanges(targetPath, task.id);
|
|
80
|
+
const changeSummaries = await analyzeFileChanges(targetPath, fileChanges);
|
|
81
|
+
|
|
82
|
+
if (fileChanges.created.length > 0 || fileChanges.modified.length > 0) {
|
|
83
|
+
content += `## Code Changes\n\n`;
|
|
84
|
+
|
|
85
|
+
if (fileChanges.created.length > 0) {
|
|
86
|
+
content += `### New Files Created\n\n`;
|
|
87
|
+
fileChanges.created.forEach(file => {
|
|
88
|
+
const summary = changeSummaries[file];
|
|
89
|
+
content += `#### \`${file}\`\n\n`;
|
|
90
|
+
if (summary) {
|
|
91
|
+
content += `${summary}\n\n`;
|
|
92
|
+
} else {
|
|
93
|
+
content += `New file created as part of the implementation.\n\n`;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (fileChanges.modified.length > 0) {
|
|
99
|
+
content += `### Files Modified\n\n`;
|
|
100
|
+
fileChanges.modified.forEach(file => {
|
|
101
|
+
const summary = changeSummaries[file];
|
|
102
|
+
content += `#### \`${file}\`\n\n`;
|
|
103
|
+
if (summary) {
|
|
104
|
+
content += `${summary}\n\n`;
|
|
105
|
+
} else {
|
|
106
|
+
content += `File was modified to implement required functionality.\n\n`;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (task.subtasks && completedSubtasks.length > 0) {
|
|
113
|
+
content += `## Completed Subtasks\n\n`;
|
|
114
|
+
|
|
115
|
+
completedSubtasks.forEach(subtask => {
|
|
116
|
+
content += `### ✅ Subtask ${task.id}.${subtask.id}: ${subtask.title}\n\n`;
|
|
117
|
+
content += `${subtask.description || 'No description available.'}\n\n`;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (fileChanges.created.length === 0 && fileChanges.modified.length === 0) {
|
|
122
|
+
content += `## Note\n\n`;
|
|
123
|
+
content += `_File changes could not be automatically detected from git history. `;
|
|
124
|
+
content += `This may indicate that changes haven't been committed yet, or the task was completed without git tracking._\n\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (task.createdAt) {
|
|
128
|
+
content += `---\n\n`;
|
|
129
|
+
content += `**Task Created**: ${new Date(task.createdAt).toLocaleDateString()}\n`;
|
|
130
|
+
if (taskStatus?.status === 'done') {
|
|
131
|
+
content += `**Task Completed**: ${completionDate}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return content;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getFileChanges(targetPath, taskId) {
|
|
139
|
+
const created = [];
|
|
140
|
+
const modified = [];
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const isGitRepo = fs.existsSync(path.join(targetPath, '.git'));
|
|
144
|
+
|
|
145
|
+
if (!isGitRepo) {
|
|
146
|
+
return { created, modified };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const gitLog = execSync(
|
|
150
|
+
`git log --all --oneline --grep="task ${taskId}" --grep="Task ${taskId}" --grep="task-${taskId}" --grep="Task ${taskId}:" -i`,
|
|
151
|
+
{ cwd: targetPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
152
|
+
).trim();
|
|
153
|
+
|
|
154
|
+
if (gitLog) {
|
|
155
|
+
const commits = gitLog.split('\n').map(line => line.split(' ')[0]);
|
|
156
|
+
|
|
157
|
+
for (const commit of commits) {
|
|
158
|
+
try {
|
|
159
|
+
const diff = execSync(
|
|
160
|
+
`git diff-tree --no-commit-id --name-status -r ${commit}`,
|
|
161
|
+
{ cwd: targetPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
162
|
+
).trim();
|
|
163
|
+
|
|
164
|
+
diff.split('\n').forEach(line => {
|
|
165
|
+
const match = line.match(/^([AMD])\s+(.+)$/);
|
|
166
|
+
if (match) {
|
|
167
|
+
const status = match[1];
|
|
168
|
+
const file = match[2];
|
|
169
|
+
|
|
170
|
+
if (status === 'A') {
|
|
171
|
+
if (!created.includes(file)) created.push(file);
|
|
172
|
+
} else if (status === 'M' || status === 'D') {
|
|
173
|
+
if (!modified.includes(file) && !created.includes(file)) modified.push(file);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { created, modified };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function analyzeFileChanges(targetPath, fileChanges) {
|
|
188
|
+
const summaries = {};
|
|
189
|
+
|
|
190
|
+
for (const file of [...fileChanges.created, ...fileChanges.modified]) {
|
|
191
|
+
const filePath = path.join(targetPath, file);
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(filePath)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
199
|
+
const fileExt = path.extname(file).toLowerCase();
|
|
200
|
+
|
|
201
|
+
let summary = '';
|
|
202
|
+
|
|
203
|
+
if (fileChanges.created.includes(file)) {
|
|
204
|
+
summary = generateFileSummary(file, fileContent, fileExt, 'created');
|
|
205
|
+
} else {
|
|
206
|
+
summary = generateFileSummary(file, fileContent, fileExt, 'modified');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
summaries[file] = summary;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
summaries[file] = `File ${fileChanges.created.includes(file) ? 'created' : 'modified'} as part of the implementation.`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return summaries;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function generateFileSummary(filePath, content, ext, type) {
|
|
219
|
+
const fileName = path.basename(filePath);
|
|
220
|
+
const lines = content.split('\n').length;
|
|
221
|
+
|
|
222
|
+
let summary = '';
|
|
223
|
+
|
|
224
|
+
if (type === 'created') {
|
|
225
|
+
summary = `This new file was created to implement part of the task. `;
|
|
226
|
+
} else {
|
|
227
|
+
summary = `This file was modified to implement required functionality. `;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
summary += `The file contains approximately ${lines} lines of code. `;
|
|
231
|
+
|
|
232
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
|
|
233
|
+
const hasExports = /export\s+(default\s+)?(function|class|const|let|var|interface|type|enum)/.test(content);
|
|
234
|
+
const hasImports = /^import\s+/.test(content);
|
|
235
|
+
const hasComponents = /(function|const)\s+\w+\s*[=:]\s*(\(|function|React\.FC)/.test(content);
|
|
236
|
+
const functionCount = (content.match(/(?:function|const|let|var)\s+\w+\s*[=:]/g) || []).length;
|
|
237
|
+
const classCount = (content.match(/class\s+\w+/g) || []).length;
|
|
238
|
+
|
|
239
|
+
if (hasComponents) {
|
|
240
|
+
summary += `It appears to be a React component or JavaScript module. `;
|
|
241
|
+
} else if (classCount > 0) {
|
|
242
|
+
summary += `It contains ${classCount} class${classCount > 1 ? 'es' : ''}. `;
|
|
243
|
+
} else if (functionCount > 0) {
|
|
244
|
+
summary += `It contains ${functionCount} function${functionCount > 1 ? 's' : ''} or method${functionCount > 1 ? 's' : ''}. `;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasExports) {
|
|
248
|
+
summary += `The file exports functionality for use in other parts of the application. `;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (hasImports) {
|
|
252
|
+
const importCount = (content.match(/^import\s+/gm) || []).length;
|
|
253
|
+
summary += `It imports ${importCount} dependenc${importCount > 1 ? 'ies' : 'y'} from other modules.`;
|
|
254
|
+
}
|
|
255
|
+
} else if (ext === '.css' || ext === '.scss' || ext === '.less') {
|
|
256
|
+
const ruleCount = (content.match(/[^{}]+{[^}]*}/g) || []).length;
|
|
257
|
+
summary += `It contains ${ruleCount} CSS rule${ruleCount !== 1 ? 's' : ''} for styling the user interface.`;
|
|
258
|
+
} else if (ext === '.json') {
|
|
259
|
+
summary += `It contains configuration or data in JSON format.`;
|
|
260
|
+
} else if (ext === '.md') {
|
|
261
|
+
summary += `It contains documentation or markdown content.`;
|
|
262
|
+
} else if (ext === '.test.ts' || ext === '.test.js' || ext === '.spec.ts' || ext === '.spec.js') {
|
|
263
|
+
const testCount = (content.match(/(?:it|test|describe|test\(|it\(|describe\()/g) || []).length;
|
|
264
|
+
summary += `It contains ${testCount} test case${testCount !== 1 ? 's' : ''} for verifying the implementation.`;
|
|
265
|
+
} else if (ext === '.html') {
|
|
266
|
+
summary += `It contains HTML markup for the user interface.`;
|
|
267
|
+
} else if (ext === '.py') {
|
|
268
|
+
const functionCount = (content.match(/def\s+\w+/g) || []).length;
|
|
269
|
+
const classCount = (content.match(/class\s+\w+/g) || []).length;
|
|
270
|
+
summary += `It contains ${functionCount} function${functionCount !== 1 ? 's' : ''} and ${classCount} class${classCount !== 1 ? 'es' : ''}.`;
|
|
271
|
+
} else {
|
|
272
|
+
summary += `It is a ${ext.substring(1).toUpperCase()} file.`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return summary;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { generateReport };
|
package/lib/version.js
CHANGED
|
@@ -35,17 +35,14 @@ async function checkVersionUpdates() {
|
|
|
35
35
|
|
|
36
36
|
const rl = createInterface();
|
|
37
37
|
|
|
38
|
-
// Check Claude
|
|
39
38
|
if (versions.claude) {
|
|
40
39
|
await checkModelVersion('Claude', versions.claude, '.claude/settings.json', rl);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// Check Cursor
|
|
44
42
|
if (versions.cursor) {
|
|
45
43
|
await checkModelVersion('Cursor', versions.cursor, '.cursor/settings.json', rl);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
// Check Gemini
|
|
49
46
|
if (versions.gemini) {
|
|
50
47
|
await checkModelVersion('Gemini', versions.gemini, '.gemini/settings.json', rl);
|
|
51
48
|
}
|
|
@@ -69,7 +66,9 @@ async function checkModelVersion(modelName, versionInfo, settingsPath, rl) {
|
|
|
69
66
|
|
|
70
67
|
try {
|
|
71
68
|
const settings = await fs.readJSON(fullSettingsPath);
|
|
72
|
-
const currentVersion = settings.model
|
|
69
|
+
const currentVersion = typeof settings.model === 'string'
|
|
70
|
+
? settings.model
|
|
71
|
+
: settings.model?.name;
|
|
73
72
|
|
|
74
73
|
if (!currentVersion) {
|
|
75
74
|
console.log(chalk.gray(`${modelName}: No model configured`));
|
|
@@ -95,7 +94,11 @@ async function checkModelVersion(modelName, versionInfo, settingsPath, rl) {
|
|
|
95
94
|
);
|
|
96
95
|
|
|
97
96
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
98
|
-
settings.model
|
|
97
|
+
if (typeof settings.model === 'object' && settings.model !== null) {
|
|
98
|
+
settings.model.name = versionInfo.latest;
|
|
99
|
+
} else {
|
|
100
|
+
settings.model = versionInfo.latest;
|
|
101
|
+
}
|
|
99
102
|
await fs.writeJSON(fullSettingsPath, settings, { spaces: 2 });
|
|
100
103
|
console.log(chalk.green(` ✅ ${modelName} updated to ${versionInfo.latest}`));
|
|
101
104
|
console.log(chalk.cyan(' (Repository template updated - run init/update on projects to apply)'));
|