claude-code-templates 1.15.0 → 1.15.1
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/README.md +7 -7
- package/bin/create-claude-config.js +15 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +422 -83
- package/src/health-check.js +310 -0
- package/src/index.js +944 -56
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- package/templates/rust/README.md +0 -26
package/src/file-operations.js
CHANGED
|
@@ -4,6 +4,285 @@ 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, retryCount = 0) {
|
|
19
|
+
// Check cache first
|
|
20
|
+
if (downloadCache.has(filePath)) {
|
|
21
|
+
return downloadCache.get(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const maxRetries = 3;
|
|
25
|
+
const baseDelay = 1000;
|
|
26
|
+
const retryDelay = baseDelay * Math.pow(2, retryCount); // Exponential backoff: 1s, 2s, 4s
|
|
27
|
+
const githubUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}/${filePath}`;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(githubUrl);
|
|
31
|
+
|
|
32
|
+
// Handle rate limiting for raw.githubusercontent.com (though less common)
|
|
33
|
+
if (response.status === 403 && retryCount < maxRetries) {
|
|
34
|
+
const rateLimitMsg = response.statusText.toLowerCase();
|
|
35
|
+
if (rateLimitMsg.includes('rate limit') || rateLimitMsg.includes('forbidden')) {
|
|
36
|
+
console.log(chalk.yellow(`⏳ Rate limited downloading ${filePath}, retrying in ${Math.ceil(retryDelay / 1000)}s...`));
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
38
|
+
return downloadFileFromGitHub(filePath, retryCount + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
// For 404s, just throw - these are legitimate missing files
|
|
44
|
+
if (response.status === 404) {
|
|
45
|
+
throw new Error(`File not found: ${filePath} (404)`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// For other errors, retry if possible
|
|
49
|
+
if (retryCount < maxRetries) {
|
|
50
|
+
console.log(chalk.yellow(`⚠️ Error ${response.status} downloading ${filePath}, retrying...`));
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
52
|
+
return downloadFileFromGitHub(filePath, retryCount + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const content = await response.text();
|
|
59
|
+
downloadCache.set(filePath, content);
|
|
60
|
+
return content;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Network errors - retry if possible
|
|
63
|
+
if (retryCount < maxRetries && (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.message.includes('fetch'))) {
|
|
64
|
+
console.log(chalk.yellow(`⚠️ Network error downloading ${filePath}, retrying in ${Math.ceil(retryDelay / 1000)}s...`));
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
66
|
+
return downloadFileFromGitHub(filePath, retryCount + 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Don't log error here - let caller handle it
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function downloadDirectoryFromGitHub(dirPath, retryCount = 0) {
|
|
75
|
+
const maxRetries = 5; // Increased retry attempts
|
|
76
|
+
const baseDelay = 2000; // Base delay of 2 seconds
|
|
77
|
+
const retryDelay = baseDelay * Math.pow(2, retryCount); // Exponential backoff: 2s, 4s, 8s, 16s, 32s
|
|
78
|
+
|
|
79
|
+
// For directories, we need to get the list of files first
|
|
80
|
+
// GitHub API endpoint to get directory contents
|
|
81
|
+
const apiUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${GITHUB_CONFIG.templatesPath}/${dirPath}?ref=${GITHUB_CONFIG.branch}`;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(apiUrl);
|
|
85
|
+
|
|
86
|
+
// Handle rate limiting with more sophisticated detection
|
|
87
|
+
if (response.status === 403) {
|
|
88
|
+
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
89
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
90
|
+
const isRateLimit = rateLimitRemaining === '0' || response.statusText.toLowerCase().includes('rate limit');
|
|
91
|
+
|
|
92
|
+
if (isRateLimit && retryCount < maxRetries) {
|
|
93
|
+
let waitTime = retryDelay;
|
|
94
|
+
|
|
95
|
+
// If we have reset time, calculate exact wait time
|
|
96
|
+
if (rateLimitReset) {
|
|
97
|
+
const resetTime = parseInt(rateLimitReset) * 1000;
|
|
98
|
+
const currentTime = Date.now();
|
|
99
|
+
const exactWaitTime = Math.max(resetTime - currentTime + 1000, retryDelay); // Add 1s buffer
|
|
100
|
+
waitTime = Math.min(exactWaitTime, 60000); // Cap at 60 seconds
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(chalk.yellow(`⏳ GitHub API rate limit exceeded for ${dirPath}`));
|
|
104
|
+
console.log(chalk.yellow(` Waiting ${Math.ceil(waitTime / 1000)}s before retry ${retryCount + 1}/${maxRetries}...`));
|
|
105
|
+
console.log(chalk.gray(` Rate limit resets at: ${rateLimitReset ? new Date(parseInt(rateLimitReset) * 1000).toLocaleTimeString() : 'unknown'}`));
|
|
106
|
+
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
108
|
+
return downloadDirectoryFromGitHub(dirPath, retryCount + 1);
|
|
109
|
+
} else if (isRateLimit) {
|
|
110
|
+
console.log(chalk.red(`❌ GitHub API rate limit exceeded after ${maxRetries} retries`));
|
|
111
|
+
console.log(chalk.yellow(` Directory ${dirPath} will be skipped (some template files may be missing)`));
|
|
112
|
+
return {}; // Return empty object instead of throwing error
|
|
113
|
+
} else {
|
|
114
|
+
// Different 403 error (permissions, etc.)
|
|
115
|
+
console.log(chalk.yellow(`⚠️ Access denied for ${dirPath} (403). This may be normal for some templates.`));
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
// If it's a 404, the directory doesn't exist - that's ok for some templates
|
|
122
|
+
if (response.status === 404) {
|
|
123
|
+
console.log(chalk.yellow(`⚠️ Directory ${dirPath} not found (this is normal for some templates)`));
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// For other errors, retry if we haven't exceeded max retries
|
|
128
|
+
if (retryCount < maxRetries) {
|
|
129
|
+
console.log(chalk.yellow(`⚠️ Error ${response.status} for ${dirPath}, retrying in ${Math.ceil(retryDelay / 1000)}s...`));
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
131
|
+
return downloadDirectoryFromGitHub(dirPath, retryCount + 1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new Error(`Failed to get directory listing for ${dirPath}: ${response.status} ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const items = await response.json();
|
|
138
|
+
const files = {};
|
|
139
|
+
let successCount = 0;
|
|
140
|
+
let skipCount = 0;
|
|
141
|
+
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
if (item.type === 'file') {
|
|
144
|
+
const relativePath = path.relative(GITHUB_CONFIG.templatesPath, item.path);
|
|
145
|
+
try {
|
|
146
|
+
const content = await downloadFileFromGitHub(relativePath);
|
|
147
|
+
files[item.name] = content;
|
|
148
|
+
successCount++;
|
|
149
|
+
} catch (fileError) {
|
|
150
|
+
skipCount++;
|
|
151
|
+
if (fileError.message.includes('rate limit') || fileError.message.includes('403')) {
|
|
152
|
+
console.log(chalk.yellow(`⚠️ Rate limited while downloading ${item.name}, skipping...`));
|
|
153
|
+
} else {
|
|
154
|
+
console.log(chalk.yellow(`⚠️ Skipped ${item.name}: ${fileError.message}`));
|
|
155
|
+
}
|
|
156
|
+
// Continue with other files instead of failing completely
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (successCount > 0) {
|
|
162
|
+
console.log(chalk.green(`✓ Downloaded ${successCount} files from ${dirPath}${skipCount > 0 ? ` (${skipCount} skipped)` : ''}`));
|
|
163
|
+
} else if (skipCount > 0) {
|
|
164
|
+
console.log(chalk.yellow(`⚠️ All ${skipCount} files in ${dirPath} were skipped due to errors`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return files;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (retryCount < maxRetries && (error.message.includes('rate limit') || error.message.includes('ECONNRESET'))) {
|
|
170
|
+
console.log(chalk.yellow(`⚠️ Network error for ${dirPath}, retrying in ${Math.ceil(retryDelay / 1000)}s...`));
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
172
|
+
return downloadDirectoryFromGitHub(dirPath, retryCount + 1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.error(chalk.red(`❌ Error downloading directory ${dirPath} from GitHub:`), error.message);
|
|
176
|
+
console.log(chalk.yellow(` Continuing with available files (some template files may be missing)`));
|
|
177
|
+
return {}; // Return empty object to continue with other parts of the template
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Helper functions for processing downloaded content
|
|
182
|
+
async function processSettingsFileFromContent(settingsContent, destPath, templateConfig) {
|
|
183
|
+
const settings = JSON.parse(settingsContent);
|
|
184
|
+
|
|
185
|
+
// Filter hooks based on selection
|
|
186
|
+
if (templateConfig.selectedHooks && settings.hooks) {
|
|
187
|
+
settings.hooks = filterHooksBySelection(settings.hooks, templateConfig.selectedHooks);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const destDir = path.dirname(destPath);
|
|
191
|
+
await fs.ensureDir(destDir);
|
|
192
|
+
await fs.writeJson(destPath, settings, { spaces: 2 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function mergeSettingsFileFromContent(settingsContent, destPath, templateConfig) {
|
|
196
|
+
const newSettings = JSON.parse(settingsContent);
|
|
197
|
+
let existingSettings = {};
|
|
198
|
+
|
|
199
|
+
if (await fs.pathExists(destPath)) {
|
|
200
|
+
existingSettings = await fs.readJson(destPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Filter hooks based on selection
|
|
204
|
+
if (templateConfig.selectedHooks && newSettings.hooks) {
|
|
205
|
+
newSettings.hooks = filterHooksBySelection(newSettings.hooks, templateConfig.selectedHooks);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Merge settings
|
|
209
|
+
const mergedSettings = {
|
|
210
|
+
...existingSettings,
|
|
211
|
+
...newSettings,
|
|
212
|
+
hooks: {
|
|
213
|
+
...existingSettings.hooks,
|
|
214
|
+
...newSettings.hooks
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const destDir = path.dirname(destPath);
|
|
219
|
+
await fs.ensureDir(destDir);
|
|
220
|
+
await fs.writeJson(destPath, mergedSettings, { spaces: 2 });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function processMCPFileFromContent(mcpContent, destPath, templateConfig) {
|
|
224
|
+
const mcpConfig = JSON.parse(mcpContent);
|
|
225
|
+
|
|
226
|
+
// Clean and prepare MCP config (only keep mcpServers without descriptions)
|
|
227
|
+
const cleanMcpConfig = { mcpServers: {} };
|
|
228
|
+
if (mcpConfig.mcpServers) {
|
|
229
|
+
for (const serverName in mcpConfig.mcpServers) {
|
|
230
|
+
if (mcpConfig.mcpServers[serverName] && typeof mcpConfig.mcpServers[serverName] === 'object') {
|
|
231
|
+
const serverConfig = { ...mcpConfig.mcpServers[serverName] };
|
|
232
|
+
delete serverConfig.description; // Remove description field
|
|
233
|
+
cleanMcpConfig.mcpServers[serverName] = serverConfig;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Filter MCPs based on selection
|
|
239
|
+
if (templateConfig.selectedMCPs && cleanMcpConfig.mcpServers) {
|
|
240
|
+
cleanMcpConfig.mcpServers = filterMCPsBySelection(cleanMcpConfig.mcpServers, templateConfig.selectedMCPs);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const destDir = path.dirname(destPath);
|
|
244
|
+
await fs.ensureDir(destDir);
|
|
245
|
+
await fs.writeJson(destPath, cleanMcpConfig, { spaces: 2 });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function mergeMCPFileFromContent(mcpContent, destPath, templateConfig) {
|
|
249
|
+
const newMcpConfig = JSON.parse(mcpContent);
|
|
250
|
+
let existingMcpConfig = {};
|
|
251
|
+
|
|
252
|
+
if (await fs.pathExists(destPath)) {
|
|
253
|
+
existingMcpConfig = await fs.readJson(destPath);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Clean new MCP config (only keep mcpServers without descriptions)
|
|
257
|
+
const cleanNewMcpConfig = { mcpServers: {} };
|
|
258
|
+
if (newMcpConfig.mcpServers) {
|
|
259
|
+
for (const serverName in newMcpConfig.mcpServers) {
|
|
260
|
+
if (newMcpConfig.mcpServers[serverName] && typeof newMcpConfig.mcpServers[serverName] === 'object') {
|
|
261
|
+
const serverConfig = { ...newMcpConfig.mcpServers[serverName] };
|
|
262
|
+
delete serverConfig.description; // Remove description field
|
|
263
|
+
cleanNewMcpConfig.mcpServers[serverName] = serverConfig;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Filter MCPs based on selection
|
|
269
|
+
if (templateConfig.selectedMCPs && cleanNewMcpConfig.mcpServers) {
|
|
270
|
+
cleanNewMcpConfig.mcpServers = filterMCPsBySelection(cleanNewMcpConfig.mcpServers, templateConfig.selectedMCPs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Merge MCP configurations (only keep mcpServers)
|
|
274
|
+
const mergedMcpConfig = {
|
|
275
|
+
mcpServers: {
|
|
276
|
+
...existingMcpConfig.mcpServers,
|
|
277
|
+
...cleanNewMcpConfig.mcpServers
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const destDir = path.dirname(destPath);
|
|
282
|
+
await fs.ensureDir(destDir);
|
|
283
|
+
await fs.writeJson(destPath, mergedMcpConfig, { spaces: 2 });
|
|
284
|
+
}
|
|
285
|
+
|
|
7
286
|
async function checkExistingFiles(targetDir, templateConfig) {
|
|
8
287
|
const existingFiles = [];
|
|
9
288
|
|
|
@@ -86,7 +365,7 @@ async function createBackups(existingFiles, targetDir) {
|
|
|
86
365
|
}
|
|
87
366
|
|
|
88
367
|
async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
89
|
-
|
|
368
|
+
console.log(chalk.gray(`📥 Downloading templates from GitHub (${GITHUB_CONFIG.branch} branch)...`));
|
|
90
369
|
|
|
91
370
|
// Check for existing files and get user preference
|
|
92
371
|
const existingFiles = await checkExistingFiles(targetDir, templateConfig);
|
|
@@ -112,9 +391,14 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
|
112
391
|
// Determine overwrite behavior based on user choice
|
|
113
392
|
const shouldOverwrite = userAction !== 'merge';
|
|
114
393
|
|
|
394
|
+
// Track success/failure statistics
|
|
395
|
+
let totalFiles = templateConfig.files.length;
|
|
396
|
+
let successfulFiles = 0;
|
|
397
|
+
let skippedFiles = 0;
|
|
398
|
+
let failedFiles = 0;
|
|
399
|
+
|
|
115
400
|
// Copy base files and framework-specific files
|
|
116
401
|
for (const file of templateConfig.files) {
|
|
117
|
-
const sourcePath = path.join(templateDir, file.source);
|
|
118
402
|
const destPath = path.join(targetDir, file.destination);
|
|
119
403
|
|
|
120
404
|
try {
|
|
@@ -123,133 +407,188 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
|
123
407
|
// This is a framework-specific commands directory - merge with existing commands
|
|
124
408
|
await fs.ensureDir(destPath);
|
|
125
409
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const destFile = path.join(destPath, frameworkFile);
|
|
410
|
+
// Download framework-specific commands from GitHub
|
|
411
|
+
try {
|
|
412
|
+
const frameworkFiles = await downloadDirectoryFromGitHub(file.source);
|
|
413
|
+
let filesWritten = 0;
|
|
131
414
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
415
|
+
for (const [frameworkFileName, content] of Object.entries(frameworkFiles)) {
|
|
416
|
+
const destFile = path.join(destPath, frameworkFileName);
|
|
417
|
+
|
|
418
|
+
// In merge mode, skip if file already exists
|
|
419
|
+
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
420
|
+
console.log(chalk.blue(`⏭️ Skipped ${frameworkFileName} (already exists)`));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await fs.writeFile(destFile, content, 'utf8');
|
|
425
|
+
filesWritten++;
|
|
136
426
|
}
|
|
137
427
|
|
|
138
|
-
|
|
428
|
+
if (filesWritten > 0) {
|
|
429
|
+
console.log(chalk.green(`✓ Downloaded ${filesWritten} framework commands ${file.source} → ${file.destination}`));
|
|
430
|
+
successfulFiles++;
|
|
431
|
+
} else {
|
|
432
|
+
console.log(chalk.yellow(`⚠️ No framework commands available for ${file.source}`));
|
|
433
|
+
skippedFiles++;
|
|
434
|
+
}
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.log(chalk.yellow(`⚠️ Could not download framework commands from ${file.source}: ${error.message}`));
|
|
437
|
+
console.log(chalk.yellow(` This is normal for some templates - continuing...`));
|
|
438
|
+
failedFiles++;
|
|
139
439
|
}
|
|
140
|
-
|
|
141
|
-
console.log(chalk.green(`✓ Copied framework commands ${file.source} → ${file.destination}`));
|
|
142
440
|
} else if (file.source.includes('.claude') && !file.source.includes('examples/')) {
|
|
143
|
-
// This is base .claude directory -
|
|
144
|
-
await fs.
|
|
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');
|
|
441
|
+
// This is base .claude directory - download it but handle commands specially
|
|
442
|
+
await fs.ensureDir(destPath);
|
|
155
443
|
|
|
156
|
-
|
|
157
|
-
|
|
444
|
+
// Download base .claude directory structure from GitHub
|
|
445
|
+
try {
|
|
446
|
+
const baseClaudeFiles = await downloadDirectoryFromGitHub(file.source);
|
|
158
447
|
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
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);
|
|
448
|
+
// Write non-command files first
|
|
449
|
+
for (const [fileName, content] of Object.entries(baseClaudeFiles)) {
|
|
450
|
+
if (fileName !== 'commands') { // Skip commands directory, handle separately
|
|
451
|
+
const destFile = path.join(destPath, fileName);
|
|
167
452
|
|
|
168
453
|
// In merge mode, skip if file already exists
|
|
169
454
|
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
170
|
-
console.log(chalk.blue(`⏭️ Skipped ${
|
|
455
|
+
console.log(chalk.blue(`⏭️ Skipped ${fileName} (already exists)`));
|
|
171
456
|
continue;
|
|
172
457
|
}
|
|
173
458
|
|
|
174
|
-
await fs.
|
|
459
|
+
await fs.writeFile(destFile, content, 'utf8');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Now handle base commands specifically
|
|
464
|
+
const destCommandsPath = path.join(destPath, 'commands');
|
|
465
|
+
await fs.ensureDir(destCommandsPath);
|
|
466
|
+
|
|
467
|
+
// Download base commands from GitHub
|
|
468
|
+
const baseCommandsDir = `${file.source}/commands`;
|
|
469
|
+
try {
|
|
470
|
+
const baseCommands = await downloadDirectoryFromGitHub(baseCommandsDir);
|
|
471
|
+
const excludeCommands = ['react-component.md', 'route.md', 'api-endpoint.md']; // Commands moved to framework dirs
|
|
472
|
+
|
|
473
|
+
for (const [baseCommandName, commandContent] of Object.entries(baseCommands)) {
|
|
474
|
+
if (!excludeCommands.includes(baseCommandName)) {
|
|
475
|
+
const destFile = path.join(destCommandsPath, baseCommandName);
|
|
476
|
+
|
|
477
|
+
// In merge mode, skip if file already exists
|
|
478
|
+
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
479
|
+
console.log(chalk.blue(`⏭️ Skipped ${baseCommandName} (already exists)`));
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await fs.writeFile(destFile, commandContent, 'utf8');
|
|
484
|
+
}
|
|
175
485
|
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
// Commands directory might not exist for some templates, that's ok
|
|
488
|
+
console.log(chalk.yellow(`⚠️ No commands directory found for ${baseCommandsDir}`));
|
|
176
489
|
}
|
|
490
|
+
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.log(chalk.yellow(`⚠️ Could not download .claude directory (${error.message})`));
|
|
493
|
+
console.log(chalk.yellow(` Continuing with other template files...`));
|
|
494
|
+
failedFiles++;
|
|
495
|
+
// Don't throw - continue with other files
|
|
496
|
+
continue; // Skip the success message
|
|
177
497
|
}
|
|
178
498
|
|
|
179
|
-
console.log(chalk.green(`✓
|
|
499
|
+
console.log(chalk.green(`✓ Downloaded base configuration and commands ${file.source} → ${file.destination}`));
|
|
500
|
+
successfulFiles++;
|
|
180
501
|
} else if (file.source.includes('settings.json') && templateConfig.selectedHooks) {
|
|
502
|
+
// Download and process settings.json with hooks
|
|
503
|
+
const settingsContent = await downloadFileFromGitHub(file.source);
|
|
504
|
+
|
|
181
505
|
// In merge mode, merge settings instead of overwriting
|
|
182
506
|
if (userAction === 'merge') {
|
|
183
|
-
await
|
|
507
|
+
await mergeSettingsFileFromContent(settingsContent, destPath, templateConfig);
|
|
184
508
|
console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected hooks)`));
|
|
185
509
|
} else {
|
|
186
|
-
await
|
|
187
|
-
console.log(chalk.green(`✓
|
|
510
|
+
await processSettingsFileFromContent(settingsContent, destPath, templateConfig);
|
|
511
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected hooks)`));
|
|
188
512
|
}
|
|
513
|
+
successfulFiles++;
|
|
189
514
|
} else if (file.source.includes('.mcp.json') && templateConfig.selectedMCPs) {
|
|
515
|
+
// Download and process MCP config with selected MCPs
|
|
516
|
+
const mcpContent = await downloadFileFromGitHub(file.source);
|
|
517
|
+
|
|
190
518
|
// In merge mode, merge MCP config instead of overwriting
|
|
191
519
|
if (userAction === 'merge') {
|
|
192
|
-
await
|
|
520
|
+
await mergeMCPFileFromContent(mcpContent, destPath, templateConfig);
|
|
193
521
|
console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected MCPs)`));
|
|
194
522
|
} else {
|
|
195
|
-
await
|
|
196
|
-
console.log(chalk.green(`✓
|
|
523
|
+
await processMCPFileFromContent(mcpContent, destPath, templateConfig);
|
|
524
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected MCPs)`));
|
|
197
525
|
}
|
|
526
|
+
successfulFiles++;
|
|
198
527
|
} else {
|
|
199
|
-
//
|
|
528
|
+
// Download regular files (CLAUDE.md, etc.)
|
|
200
529
|
// In merge mode, skip if file already exists
|
|
201
530
|
if (userAction === 'merge' && await fs.pathExists(destPath)) {
|
|
202
531
|
console.log(chalk.blue(`⏭️ Skipped ${file.destination} (already exists)`));
|
|
532
|
+
skippedFiles++;
|
|
203
533
|
continue;
|
|
204
534
|
}
|
|
205
535
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
536
|
+
try {
|
|
537
|
+
const fileContent = await downloadFileFromGitHub(file.source);
|
|
538
|
+
const destDir = path.dirname(destPath);
|
|
539
|
+
await fs.ensureDir(destDir);
|
|
540
|
+
await fs.writeFile(destPath, fileContent, 'utf8');
|
|
541
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination}`));
|
|
542
|
+
successfulFiles++;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error.message.includes('404')) {
|
|
545
|
+
console.log(chalk.yellow(`⚠️ File ${file.source} not found (404) - this is normal for some templates`));
|
|
546
|
+
skippedFiles++;
|
|
547
|
+
} else {
|
|
548
|
+
console.log(chalk.yellow(`⚠️ Could not download ${file.source}: ${error.message}`));
|
|
549
|
+
console.log(chalk.yellow(` Continuing with other template files...`));
|
|
550
|
+
failedFiles++;
|
|
211
551
|
}
|
|
212
|
-
}
|
|
213
|
-
console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination}`));
|
|
552
|
+
}
|
|
214
553
|
}
|
|
215
554
|
} catch (error) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
555
|
+
// Only throw for critical errors that should stop the entire process
|
|
556
|
+
if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
|
|
557
|
+
console.error(chalk.red(`✗ Permission error copying ${file.source}:`), error.message);
|
|
558
|
+
throw error;
|
|
559
|
+
} else {
|
|
560
|
+
console.log(chalk.yellow(`⚠️ Could not process ${file.source}: ${error.message}`));
|
|
561
|
+
console.log(chalk.yellow(` Skipping this file and continuing...`));
|
|
562
|
+
failedFiles++;
|
|
238
563
|
}
|
|
239
564
|
}
|
|
240
|
-
|
|
241
|
-
console.log(chalk.cyan(`📋 Installed ${templateConfig.selectedCommands.length} commands`));
|
|
242
565
|
}
|
|
243
566
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
567
|
+
// Show download summary
|
|
568
|
+
console.log(chalk.cyan('\n📦 Template Installation Summary:'));
|
|
569
|
+
if (successfulFiles > 0) {
|
|
570
|
+
console.log(chalk.green(` ✓ ${successfulFiles} files downloaded successfully`));
|
|
571
|
+
}
|
|
572
|
+
if (skippedFiles > 0) {
|
|
573
|
+
console.log(chalk.blue(` ⏭️ ${skippedFiles} files skipped (already exist or not needed)`));
|
|
574
|
+
}
|
|
575
|
+
if (failedFiles > 0) {
|
|
576
|
+
console.log(chalk.yellow(` ⚠️ ${failedFiles} files failed to download (continuing anyway)`));
|
|
247
577
|
}
|
|
248
578
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
579
|
+
console.log(chalk.gray(`\n📚 Source: https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tree/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}`));
|
|
580
|
+
|
|
581
|
+
// Consider it successful if we got at least some files
|
|
582
|
+
const hasEssentialFiles = successfulFiles > 0;
|
|
583
|
+
if (hasEssentialFiles) {
|
|
584
|
+
console.log(chalk.green('\n✅ Template installation completed successfully!'));
|
|
585
|
+
if (failedFiles > 0) {
|
|
586
|
+
console.log(chalk.yellow(' Some optional files were skipped due to rate limits or missing files.'));
|
|
587
|
+
console.log(chalk.yellow(' This is normal and your Claude Code configuration should work properly.'));
|
|
588
|
+
}
|
|
252
589
|
}
|
|
590
|
+
|
|
591
|
+
return hasEssentialFiles;
|
|
253
592
|
}
|
|
254
593
|
|
|
255
594
|
async function runPostInstallationValidation(targetDir, templateConfig) {
|