claude-code-templates 1.10.0 → 1.11.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.
Files changed (35) hide show
  1. package/README.md +6 -0
  2. package/bin/create-claude-config.js +1 -0
  3. package/package.json +2 -2
  4. package/src/analytics/core/ConversationAnalyzer.js +94 -20
  5. package/src/analytics/core/FileWatcher.js +146 -11
  6. package/src/analytics/data/DataCache.js +124 -19
  7. package/src/analytics/notifications/NotificationManager.js +37 -0
  8. package/src/analytics/notifications/WebSocketServer.js +1 -1
  9. package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
  10. package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
  11. package/src/analytics-web/components/AgentsPage.js +2535 -0
  12. package/src/analytics-web/components/App.js +430 -0
  13. package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
  14. package/src/analytics-web/components/DashboardPage.js +1527 -0
  15. package/src/analytics-web/components/Sidebar.js +197 -0
  16. package/src/analytics-web/components/ToolDisplay.js +539 -0
  17. package/src/analytics-web/index.html +3275 -1792
  18. package/src/analytics-web/services/DataService.js +89 -16
  19. package/src/analytics-web/services/StateService.js +9 -0
  20. package/src/analytics-web/services/WebSocketService.js +17 -5
  21. package/src/analytics.js +323 -35
  22. package/src/console-bridge.js +610 -0
  23. package/src/file-operations.js +143 -23
  24. package/src/hook-scanner.js +21 -1
  25. package/src/index.js +24 -1
  26. package/src/templates.js +28 -0
  27. package/src/test-console-bridge.js +67 -0
  28. package/src/utils.js +46 -0
  29. package/templates/ruby/.claude/commands/model.md +360 -0
  30. package/templates/ruby/.claude/commands/test.md +480 -0
  31. package/templates/ruby/.claude/settings.json +146 -0
  32. package/templates/ruby/.mcp.json +83 -0
  33. package/templates/ruby/CLAUDE.md +284 -0
  34. package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +490 -0
  35. package/templates/ruby/examples/rails-app/CLAUDE.md +376 -0
@@ -1,29 +1,117 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
4
5
  const { getHooksForLanguage, filterHooksBySelection, getMCPsForLanguage, filterMCPsBySelection } = require('./hook-scanner');
5
6
 
