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/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,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
- // Copiar apenas templates, não sobrescrever dados do usuário
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
- 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)'));
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 = 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.0",
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": {