claude-code-templates 1.14.0 → 1.14.2

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.
@@ -43,8 +43,9 @@ program
43
43
  .name('create-claude-config')
44
44
  .description('Setup Claude Code configurations for different programming languages')
45
45
  .version(require('../package.json').version)
46
- .option('-l, --language <language>', 'specify programming language')
47
- .option('-f, --framework <framework>', 'specify framework')
46
+ .option('-l, --language <language>', 'specify programming language (deprecated, use --template)')
47
+ .option('-f, --framework <framework>', 'specify framework (deprecated, use --template)')
48
+ .option('-t, --template <template>', 'specify template (e.g., common, javascript-typescript, python, ruby)')
48
49
  .option('-d, --directory <directory>', 'target directory (default: current directory)')
49
50
  .option('-y, --yes', 'skip prompts and use defaults')
50
51
  .option('--dry-run', 'show what would be copied without actually copying')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,6 +4,152 @@ const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
5
  const { getHooksForLanguage, filterHooksBySelection, getMCPsForLanguage, filterMCPsBySelection } = require('./hook-scanner');
6
6
 
7
+ // GitHub configuration for downloading templates
8
+ const GITHUB_CONFIG = {
9
+ owner: 'davila7',
10
+ repo: 'claude-code-templates',
11
+ branch: 'main',
12
+ templatesPath: 'cli-tool/templates'
13
+ };
14
+
15
+ // Cache for downloaded files to avoid repeated downloads
16
+ const downloadCache = new Map();
17
+
18
+ async function downloadFileFromGitHub(filePath) {
19
+ // Check cache first
20
+ if (downloadCache.has(filePath)) {
21
+ return downloadCache.get(filePath);
22
+ }
23
+
24
+ const githubUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}/${filePath}`;
25
+
26
+ try {
27
+ const response = await fetch(githubUrl);
28
+ if (!response.ok) {
29
+ throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
30
+ }
31
+
32
+ const content = await response.text();
33
+ downloadCache.set(filePath, content);
34
+ return content;
35
+ } catch (error) {
36
+ console.error(chalk.red(`❌ Error downloading ${filePath} from GitHub:`), error.message);
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async function downloadDirectoryFromGitHub(dirPath) {
42
+ // For directories, we need to get the list of files first
43
+ // GitHub API endpoint to get directory contents
44
+ const apiUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${GITHUB_CONFIG.templatesPath}/${dirPath}?ref=${GITHUB_CONFIG.branch}`;
45
+
46
+ try {
47
+ const response = await fetch(apiUrl);
48
+ if (!response.ok) {
49
+ throw new Error(`Failed to get directory listing for ${dirPath}: ${response.status} ${response.statusText}`);
50
+ }
51
+
52
+ const items = await response.json();
53
+ const files = {};
54
+
55
+ for (const item of items) {
56
+ if (item.type === 'file') {
57
+ const relativePath = path.relative(GITHUB_CONFIG.templatesPath, item.path);
58
+ const content = await downloadFileFromGitHub(relativePath);
59
+ files[item.name] = content;
60
+ }
61
+ }
62
+
63
+ return files;
64
+ } catch (error) {
65
+ console.error(chalk.red(`❌ Error downloading directory ${dirPath} from GitHub:`), error.message);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ // Helper functions for processing downloaded content
71
+ async function processSettingsFileFromContent(settingsContent, destPath, templateConfig) {
72
+ const settings = JSON.parse(settingsContent);
73
+
74
+ // Filter hooks based on selection
75
+ if (templateConfig.selectedHooks && settings.hooks) {
76
+ settings.hooks = filterHooksBySelection(settings.hooks, templateConfig.selectedHooks);
77
+ }
78
+
79
+ const destDir = path.dirname(destPath);
80
+ await fs.ensureDir(destDir);
81
+ await fs.writeJson(destPath, settings, { spaces: 2 });
82
+ }
83
+
84
+ async function mergeSettingsFileFromContent(settingsContent, destPath, templateConfig) {
85
+ const newSettings = JSON.parse(settingsContent);
86
+ let existingSettings = {};
87
+
88
+ if (await fs.pathExists(destPath)) {
89
+ existingSettings = await fs.readJson(destPath);
90
+ }
91
+
92
+ // Filter hooks based on selection
93
+ if (templateConfig.selectedHooks && newSettings.hooks) {
94
+ newSettings.hooks = filterHooksBySelection(newSettings.hooks, templateConfig.selectedHooks);
95
+ }
96
+
97
+ // Merge settings
98
+ const mergedSettings = {
99
+ ...existingSettings,
100
+ ...newSettings,
101
+ hooks: {
102
+ ...existingSettings.hooks,
103
+ ...newSettings.hooks
104
+ }
105
+ };
106
+
107
+ const destDir = path.dirname(destPath);
108
+ await fs.ensureDir(destDir);
109
+ await fs.writeJson(destPath, mergedSettings, { spaces: 2 });
110
+ }
111
+
112
+ async function processMCPFileFromContent(mcpContent, destPath, templateConfig) {
113
+ const mcpConfig = JSON.parse(mcpContent);
114
+
115
+ // Filter MCPs based on selection
116
+ if (templateConfig.selectedMCPs && mcpConfig.mcpServers) {
117
+ mcpConfig.mcpServers = filterMCPsBySelection(mcpConfig.mcpServers, templateConfig.selectedMCPs);
118
+ }
119
+
120
+ const destDir = path.dirname(destPath);
121
+ await fs.ensureDir(destDir);
122
+ await fs.writeJson(destPath, mcpConfig, { spaces: 2 });
123
+ }
124
+
125
+ async function mergeMCPFileFromContent(mcpContent, destPath, templateConfig) {
126
+ const newMcpConfig = JSON.parse(mcpContent);
127
+ let existingMcpConfig = {};
128
+
129
+ if (await fs.pathExists(destPath)) {
130
+ existingMcpConfig = await fs.readJson(destPath);
131
+ }
132
+
133
+ // Filter MCPs based on selection
134
+ if (templateConfig.selectedMCPs && newMcpConfig.mcpServers) {
135
+ newMcpConfig.mcpServers = filterMCPsBySelection(newMcpConfig.mcpServers, templateConfig.selectedMCPs);
136
+ }
137
+
138
+ // Merge MCP configurations
139
+ const mergedMcpConfig = {
140
+ ...existingMcpConfig,
141
+ ...newMcpConfig,
142
+ mcpServers: {
143
+ ...existingMcpConfig.mcpServers,
144
+ ...newMcpConfig.mcpServers
145
+ }
146
+ };
147
+
148
+ const destDir = path.dirname(destPath);
149
+ await fs.ensureDir(destDir);
150
+ await fs.writeJson(destPath, mergedMcpConfig, { spaces: 2 });
151
+ }
152
+
7
153
  async function checkExistingFiles(targetDir, templateConfig) {
8
154
  const existingFiles = [];
9
155
 
@@ -86,7 +232,7 @@ async function createBackups(existingFiles, targetDir) {
86
232
  }
87
233
 
88
234
  async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
89
- const templateDir = path.join(__dirname, '../templates');
235
+ console.log(chalk.gray(`📥 Downloading templates from GitHub (${GITHUB_CONFIG.branch} branch)...`));
90
236
 
91
237
  // Check for existing files and get user preference
92
238
  const existingFiles = await checkExistingFiles(targetDir, templateConfig);
@@ -114,7 +260,6 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
114
260
 
115
261
  // Copy base files and framework-specific files
116
262
  for (const file of templateConfig.files) {
117
- const sourcePath = path.join(templateDir, file.source);
118
263
  const destPath = path.join(targetDir, file.destination);
119
264
 
120
265
  try {
@@ -123,94 +268,115 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
123
268
  // This is a framework-specific commands directory - merge with existing commands
124
269
  await fs.ensureDir(destPath);
125
270
 
126
- // Copy framework-specific commands to the commands directory
127
- const frameworkFiles = await fs.readdir(sourcePath);
128
- for (const frameworkFile of frameworkFiles) {
129
- const srcFile = path.join(sourcePath, frameworkFile);
130
- const destFile = path.join(destPath, frameworkFile);
271
+ // Download framework-specific commands from GitHub
272
+ const frameworkFiles = await downloadDirectoryFromGitHub(file.source);
273
+ for (const [frameworkFileName, content] of Object.entries(frameworkFiles)) {
274
+ const destFile = path.join(destPath, frameworkFileName);
131
275
 
132
276
  // In merge mode, skip if file already exists
133
277
  if (userAction === 'merge' && await fs.pathExists(destFile)) {
134
- console.log(chalk.blue(`⏭️ Skipped ${frameworkFile} (already exists)`));
278
+ console.log(chalk.blue(`⏭️ Skipped ${frameworkFileName} (already exists)`));
135
279
  continue;
136
280
  }
137
281
 
138
- await fs.copy(srcFile, destFile, { overwrite: shouldOverwrite });
282
+ await fs.writeFile(destFile, content, 'utf8');
139
283
  }
140
284
 
141
- console.log(chalk.green(`✓ Copied framework commands ${file.source} → ${file.destination}`));
285
+ console.log(chalk.green(`✓ Downloaded framework commands ${file.source} → ${file.destination}`));
142
286
  } else if (file.source.includes('.claude') && !file.source.includes('examples/')) {
143
- // This is base .claude directory - copy it but handle commands specially
144
- await fs.copy(sourcePath, destPath, {
145
- overwrite: shouldOverwrite,
146
- filter: (src) => {
147
- // Skip the commands directory itself - we'll handle it separately
148
- return !src.endsWith('.claude/commands');
149
- }
150
- });
151
-
152
- // Now handle base commands specifically
153
- const baseCommandsPath = path.join(sourcePath, 'commands');
154
- const destCommandsPath = path.join(destPath, 'commands');
287
+ // This is base .claude directory - download it but handle commands specially
288
+ await fs.ensureDir(destPath);
155
289
 
156
- if (await fs.pathExists(baseCommandsPath)) {
157
- await fs.ensureDir(destCommandsPath);
158
-
159
- // Copy base commands, but exclude framework-specific ones that were moved
160
- const baseCommands = await fs.readdir(baseCommandsPath);
161
- const excludeCommands = ['react-component.md', 'route.md', 'api-endpoint.md']; // Commands moved to framework dirs
290
+ // Download base .claude directory structure from GitHub
291
+ try {
292
+ const baseClaudeFiles = await downloadDirectoryFromGitHub(file.source);
162
293
 
163
- for (const baseCommand of baseCommands) {
164
- if (!excludeCommands.includes(baseCommand)) {
165
- const srcFile = path.join(baseCommandsPath, baseCommand);
166
- const destFile = path.join(destCommandsPath, baseCommand);
294
+ // Write non-command files first
295
+ for (const [fileName, content] of Object.entries(baseClaudeFiles)) {
296
+ if (fileName !== 'commands') { // Skip commands directory, handle separately
297
+ const destFile = path.join(destPath, fileName);
167
298
 
168
299
  // In merge mode, skip if file already exists
169
300
  if (userAction === 'merge' && await fs.pathExists(destFile)) {
170
- console.log(chalk.blue(`⏭️ Skipped ${baseCommand} (already exists)`));
301
+ console.log(chalk.blue(`⏭️ Skipped ${fileName} (already exists)`));
171
302
  continue;
172
303
  }
173
304
 
174
- await fs.copy(srcFile, destFile, { overwrite: shouldOverwrite });
305
+ await fs.writeFile(destFile, content, 'utf8');
175
306
  }
176
307
  }
308
+
309
+ // Now handle base commands specifically
310
+ const destCommandsPath = path.join(destPath, 'commands');
311
+ await fs.ensureDir(destCommandsPath);
312
+
313
+ // Download base commands from GitHub
314
+ const baseCommandsDir = `${file.source}/commands`;
315
+ try {
316
+ const baseCommands = await downloadDirectoryFromGitHub(baseCommandsDir);
317
+ const excludeCommands = ['react-component.md', 'route.md', 'api-endpoint.md']; // Commands moved to framework dirs
318
+
319
+ for (const [baseCommandName, commandContent] of Object.entries(baseCommands)) {
320
+ if (!excludeCommands.includes(baseCommandName)) {
321
+ const destFile = path.join(destCommandsPath, baseCommandName);
322
+
323
+ // In merge mode, skip if file already exists
324
+ if (userAction === 'merge' && await fs.pathExists(destFile)) {
325
+ console.log(chalk.blue(`⏭️ Skipped ${baseCommandName} (already exists)`));
326
+ continue;
327
+ }
328
+
329
+ await fs.writeFile(destFile, commandContent, 'utf8');
330
+ }
331
+ }
332
+ } catch (error) {
333
+ // Commands directory might not exist for some templates, that's ok
334
+ console.log(chalk.yellow(`⚠️ No commands directory found for ${baseCommandsDir}`));
335
+ }
336
+
337
+ } catch (error) {
338
+ console.error(chalk.red(`❌ Error downloading .claude directory: ${error.message}`));
339
+ throw error;
177
340
  }
178
341
 
179
- console.log(chalk.green(`✓ Copied base configuration and commands ${file.source} → ${file.destination}`));
342
+ console.log(chalk.green(`✓ Downloaded base configuration and commands ${file.source} → ${file.destination}`));
180
343
  } else if (file.source.includes('settings.json') && templateConfig.selectedHooks) {
344
+ // Download and process settings.json with hooks
345
+ const settingsContent = await downloadFileFromGitHub(file.source);
346
+
181
347
  // In merge mode, merge settings instead of overwriting
182
348
  if (userAction === 'merge') {
183
- await mergeSettingsFile(sourcePath, destPath, templateConfig);
349
+ await mergeSettingsFileFromContent(settingsContent, destPath, templateConfig);
184
350
  console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected hooks)`));
185
351
  } else {
186
- await processSettingsFile(sourcePath, destPath, templateConfig);
187
- console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected hooks)`));
352
+ await processSettingsFileFromContent(settingsContent, destPath, templateConfig);
353
+ console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected hooks)`));
188
354
  }
189
355
  } else if (file.source.includes('.mcp.json') && templateConfig.selectedMCPs) {
356
+ // Download and process MCP config with selected MCPs
357
+ const mcpContent = await downloadFileFromGitHub(file.source);
358
+
190
359
  // In merge mode, merge MCP config instead of overwriting
191
360
  if (userAction === 'merge') {
192
- await mergeMCPFile(sourcePath, destPath, templateConfig);
361
+ await mergeMCPFileFromContent(mcpContent, destPath, templateConfig);
193
362
  console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected MCPs)`));
194
363
  } else {
195
- await processMCPFile(sourcePath, destPath, templateConfig);
196
- console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination} (with selected MCPs)`));
364
+ await processMCPFileFromContent(mcpContent, destPath, templateConfig);
365
+ console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected MCPs)`));
197
366
  }
