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/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
 
@@ -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
- // Copiar arquivo de exemplo para screens se não existir
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
- console.log(chalk.blue('Gemini:'), chalk.yellow(settings.model));
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 = versionInfo.latest;
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)'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rbin-task-flow",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered task management for Claude, Cursor, and Gemini",
5
5
  "main": "index.js",
6
6
  "bin": {