6
- async function copyTemplateFiles(templateConfig, targetDir) {
7
- const templateDir = path.join(__dirname, '../templates');
7
+ async function checkExistingFiles(targetDir, templateConfig) {
8
+ const existingFiles = [];
8
9
 
9
- // Check if CLAUDE.md already exists
10
+ // Check for existing CLAUDE.md
10
11
  const claudeFile = path.join(targetDir, 'CLAUDE.md');
11
12
  if (await fs.pathExists(claudeFile)) {
12
- // Create backup
13
- const backupFile = path.join(targetDir, 'CLAUDE.md.backup');
14
- await fs.copy(claudeFile, backupFile);
15
- console.log(chalk.yellow(`📋 Existing CLAUDE.md backed up to CLAUDE.md.backup`));
13
+ existingFiles.push('CLAUDE.md');
16
14
  }
17
15
 
18
- // Check if .claude directory already exists
16
+ // Check for existing .claude directory
19
17
  const claudeDir = path.join(targetDir, '.claude');
20
18
  if (await fs.pathExists(claudeDir)) {
21
- // Create backup
22
- const backupDir = path.join(targetDir, '.claude.backup');
23
- await fs.copy(claudeDir, backupDir);
24
- console.log(chalk.yellow(`📁 Existing .claude directory backed up to .claude.backup`));
19
+ existingFiles.push('.claude/');
20
+ }
21
+
22
+ // Check for existing .mcp.json
23
+ const mcpFile = path.join(targetDir, '.mcp.json');
24
+ if (await fs.pathExists(mcpFile)) {
25
+ existingFiles.push('.mcp.json');
26
+ }
27
+
28
+ return existingFiles;
29
+ }
30
+
31
+ async function promptUserForOverwrite(existingFiles, targetDir) {
32
+ if (existingFiles.length === 0) {
33
+ return 'proceed'; // No existing files, safe to proceed
34
+ }
35
+
36
+ console.log(chalk.yellow('\n⚠️ Existing Claude Code configuration detected!'));
37
+ console.log(chalk.yellow('The following files/directories already exist:'));
38
+ existingFiles.forEach(file => {
39
+ console.log(chalk.yellow(` • ${file}`));
40
+ });
41
+
42
+ const choices = [
43
+ {
44
+ name: '🔄 Backup and overwrite - Create backups and install new configuration',
45
+ value: 'backup',
46
+ short: 'Backup and overwrite'
47
+ },
48
+ {
49
+ name: '🔀 Merge configurations - Combine existing with new templates',
50
+ value: 'merge',
51
+ short: 'Merge'
52
+ },
53
+ {
54
+ name: '❌ Cancel setup - Keep existing configuration unchanged',
55
+ value: 'cancel',
56
+ short: 'Cancel'
57
+ }
58
+ ];
59
+
60
+ const answer = await inquirer.prompt([{
61
+ type: 'list',
62
+ name: 'action',
63
+ message: 'How would you like to proceed?',
64
+ choices,
65
+ default: 'backup'
66
+ }]);
67
+
68
+ return answer.action;
69
+ }
70
+
71
+ async function createBackups(existingFiles, targetDir) {
72
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
73
+
74
+ for (const file of existingFiles) {
75
+ const sourcePath = path.join(targetDir, file);
76
+ const backupPath = path.join(targetDir, `${file.replace('/', '')}.backup-${timestamp}`);
77
+
78
+ try {
79
+ await fs.copy(sourcePath, backupPath);
80
+ console.log(chalk.green(`📋 Backed up ${file} → ${path.basename(backupPath)}`));
81
+ } catch (error) {
82
+ console.error(chalk.red(`✗ Failed to backup ${file}:`), error.message);
83
+ throw error;
84
+ }
85
+ }
86
+ }
87
+
88
+ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
89
+ const templateDir = path.join(__dirname, '../templates');
90
+
91
+ // Check for existing files and get user preference
92
+ const existingFiles = await checkExistingFiles(targetDir, templateConfig);
93
+ let userAction = 'proceed';
94
+
95
+ if (!options.yes && !options.dryRun) {
96
+ userAction = await promptUserForOverwrite(existingFiles, targetDir);
97
+
98
+ if (userAction === 'cancel') {
99
+ console.log(chalk.blue('✓ Setup cancelled. Your existing configuration remains unchanged.'));
100
+ return false; // Indicate cancellation
101
+ }
102
+ } else if (existingFiles.length > 0) {
103
+ // In --yes mode, default to backup behavior
104
+ userAction = 'backup';
105
+ }
106
+
107
+ // Create backups if requested
108
+ if (userAction === 'backup' && existingFiles.length > 0) {
109
+ await createBackups(existingFiles, targetDir);
25
110
  }
26
111
 
112
+ // Determine overwrite behavior based on user choice
113
+ const shouldOverwrite = userAction !== 'merge';
114
+
27
115
  // Copy base files and framework-specific files
28
116
  for (const file of templateConfig.files) {
29
117
  const sourcePath = path.join(templateDir, file.source);
@@ -40,14 +128,21 @@ async function copyTemplateFiles(templateConfig, targetDir) {
40
128
  for (const frameworkFile of frameworkFiles) {
41
129
  const srcFile = path.join(sourcePath, frameworkFile);
42
130
  const destFile = path.join(destPath, frameworkFile);
43
- await fs.copy(srcFile, destFile, { overwrite: true });
131
+
132
+ // In merge mode, skip if file already exists
133
+ if (userAction === 'merge' && await fs.pathExists(destFile)) {
134
+ console.log(chalk.blue(`⏭️ Skipped ${frameworkFile} (already exists)`));
135
+ continue;
136
+ }
137
+
138
+ await fs.copy(srcFile, destFile, { overwrite: shouldOverwrite });
44
139
  }
45
140
 
46
141
  console.log(chalk.green(`✓ Copied framework commands ${file.source} → ${file.destination}`));
47
142
  } else if (file.source.includes('.claude') && !file.source.includes('examples/')) {
48
143
  // This is base .claude directory - copy it but handle commands specially
49
144
  await fs.copy(sourcePath, destPath, {
50
- overwrite: true,
145
+ overwrite: shouldOverwrite,
51
146
  filter: (src) => {
52
147
  // Skip the commands directory itself - we'll handle it separately
53
148
  return !src.endsWith('.claude/commands');
@@ -69,24 +164,47 @@ async function copyTemplateFiles(templateConfig, targetDir) {
69
164
  if (!excludeCommands.includes(baseCommand)) {
70
165
  const srcFile = path.join(baseCommandsPath, baseCommand);
71
166
  const destFile = path.join(destCommandsPath, baseCommand);
72
- await fs.copy(srcFile, destFile, { overwrite: true });
167
+
168
+ // In merge mode, skip if file already exists
169
+ if (userAction === 'merge' && await fs.pathExists(destFile)) {
170
+ console.log(chalk.blue(`⏭️ Skipped ${baseCommand} (already exists)`));
171
+ continue;
172
+ }
173
+
174
+ await fs.copy(srcFile, destFile, { overwrite: shouldOverwrite });
73
175
  }
74
176
  }
75
177
  }
76
178
 
77
179
  console.log(chalk.green(`✓ Copied base configuration and commands ${file.source} → ${file.destination}`));
78
180
  } else if (file.source.includes('settings.json') && templateConfig.selectedHooks) {
79
- // Handle settings.json with hook filtering
80
- await processSettingsFile(sourcePath, destPath, templateConfig);
81
- console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected hooks)`));
181
+ // In merge mode, merge settings instead of overwriting
182
+ if (userAction === 'merge') {
183
+ await mergeSettingsFile(sourcePath, destPath, templateConfig);
184
+ console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected hooks)`));
185
+ } else {
186
+ await processSettingsFile(sourcePath, destPath, templateConfig);
187
+ console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected hooks)`));
188
+ }
82
189
  } else if (file.source.includes('.mcp.json') && templateConfig.selectedMCPs) {
83
- // Handle .mcp.json with MCP filtering
84
- await processMCPFile(sourcePath, destPath, templateConfig);
85
- console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected MCPs)`));
190
+ // In merge mode, merge MCP config instead of overwriting
191
+ if (userAction === 'merge') {
192
+ await mergeMCPFile(sourcePath, destPath, templateConfig);
193
+ console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected MCPs)`));
194
+ } else {
195
+ await processMCPFile(sourcePath, destPath, templateConfig);
196
+ console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected MCPs)`));
197
+ }
86
198
  } else {
87
- // Copy regular files (CLAUDE.md, settings.json, etc.)
199
+ // Copy regular files (CLAUDE.md, etc.)
200
+ // In merge mode, skip if file already exists
201
+ if (userAction === 'merge' && await fs.pathExists(destPath)) {
202
+ console.log(chalk.blue(`⏭️ Skipped ${file.destination} (already exists)`));
203
+ continue;
204
+ }
205
+
88
206
  await fs.copy(sourcePath, destPath, {
89
- overwrite: true,
207
+ overwrite: shouldOverwrite,
90
208
  filter: (src) => {
91
209
  // Skip commands directory during regular copy - we handle them above
92
210
  return !src.includes('.claude/commands');
@@ -100,6 +218,8 @@ async function copyTemplateFiles(templateConfig, targetDir) {
100
218
  }
101
219
  }
102
220
 
221
+ return true; // Indicate successful completion
222
+
103
223
  // Copy selected commands individually
104
224
  if (templateConfig.selectedCommands && templateConfig.selectedCommands.length > 0) {
105
225
  const commandsDir = path.join(targetDir, '.claude', 'commands');
@@ -123,6 +123,10 @@ function getHookDescription(hook, matcher, type) {
123
123
  return 'Block print() statements in Python files';
124
124
  }
125
125
 
126
+ if (command.includes('puts\\|p ') && command.includes('rb$')) {
127
+ return 'Block puts/p statements in Ruby files';
128
+ }
129
+
126
130
  if (command.includes('fmt.Print') && command.includes('go$')) {
127
131
  return 'Block fmt.Print statements in Go files';
128
132
  }
@@ -131,7 +135,7 @@ function getHookDescription(hook, matcher, type) {
131
135
  return 'Block println! macros in Rust files';
132
136
  }
133
137
 
134
- if (command.includes('npm audit') || command.includes('pip-audit') || command.includes('cargo audit')) {
138
+ if (command.includes('npm audit') || command.includes('pip-audit') || command.includes('bundle audit') || command.includes('cargo audit')) {
135
139
  return 'Security audit for dependencies';
136
140
  }
137
141
 
@@ -143,6 +147,18 @@ function getHookDescription(hook, matcher, type) {
143
147
  return 'Auto-format Python files with Black';
144
148
  }
145
149
 
150
+ if (command.includes('rubocop -A') && command.includes('rb$')) {
151
+ return 'Auto-format Ruby files with RuboCop';
152
+ }
153
+
154
+ if (command.includes('rubocop') && command.includes('rb$') && !command.includes('-A')) {
155
+ return 'Run Ruby linting with RuboCop';
156
+ }
157
+
158
+ if (command.includes('brakeman')) {
159
+ return 'Run Ruby security scan with Brakeman';
160
+ }
161
+
146
162
  if (command.includes('isort') && command.includes('py$')) {
147
163
  return 'Auto-sort Python imports with isort';
148
164
  }
@@ -195,6 +211,10 @@ function getHookDescription(hook, matcher, type) {
195
211
  return 'Auto-run Python tests for modified files';
196
212
  }
197
213
 
214
+ if (command.includes('rspec')) {
215
+ return 'Auto-run Ruby tests with RSpec';
216
+ }
217
+
198
218
  if (command.includes('go test')) {
199
219
  return 'Auto-run Go tests for modified files';
200
220
  }
package/src/index.js CHANGED
@@ -28,6 +28,11 @@ async function showMainMenu() {
28
28
  value: 'analytics',
29
29
  short: 'Analytics Dashboard'
30
30
  },
31
+ {
32
+ name: '💬 Chats Dashboard - View and analyze your Claude conversations',
33
+ value: 'chats',
34
+ short: 'Chats Dashboard'
35
+ },
31
36
  {
32
37
  name: '🔍 Health Check - Verify your Claude Code setup and configuration',
33
38
  value: 'health',
@@ -48,6 +53,12 @@ async function showMainMenu() {
48
53
  return;
49
54
  }
50
55
 
56
+ if (initialChoice.action === 'chats') {
57
+ console.log(chalk.blue('💬 Launching Claude Code Chats Dashboard...'));
58
+ await runAnalytics({ openTo: 'agents' });
59
+ return;
60
+ }
61
+
51
62
  if (initialChoice.action === 'health') {
52
63
  console.log(chalk.blue('🔍 Running Health Check...'));
53
64
  const healthResult = await runHealthCheck();
@@ -93,6 +104,12 @@ async function createClaudeConfig(options = {}) {
93
104
  return;
94
105
  }
95
106
 
107
+ // Handle chats/agents dashboard
108
+ if (options.chats || options.agents) {
109
+ await runAnalytics({ ...options, openTo: 'agents' });
110
+ return;
111
+ }
112
+
96
113
  // Handle health check
97
114
  let shouldRunSetup = false;
98
115
  if (options.healthCheck || options.health || options.check || options.verify) {
@@ -187,7 +204,11 @@ async function createClaudeConfig(options = {}) {
187
204
  // Copy template files
188
205
  const copySpinner = ora('Copying template files...').start();
189
206
  try {
190
- await copyTemplateFiles(templateConfig, targetDir);
207
+ const result = await copyTemplateFiles(templateConfig, targetDir, options);
208
+ if (result === false) {
209
+ copySpinner.info('Setup cancelled by user');
210
+ return; // Exit early if user cancelled
211
+ }
191
212
  copySpinner.succeed('Template files copied successfully');
192
213
  } catch (error) {
193
214
  copySpinner.fail('Failed to copy template files');
@@ -200,6 +221,8 @@ async function createClaudeConfig(options = {}) {
200
221
  console.log(chalk.white(' 1. Review the generated CLAUDE.md file'));
201
222
  console.log(chalk.white(' 2. Customize the configuration for your project'));
202
223
  console.log(chalk.white(' 3. Start using Claude Code with: claude'));
224
+ console.log('');
225
+ console.log(chalk.blue('🌐 View all available templates at: https://davila7.github.io/claude-code-templates/'));
203
226
 
204
227
  if (config.language !== 'common') {
205
228
  console.log(chalk.yellow(`💡 Language-specific features for ${config.language} have been configured`));
package/src/templates.js CHANGED
@@ -8,6 +8,7 @@ const TEMPLATES_CONFIG = {
8
8
  files: [
9
9
  { source: 'common/CLAUDE.md', destination: 'CLAUDE.md' },
10
10
  { source: 'common/.claude', destination: '.claude' },
11
+ { source: 'common/.claude/settings.json', destination: '.claude/settings.json' },
11
12
  { source: 'common/.mcp.json', destination: '.mcp.json' }
12
13
  ]
13
14
  },
@@ -17,6 +18,7 @@ const TEMPLATES_CONFIG = {
17
18
  files: [
18
19
  { source: 'javascript-typescript/CLAUDE.md', destination: 'CLAUDE.md' },
19
20
  { source: 'javascript-typescript/.claude', destination: '.claude' },
21
+ { source: 'javascript-typescript/.claude/settings.json', destination: '.claude/settings.json' },
20
22
  { source: 'javascript-typescript/.mcp.json', destination: '.mcp.json' }
21
23
  ],
22
24
  frameworks: {
@@ -52,6 +54,7 @@ const TEMPLATES_CONFIG = {
52
54
  files: [
53
55
  { source: 'python/CLAUDE.md', destination: 'CLAUDE.md' },
54
56
  { source: 'python/.claude', destination: '.claude' },
57
+ { source: 'python/.claude/settings.json', destination: '.claude/settings.json' },
55
58
  { source: 'python/.mcp.json', destination: '.mcp.json' }
56
59
  ],
57
60
  frameworks: {
@@ -76,6 +79,31 @@ const TEMPLATES_CONFIG = {
76
79
  }
77
80
  }
78
81
  },
82
+ 'ruby': {
83
+ name: 'Ruby',
84
+ description: 'Optimized for Ruby development with modern tools',
85
+ files: [
86
+ { source: 'ruby/CLAUDE.md', destination: 'CLAUDE.md' },
87
+ { source: 'ruby/.claude', destination: '.claude' },
88
+ { source: 'ruby/.claude/settings.json', destination: '.claude/settings.json' },
89
+ { source: 'ruby/.mcp.json', destination: '.mcp.json' }
90
+ ],
91
+ frameworks: {
92
+ 'rails': {
93
+ name: 'Ruby on Rails 8',
94
+ additionalFiles: [
95
+ { source: 'ruby/examples/rails-app/.claude/commands', destination: '.claude/commands' },
96
+ { source: 'ruby/examples/rails-app/CLAUDE.md', destination: 'CLAUDE.md' }
97
+ ]
98
+ },
99
+ 'sinatra': {
100
+ name: 'Sinatra',
101
+ additionalFiles: [
102
+ { source: 'ruby/examples/sinatra-app/.claude/commands', destination: '.claude/commands' }
103
+ ]
104
+ }
105
+ }
106
+ },
79
107
  'rust': {
80
108
  name: 'Rust',
81
109
  description: 'Optimized for Rust development',
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ const ConsoleBridge = require('./console-bridge');
4
+ const chalk = require('chalk');
5
+
6
+ /**
7
+ * Test script for the Console Bridge
8
+ */
9
+ async function testConsoleBridge() {
10
+ console.log(chalk.blue('🧪 Testing Console Bridge Integration'));
11
+ console.log(chalk.gray('This will monitor running Claude Code processes and enable web-based interaction'));
12
+ console.log('');
13
+
14
+ // Create console bridge instance
15
+ const bridge = new ConsoleBridge({
16
+ port: 3334,
17
+ debug: true
18
+ });
19
+
20
+ try {
21
+ // Initialize the bridge
22
+ const success = await bridge.initialize();
23
+
24
+ if (!success) {
25
+ console.error(chalk.red('❌ Failed to initialize Console Bridge'));
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log('');
30
+ console.log(chalk.green('✅ Console Bridge is running!'));
31
+ console.log(chalk.cyan('📡 WebSocket server: ws://localhost:3334'));
32
+ console.log(chalk.cyan('🌐 Web interface should connect to this WebSocket'));
33
+ console.log('');
34
+ console.log(chalk.yellow('💡 Instructions:'));
35
+ console.log(' 1. Open your analytics dashboard (npm run analytics:start)');
36
+ console.log(' 2. Navigate to the Agents page');
37
+ console.log(' 3. The console interaction panel should appear when Claude Code prompts are detected');
38
+ console.log(' 4. Run Claude Code in another terminal to test interaction');
39
+ console.log('');
40
+ console.log(chalk.gray('Press Ctrl+C to stop the bridge'));
41
+
42
+ // Handle graceful shutdown
43
+ process.on('SIGINT', async () => {
44
+ console.log('');
45
+ console.log(chalk.yellow('🛑 Shutting down Console Bridge...'));
46
+ await bridge.shutdown();
47
+ process.exit(0);
48
+ });
49
+
50
+ process.on('SIGTERM', async () => {
51
+ await bridge.shutdown();
52
+ process.exit(0);
53
+ });
54
+
55
+ // Keep the process running
56
+ setInterval(() => {
57
+ // Heartbeat to keep process alive
58
+ }, 1000);
59
+
60
+ } catch (error) {
61
+ console.error(chalk.red('❌ Error running Console Bridge:'), error);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ // Run the test
67
+ testConsoleBridge();
package/src/utils.js CHANGED
@@ -60,6 +60,50 @@ async function detectProject(targetDir) {
60
60
  }
61
61
  }
62
62
 
63
+ // Check for Ruby files
64
+ const rubyFiles = await findFilesByExtension(targetDir, ['.rb']);
65
+ const gemfilePath = path.join(targetDir, 'Gemfile');
66
+ const gemfileLockPath = path.join(targetDir, 'Gemfile.lock');
67
+
68
+ if (rubyFiles.length > 0 || await fs.pathExists(gemfilePath)) {
69
+ detectedLanguages.push('ruby');
70
+
71
+ // Check for Ruby frameworks
72
+ if (await fs.pathExists(gemfilePath)) {
73
+ try {
74
+ const gemfile = await fs.readFile(gemfilePath, 'utf-8');
75
+ if (gemfile.includes('rails')) {
76
+ detectedFrameworks.push('rails');
77
+ }
78
+ if (gemfile.includes('sinatra')) {
79
+ detectedFrameworks.push('sinatra');
80
+ }
81
+ } catch (error) {
82
+ console.warn('Could not parse Gemfile');
83
+ }
84
+ }
85
+
86
+ // Check for Rails application structure
87
+ const railsAppPath = path.join(targetDir, 'config', 'application.rb');
88
+ const railsRoutesPath = path.join(targetDir, 'config', 'routes.rb');
89
+ if (await fs.pathExists(railsAppPath) || await fs.pathExists(railsRoutesPath)) {
90
+ detectedFrameworks.push('rails');
91
+ }
92
+
93
+ // Check for Rakefile (common in Rails and Ruby projects)
94
+ const rakefilePath = path.join(targetDir, 'Rakefile');
95
+ if (await fs.pathExists(rakefilePath)) {
96
+ try {
97
+ const rakefile = await fs.readFile(rakefilePath, 'utf-8');
98
+ if (rakefile.includes('Rails.application.load_tasks')) {
99
+ detectedFrameworks.push('rails');
100
+ }
101
+ } catch (error) {
102
+ // Ignore parsing errors
103
+ }
104
+ }
105
+ }
106
+
63
107
  // Check for Rust files
64
108
  const rustFiles = await findFilesByExtension(targetDir, ['.rs']);
65
109
  const cargoPath = path.join(targetDir, 'Cargo.toml');
@@ -145,6 +189,7 @@ async function getProjectSummary(targetDir) {
145
189
  hasGit: await fs.pathExists(path.join(targetDir, '.git')),
146
190
  hasNodeModules: await fs.pathExists(path.join(targetDir, 'node_modules')),
147
191
  hasVenv: await fs.pathExists(path.join(targetDir, 'venv')) || await fs.pathExists(path.join(targetDir, '.venv')),
192
+ hasBundle: await fs.pathExists(path.join(targetDir, 'vendor', 'bundle')),
148
193
  configFiles: []
149
194
  };
150
195
 
@@ -152,6 +197,7 @@ async function getProjectSummary(targetDir) {
152
197
  const configFiles = [
153
198
  'package.json', 'tsconfig.json', 'webpack.config.js', 'vite.config.js',
154
199
  'requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile',
200
+ 'Gemfile', 'Gemfile.lock', 'Rakefile', 'config.ru',
155
201
  'Cargo.toml', 'go.mod', '.gitignore', 'README.md'
156
202
  ];
157
203