198
367
  } else {
199
- // Copy regular files (CLAUDE.md, etc.)
368
+ // Download regular files (CLAUDE.md, etc.)
200
369
  // In merge mode, skip if file already exists
201
370
  if (userAction === 'merge' && await fs.pathExists(destPath)) {
202
371
  console.log(chalk.blue(`⏭️ Skipped ${file.destination} (already exists)`));
203
372
  continue;
204
373
  }
205
374
 
206
- await fs.copy(sourcePath, destPath, {
207
- overwrite: shouldOverwrite,
208
- filter: (src) => {
209
- // Skip commands directory during regular copy - we handle them above
210
- return !src.includes('.claude/commands');
211
- }
212
- });
213
- console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination}`));
375
+ const fileContent = await downloadFileFromGitHub(file.source);
376
+ const destDir = path.dirname(destPath);
377
+ await fs.ensureDir(destDir);
378
+ await fs.writeFile(destPath, fileContent, 'utf8');
379
+ console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination}`));
214
380
  }
215
381
  } catch (error) {
216
382
  console.error(chalk.red(`✗ Failed to copy ${file.source}:`), error.message);
@@ -218,38 +384,8 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
218
384
  }
219
385
  }
220
386
 
387
+ console.log(chalk.cyan(`📦 All templates downloaded from: https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tree/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}`));
221
388
  return true; // Indicate successful completion
