rbin-task-flow 1.1.2 → 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 +4 -4
- 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 +11 -32
- 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,19 +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
|
-
// Criar pasta screens
|
|
161
146
|
const screensDest = path.join(taskFlowDest, 'screens');
|
|
162
147
|
await fs.ensureDir(screensDest);
|
|
163
148
|
showSuccess('Screenshots directory (.task-flow/screens/)');
|
|
164
149
|
|
|
165
|
-
|
|
150
|
+
const docsDest = path.join(taskFlowDest, 'docs');
|
|
151
|
+
await fs.ensureDir(docsDest);
|
|
152
|
+
showSuccess('Documentation directory (.task-flow/docs/)');
|
|
153
|
+
|
|
166
154
|
const exampleSrc = path.join(taskFlowSrc, 'screens/example.png.txt');
|
|
167
155
|
const exampleDest = path.join(screensDest, 'example.png.txt');
|
|
168
156
|
if (fs.existsSync(exampleSrc) && !fs.existsSync(exampleDest)) {
|
|
169
157
|
await fs.copy(exampleSrc, exampleDest);
|
|
170
158
|
}
|
|
171
159
|
|
|
172
|
-
// Copiar apenas templates, não sobrescrever dados do usuário
|
|
173
160
|
const files = [
|
|
174
161
|
{ name: 'README.md', overwrite: true },
|
|
175
162
|
{ name: 'tasks.input.txt', overwrite: false },
|
|
@@ -181,27 +168,22 @@ async function copyTaskFlow(targetPath) {
|
|
|
181
168
|
const dest = path.join(taskFlowDest, file.name);
|
|
182
169
|
|
|
183
170
|
if (fs.existsSync(src)) {
|
|
184
|
-
// Sempre sobrescrever README, outros arquivos só se não existirem
|
|
185
171
|
if (file.overwrite || !fs.existsSync(dest)) {
|
|
186
172
|
await fs.copy(src, dest, { overwrite: file.overwrite });
|
|
187
173
|
}
|
|
188
174
|
}
|
|
189
175
|
}
|
|
190
|
-
|
|
191
|
-
// Não copiar .internal - deixar que seja criado pela IA
|
|
192
176
|
}
|
|
193
177
|
|
|
194
178
|
async function updateGitignore(targetPath) {
|
|
195
179
|
const gitignorePath = path.join(targetPath, '.gitignore');
|
|
196
180
|
|
|
197
|
-
// Criar se não existe
|
|
198
181
|
if (!fs.existsSync(gitignorePath)) {
|
|
199
182
|
await fs.writeFile(gitignorePath, '');
|
|
200
183
|
}
|
|
201
184
|
|
|
202
185
|
let content = await fs.readFile(gitignorePath, 'utf8');
|
|
203
186
|
|
|
204
|
-
// Entradas a adicionar
|
|
205
187
|
const entries = [
|
|
206
188
|
'.claude/',
|
|
207
189
|
'.gemini/',
|
|
@@ -211,16 +193,13 @@ async function updateGitignore(targetPath) {
|
|
|
211
193
|
'GEMINI.md'
|
|
212
194
|
];
|
|
213
195
|
|
|
214
|
-
// Remover entradas antigas (caso existam)
|
|
215
196
|
for (const entry of entries) {
|
|
216
197
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gm');
|
|
217
198
|
content = content.replace(regex, '');
|
|
218
199
|
}
|
|
219
200
|
|
|
220
|
-
// Remover linhas vazias consecutivas
|
|
221
201
|
content = content.replace(/\n{3,}/g, '\n\n');
|
|
222
202
|
|
|
223
|
-
// Adicionar novas entradas
|
|
224
203
|
if (!content.endsWith('\n')) {
|
|
225
204
|
content += '\n';
|
|
226
205
|
}
|
|
@@ -240,7 +219,6 @@ async function showModelVersions(targetPath) {
|
|
|
240
219
|
|
|
241
220
|
let hasModels = false;
|
|
242
221
|
|
|
243
|
-
// Claude
|
|
244
222
|
const claudeSettingsPath = path.join(targetPath, '.claude/settings.json');
|
|
245
223
|
if (fs.existsSync(claudeSettingsPath)) {
|
|
246
224
|
try {
|
|
@@ -253,11 +231,9 @@ async function showModelVersions(targetPath) {
|
|
|
253
231
|
hasModels = true;
|
|
254
232
|
}
|
|
255
233
|
} catch (error) {
|
|
256
|
-
// Ignorar erros de parsing
|
|
257
234
|
}
|
|
258
235
|
}
|
|
259
236
|
|
|
260
|
-
// Cursor
|
|
261
237
|
const cursorSettingsPath = path.join(targetPath, '.cursor/settings.json');
|
|
262
238
|
if (fs.existsSync(cursorSettingsPath)) {
|
|
263
239
|
try {
|
|
@@ -267,21 +243,24 @@ async function showModelVersions(targetPath) {
|
|
|
267
243
|
hasModels = true;
|
|
268
244
|
}
|
|
269
245
|
} catch (error) {
|
|
270
|
-
// Ignorar erros de parsing
|
|
271
246
|
}
|
|
272
247
|
}
|
|
273
248
|
|
|
274
|
-
// Gemini
|
|
275
249
|
const geminiSettingsPath = path.join(targetPath, '.gemini/settings.json');
|
|
276
250
|
if (fs.existsSync(geminiSettingsPath)) {
|
|
277
251
|
try {
|
|
278
252
|
const settings = await fs.readJSON(geminiSettingsPath);
|
|
279
253
|
if (settings.model) {
|
|
280
|
-
|
|
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)'));
|
|
281
261
|
hasModels = true;
|
|
282
262
|
}
|
|
283
263
|
} catch (error) {
|
|
284
|
-
// Ignorar erros de parsing
|
|
285
264
|
}
|
|
286
265
|
}
|
|
287
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)'));
|