222
-
223
- // Copy selected commands individually
224
- if (templateConfig.selectedCommands && templateConfig.selectedCommands.length > 0) {
225
- const commandsDir = path.join(targetDir, '.claude', 'commands');
226
- await fs.ensureDir(commandsDir);
227
-
228
- for (const command of templateConfig.selectedCommands) {
229
- try {
230
- const commandFileName = `${command.name}.md`;
231
- const destPath = path.join(commandsDir, commandFileName);
232
-
233
- await fs.copy(command.filePath, destPath);
234
- console.log(chalk.green(`✓ Added command: ${command.displayName}`));
235
- } catch (error) {
236
- console.error(chalk.red(`✗ Failed to copy command ${command.name}:`), error.message);
237
- // Don't throw - continue with other commands
238
- }
239
- }
240
-
241
- console.log(chalk.cyan(`📋 Installed ${templateConfig.selectedCommands.length} commands`));
242
- }
243
-
244
- // Report hook selection
245
- if (templateConfig.selectedHooks && templateConfig.selectedHooks.length > 0) {
246
- console.log(chalk.magenta(`🔧 Installed ${templateConfig.selectedHooks.length} automation hooks`));
247
- }
248
-
249
- // Report MCP selection
250
- if (templateConfig.selectedMCPs && templateConfig.selectedMCPs.length > 0) {
251
- console.log(chalk.blue(`🔧 Installed ${templateConfig.selectedMCPs.length} MCP`));
252
- }
253
389
  }
254
390
 
255
391
  async function runPostInstallationValidation(targetDir, templateConfig) {
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ const { runHookStats } = require('./hook-stats');
14
14
  const { runMCPStats } = require('./mcp-stats');
15
15
  const { runAnalytics } = require('./analytics');
16
16
  const { runHealthCheck } = require('./health-check');
17
+ const { trackingService } = require('./tracking-service');
17
18
 
18
19
  async function showMainMenu() {
19
20
  console.log('');
@@ -49,12 +50,14 @@ async function showMainMenu() {
49
50
 
50
51
  if (initialChoice.action === 'analytics') {
51
52
  console.log(chalk.blue('📊 Launching Claude Code Analytics Dashboard...'));
53
+ trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'interactive_menu' });
52
54
  await runAnalytics({});
53
55
  return;
54
56
  }
55
57
 
56
58
  if (initialChoice.action === 'chats') {
57
59
  console.log(chalk.blue('💬 Launching Claude Code Chats Dashboard...'));
60
+ trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'interactive_menu' });
58
61
  await runAnalytics({ openTo: 'agents' });
59
62
  return;
60
63
  }
@@ -62,6 +65,13 @@ async function showMainMenu() {
62
65
  if (initialChoice.action === 'health') {
63
66
  console.log(chalk.blue('🔍 Running Health Check...'));
64
67
  const healthResult = await runHealthCheck();
68
+
69
+ // Track health check usage
70
+ trackingService.trackHealthCheck({
71
+ setup_recommended: healthResult.runSetup,
72
+ issues_found: healthResult.issues || 0
73
+ });
74
+
65
75
  if (healthResult.runSetup) {
66
76
  console.log(chalk.blue('⚙️ Starting Project Setup...'));
67
77
  // Continue with setup flow
@@ -116,12 +126,14 @@ async function createClaudeConfig(options = {}) {
116
126
 
117
127
  // Handle analytics dashboard
118
128
  if (options.analytics) {
129
+ trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'command_line' });
119
130
  await runAnalytics(options);
120
131
  return;
121
132
  }
122
133
 
123
134
  // Handle chats/agents dashboard
124
135
  if (options.chats || options.agents) {
136
+ trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'command_line' });
125
137
  await runAnalytics({ ...options, openTo: 'agents' });
126
138
  return;
127
139
  }
@@ -130,6 +142,14 @@ async function createClaudeConfig(options = {}) {
130
142
  let shouldRunSetup = false;
131
143
  if (options.healthCheck || options.health || options.check || options.verify) {
132
144
  const healthResult = await runHealthCheck();
145
+
146
+ // Track health check usage
147
+ trackingService.trackHealthCheck({
148
+ setup_recommended: healthResult.runSetup,
149
+ issues_found: healthResult.issues || 0,
150
+ source: 'command_line'
151
+ });
152
+
133
153
  if (healthResult.runSetup) {
134
154
  console.log(chalk.blue('⚙️ Starting Project Setup...'));
135
155
  shouldRunSetup = true;
@@ -155,8 +175,8 @@ async function createClaudeConfig(options = {}) {
155
175
 
156
176
  let config;
157
177
  if (options.yes) {
158
- // Use defaults
159
- const selectedLanguage = options.language || projectInfo.detectedLanguage || 'common';
178
+ // Use defaults - prioritize --template over --language for backward compatibility
179
+ const selectedLanguage = options.template || options.language || projectInfo.detectedLanguage || 'common';
160
180
 
161
181
  // Check if selected language is coming soon
162
182
  if (selectedLanguage && TEMPLATES_CONFIG[selectedLanguage] && TEMPLATES_CONFIG[selectedLanguage].comingSoon) {
@@ -244,8 +264,8 @@ async function createClaudeConfig(options = {}) {
244
264
  console.log(chalk.white(' 2. Customize the configuration for your project'));
245
265
  console.log(chalk.white(' 3. Start using Claude Code with: claude'));
246
266
  console.log('');
247
- console.log(chalk.blue('🌐 View all available templates at: https://davila7.github.io/claude-code-templates/'));
248
- console.log(chalk.blue('📖 Read the complete documentation at: https://davila7.github.io/claude-code-templates/docu/'));
267
+ console.log(chalk.blue('🌐 View all available templates at: https://aitmpl.com/'));
268
+ console.log(chalk.blue('📖 Read the complete documentation at: https://aitmpl.com/docu/'));
249
269
 
250
270
  if (config.language !== 'common') {
251
271
  console.log(chalk.yellow(`💡 Language-specific features for ${config.language} have been configured`));
@@ -262,6 +282,17 @@ async function createClaudeConfig(options = {}) {
262
282
  if (config.mcps && config.mcps.length > 0) {
263
283
  console.log(chalk.blue(`🔧 ${config.mcps.length} MCP servers have been configured`));
264
284
  }
285
+
286
+ // Track successful template installation
287
+ if (!options.agent && !options.command && !options.mcp) {
288
+ trackingService.trackTemplateInstallation(config.language, config.framework, {
289
+ installation_method: options.setupFromMenu ? 'interactive_menu' : 'command_line',
290
+ dry_run: options.dryRun || false,
291
+ hooks_count: config.hooks ? config.hooks.length : 0,
292
+ mcps_count: config.mcps ? config.mcps.length : 0,
293
+ project_detected: !!options.detectedProject
294
+ });
295
+ }
265
296
 
266
297
  // Run post-installation validation
267
298
  if (!options.dryRun) {
@@ -274,46 +305,40 @@ async function installIndividualAgent(agentName, targetDir, options) {
274
305
  console.log(chalk.blue(`🤖 Installing agent: ${agentName}`));
275
306
 
276
307
  try {
277
- // Check if components directory exists
278
- const componentsPath = path.join(__dirname, '..', 'components', 'agents');
279
- const agentFile = path.join(componentsPath, `${agentName}.md`);
308
+ // Download agent directly from GitHub
309
+ const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`;
310
+ console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
280
311
 
281
- if (!await fs.pathExists(agentFile)) {
282
- console.log(chalk.red(`❌ Agent "${agentName}" not found`));
283
- console.log(chalk.yellow('Available agents:'));
284
-
285
- // List available agents
286
- if (await fs.pathExists(componentsPath)) {
287
- const agents = await fs.readdir(componentsPath);
288
- agents.filter(f => f.endsWith('.md')).forEach(agent => {
289
- console.log(chalk.cyan(` - ${agent.replace('.md', '')}`));
290
- });
312
+ const response = await fetch(githubUrl);
313
+ if (!response.ok) {
314
+ if (response.status === 404) {
315
+ console.log(chalk.red(`❌ Agent "${agentName}" not found`));
316
+ console.log(chalk.yellow('Available agents: api-security-audit, database-optimization, react-performance-optimization'));
317
+ return;
291
318
  }
292
- return;
319
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
293
320
  }
294
321
 
295
- // For agents, they are typically part of templates, so we need to determine
296
- // the appropriate language/framework and install the complete template
297
- const agentContent = await fs.readFile(agentFile, 'utf8');
298
- const language = extractLanguageFromAgent(agentContent, agentName);
299
- const framework = extractFrameworkFromAgent(agentContent, agentName);
322
+ const agentContent = await response.text();
300
323
 
301
- console.log(chalk.yellow(`📝 Agent "${agentName}" is part of ${language}/${framework} template`));
302
- console.log(chalk.blue('🚀 Installing complete template with this agent...'));
324
+ // Create .claude/agents directory if it doesn't exist
325
+ const agentsDir = path.join(targetDir, '.claude', 'agents');
326
+ await fs.ensureDir(agentsDir);
303
327
 
304
- // Install the template that contains this agent (avoid recursion)
305
- const setupOptions = {
306
- ...options,
307
- language,
308
- framework,
309
- yes: true,
310
- targetDirectory: targetDir,
311
- agent: null // Remove agent to avoid recursion
312
- };
313
- delete setupOptions.agent;
314
- await createClaudeConfig(setupOptions);
328
+ // Write the agent file
329
+ const targetFile = path.join(agentsDir, `${agentName}.md`);
330
+ await fs.writeFile(targetFile, agentContent, 'utf8');
315
331
 
316
332
  console.log(chalk.green(`✅ Agent "${agentName}" installed successfully!`));
333
+ console.log(chalk.cyan(`📁 Installed to: ${path.relative(targetDir, targetFile)}`));
334
+ console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
335
+
336
+ // Track successful agent installation
337
+ trackingService.trackDownload('agent', agentName, {
338
+ installation_type: 'individual_component',
339
+ target_directory: path.relative(process.cwd(), targetDir),
340
+ source: 'github_main'
341
+ });
317
342
 
318
343
  } catch (error) {
319
344
  console.log(chalk.red(`❌ Error installing agent: ${error.message}`));
@@ -324,34 +349,40 @@ async function installIndividualCommand(commandName, targetDir, options) {
324
349
  console.log(chalk.blue(`⚡ Installing command: ${commandName}`));
325
350
 
326
351
  try {
327
- // Check if components directory exists
328
- const componentsPath = path.join(__dirname, '..', 'components', 'commands');
329
- const commandFile = path.join(componentsPath, `${commandName}.md`);
352
+ // Download command directly from GitHub
353
+ const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/commands/${commandName}.md`;
354
+ console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
330
355
 
331
- if (!await fs.pathExists(commandFile)) {
332
- console.log(chalk.red(`❌ Command "${commandName}" not found`));
333
- console.log(chalk.yellow('Available commands:'));
334
-
335
- // List available commands
336
- if (await fs.pathExists(componentsPath)) {
337
- const commands = await fs.readdir(componentsPath);
338
- commands.filter(f => f.endsWith('.md')).forEach(command => {
339
- console.log(chalk.cyan(` - ${command.replace('.md', '')}`));
340
- });
356
+ const response = await fetch(githubUrl);
357
+ if (!response.ok) {
358
+ if (response.status === 404) {
359
+ console.log(chalk.red(`❌ Command "${commandName}" not found`));
360
+ console.log(chalk.yellow('Available commands: check-file, generate-tests'));
361
+ return;
341
362
  }
342
- return;
363
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
343
364
  }
344
365
 
366
+ const commandContent = await response.text();
367
+
345
368
  // Create .claude/commands directory if it doesn't exist
346
369
  const commandsDir = path.join(targetDir, '.claude', 'commands');
347
370
  await fs.ensureDir(commandsDir);
348
371
 
349
- // Copy the command file
372
+ // Write the command file
350
373
  const targetFile = path.join(commandsDir, `${commandName}.md`);
351
- await fs.copy(commandFile, targetFile);
374
+ await fs.writeFile(targetFile, commandContent, 'utf8');
352
375
 
353
376
  console.log(chalk.green(`✅ Command "${commandName}" installed successfully!`));
354
377
  console.log(chalk.cyan(`📁 Installed to: ${path.relative(targetDir, targetFile)}`));
378
+ console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
379
+
380
+ // Track successful command installation
381
+ trackingService.trackDownload('command', commandName, {
382
+ installation_type: 'individual_command',
383
+ target_directory: path.relative(process.cwd(), targetDir),
384
+ source: 'github_main'
385
+ });
355
386
 
356
387
  } catch (error) {
357
388
  console.log(chalk.red(`❌ Error installing command: ${error.message}`));
@@ -362,26 +393,22 @@ async function installIndividualMCP(mcpName, targetDir, options) {
362
393
  console.log(chalk.blue(`🔌 Installing MCP: ${mcpName}`));
363
394
 
364
395
  try {
365
- // Check if components directory exists
366
- const componentsPath = path.join(__dirname, '..', 'components', 'mcps');
367
- const mcpFile = path.join(componentsPath, `${mcpName}.json`);
396
+ // Download MCP directly from GitHub
397
+ const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/mcps/${mcpName}.json`;
398
+ console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
368
399
 
369
- if (!await fs.pathExists(mcpFile)) {
370
- console.log(chalk.red(`❌ MCP "${mcpName}" not found`));
371
- console.log(chalk.yellow('Available MCPs:'));
372
-
373
- // List available MCPs
374
- if (await fs.pathExists(componentsPath)) {
375
- const mcps = await fs.readdir(componentsPath);
376
- mcps.filter(f => f.endsWith('.json')).forEach(mcp => {
377
- console.log(chalk.cyan(` - ${mcp.replace('.json', '')}`));
378
- });
400
+ const response = await fetch(githubUrl);
401
+ if (!response.ok) {
402
+ if (response.status === 404) {
403
+ console.log(chalk.red(`❌ MCP "${mcpName}" not found`));
404
+ console.log(chalk.yellow('Available MCPs: web-fetch, filesystem-access, github-integration, memory-integration, mysql-integration, postgresql-integration, deepgraph-react, deepgraph-nextjs, deepgraph-typescript, deepgraph-vue'));
405
+ return;
379
406
  }
380
- return;
407
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
381
408
  }
382
409
 
383
- // Read the MCP configuration
384
- const mcpConfig = await fs.readJson(mcpFile);
410
+ const mcpConfigText = await response.text();
411
+ const mcpConfig = JSON.parse(mcpConfigText);
385
412
 
386
413
  // Check if .mcp.json exists in target directory
387
414
  const targetMcpFile = path.join(targetDir, '.mcp.json');
@@ -403,6 +430,15 @@ async function installIndividualMCP(mcpName, targetDir, options) {
403
430
 
404
431
  console.log(chalk.green(`✅ MCP "${mcpName}" installed successfully!`));
405
432
  console.log(chalk.cyan(`📁 Configuration merged into: ${path.relative(targetDir, targetMcpFile)}`));
433
+ console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
434
+
435
+ // Track successful MCP installation
436
+ trackingService.trackDownload('mcp', mcpName, {
437
+ installation_type: 'individual_mcp',
438
+ merged_with_existing: existingConfig !== null,
439
+ servers_count: Object.keys(mergedConfig.mcpServers || {}).length,
440
+ source: 'github_main'
441
+ });
406
442
 
407
443
  } catch (error) {
408
444
  console.log(chalk.red(`❌ Error installing MCP: ${error.message}`));
@@ -0,0 +1,188 @@
1
+ /**
2
+ * TrackingService - Download analytics using GitHub Issues as backend
3
+ * Records component installations for analytics without impacting user experience
4
+ */
5
+
6
+ class TrackingService {
7
+ constructor() {
8
+ this.repoOwner = 'davila7';
9
+ this.repoName = 'claude-code-templates';
10
+ this.trackingEnabled = this.shouldEnableTracking();
11
+ this.timeout = 5000; // 5s timeout for tracking requests
12
+ }
13
+
14
+ /**
15
+ * Check if tracking should be enabled (respects user privacy)
16
+ */
17
+ shouldEnableTracking() {
18
+ // Allow users to opt-out
19
+ if (process.env.CCT_NO_TRACKING === 'true' ||
20
+ process.env.CCT_NO_ANALYTICS === 'true' ||
21
+ process.env.CI === 'true') {
22
+ return false;
23
+ }
24
+
25
+ // Enable by default (anonymous usage data only)
26
+ return true;
27
+ }
28
+
29
+ /**
30
+ * Track a component download/installation
31
+ * @param {string} componentType - 'agent', 'command', or 'mcp'
32
+ * @param {string} componentName - Name of the component
33
+ * @param {object} metadata - Additional context (optional)
34
+ */
35
+ async trackDownload(componentType, componentName, metadata = {}) {
36
+ if (!this.trackingEnabled) {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ // Create tracking payload
42
+ const trackingData = this.createTrackingPayload(componentType, componentName, metadata);
43
+
44
+ // Fire-and-forget tracking (don't block user experience)
45
+ this.sendTrackingData(trackingData)
46
+ .catch(error => {
47
+ // Silent failure - tracking should never impact functionality
48
+ console.debug('📊 Tracking info (non-critical):', error.message);
49
+ });
50
+
51
+ } catch (error) {
52
+ // Silently handle any tracking errors
53
+ console.debug('📊 Analytics error (non-critical):', error.message);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Create standardized tracking payload
59
+ */
60
+ createTrackingPayload(componentType, componentName, metadata) {
61
+ const timestamp = new Date().toISOString();
62
+
63
+ return {
64
+ event: 'component_download',
65
+ component_type: componentType,
66
+ component_name: componentName,
67
+ timestamp: timestamp,
68
+ session_id: this.generateSessionId(),
69
+ environment: {
70
+ node_version: process.version,
71
+ platform: process.platform,
72
+ arch: process.arch,
73
+ cli_version: this.getCliVersion()
74
+ },
75
+ metadata: metadata
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Send tracking data to GitHub Issues (async, non-blocking)
81
+ */
82
+ async sendTrackingData(trackingData) {
83
+ const title = `📊 ${trackingData.component_type}:${trackingData.component_name} - ${trackingData.timestamp.split('T')[0]}`;
84
+
85
+ const body = `\`\`\`json
86
+ ${JSON.stringify(trackingData, null, 2)}
87
+ \`\`\`
88
+
89
+ <!-- ANALYTICS_DATA -->
90
+ Component: **${trackingData.component_name}** (${trackingData.component_type})
91
+ Platform: ${trackingData.environment.platform} ${trackingData.environment.arch}
92
+ Node: ${trackingData.environment.node_version}
93
+ CLI: ${trackingData.environment.cli_version}
94
+ Session: \`${trackingData.session_id}\`
95
+ `;
96
+
97
+ const controller = new AbortController();
98
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
99
+
100
+ try {
101
+ const response = await fetch(`https://api.github.com/repos/${this.repoOwner}/${this.repoName}/issues`, {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'User-Agent': 'claude-code-templates-cli'
106
+ },
107
+ body: JSON.stringify({
108
+ title: title,
109
+ body: body,
110
+ labels: ['📊 analytics', 'download-tracking', `type:${trackingData.component_type}`]
111
+ }),
112
+ signal: controller.signal
113
+ });
114
+
115
+ clearTimeout(timeoutId);
116
+
117
+ if (!response.ok) {
118
+ throw new Error(`GitHub API responded with ${response.status}`);
119
+ }
120
+
121
+ console.debug('📊 Download tracked successfully');
122
+
123
+ } catch (error) {
124
+ clearTimeout(timeoutId);
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Generate a session ID for grouping related downloads
131
+ */
132
+ generateSessionId() {
133
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
134
+ }
135
+
136
+ /**
137
+ * Get CLI version from package.json
138
+ */
139
+ getCliVersion() {
140
+ try {
141
+ const path = require('path');
142
+ const fs = require('fs');
143
+ const packagePath = path.join(__dirname, '..', 'package.json');
144
+ const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
145
+ return packageData.version || 'unknown';
146
+ } catch (error) {
147
+ return 'unknown';
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Track template installation (full project setup)
153
+ */
154
+ async trackTemplateInstallation(language, framework, metadata = {}) {
155
+ return this.trackDownload('template', `${language}/${framework}`, {
156
+ ...metadata,
157
+ installation_type: 'full_template'
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Track health check usage
163
+ */
164
+ async trackHealthCheck(results = {}) {
165
+ return this.trackDownload('health-check', 'system-validation', {
166
+ installation_type: 'health_check',
167
+ results_summary: results
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Track analytics dashboard usage
173
+ */
174
+ async trackAnalyticsDashboard(metadata = {}) {
175
+ return this.trackDownload('analytics', 'dashboard-launch', {
176
+ installation_type: 'analytics_dashboard',
177
+ ...metadata
178
+ });
179
+ }
180
+ }
181
+
182
+ // Export singleton instance
183
+ const trackingService = new TrackingService();
184
+
185
+ module.exports = {
186
+ TrackingService,
187
+ trackingService
188
+ };