@regression-io/claude-config 0.14.16
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/LICENSE +21 -0
- package/README.md +286 -0
- package/cli.js +260 -0
- package/config-loader.js +1556 -0
- package/package.json +62 -0
- package/scripts/postinstall.js +50 -0
- package/scripts/sync-version.js +65 -0
- package/shared/mcp-registry.json +117 -0
- package/templates/composites/fastapi-react-js/rules/backend-python.md +54 -0
- package/templates/composites/fastapi-react-js/rules/frontend-react.md +69 -0
- package/templates/composites/fastapi-react-js/rules/monorepo.md +77 -0
- package/templates/composites/fastapi-react-js/template.json +7 -0
- package/templates/composites/fastapi-react-ts/rules/backend-python.md +54 -0
- package/templates/composites/fastapi-react-ts/rules/frontend-react.md +64 -0
- package/templates/composites/fastapi-react-ts/rules/monorepo.md +82 -0
- package/templates/composites/fastapi-react-ts/template.json +7 -0
- package/templates/frameworks/fastapi/rules/dependencies.md +89 -0
- package/templates/frameworks/fastapi/rules/endpoints.md +86 -0
- package/templates/frameworks/fastapi/rules/errors.md +101 -0
- package/templates/frameworks/fastapi/rules/structure.md +97 -0
- package/templates/frameworks/fastapi/template.json +6 -0
- package/templates/frameworks/mcp-python/rules/resources.md +93 -0
- package/templates/frameworks/mcp-python/rules/structure.md +74 -0
- package/templates/frameworks/mcp-python/rules/tools.md +80 -0
- package/templates/frameworks/mcp-python/template.json +6 -0
- package/templates/frameworks/python-cli/rules/commands.md +103 -0
- package/templates/frameworks/python-cli/rules/output.md +107 -0
- package/templates/frameworks/python-cli/rules/structure.md +91 -0
- package/templates/frameworks/python-cli/template.json +6 -0
- package/templates/frameworks/react-js/rules/components.md +84 -0
- package/templates/frameworks/react-js/rules/hooks.md +98 -0
- package/templates/frameworks/react-js/template.json +6 -0
- package/templates/frameworks/react-ts/rules/components.md +72 -0
- package/templates/frameworks/react-ts/rules/hooks.md +87 -0
- package/templates/frameworks/react-ts/rules/state.md +93 -0
- package/templates/frameworks/react-ts/template.json +6 -0
- package/templates/languages/javascript/rules/patterns.md +126 -0
- package/templates/languages/javascript/rules/style.md +92 -0
- package/templates/languages/javascript/template.json +6 -0
- package/templates/languages/python/rules/dependencies.md +77 -0
- package/templates/languages/python/rules/patterns.md +95 -0
- package/templates/languages/python/rules/style.md +63 -0
- package/templates/languages/python/template.json +6 -0
- package/templates/languages/typescript/rules/config.md +95 -0
- package/templates/languages/typescript/rules/patterns.md +119 -0
- package/templates/languages/typescript/rules/style.md +82 -0
- package/templates/languages/typescript/template.json +6 -0
- package/templates/universal/commands/commit.md +53 -0
- package/templates/universal/commands/debug.md +53 -0
- package/templates/universal/commands/document.md +54 -0
- package/templates/universal/commands/review.md +45 -0
- package/templates/universal/commands/security-review.md +52 -0
- package/templates/universal/commands/test.md +46 -0
- package/templates/universal/rules/api-design.md +38 -0
- package/templates/universal/rules/code-quality.md +40 -0
- package/templates/universal/rules/documentation.md +38 -0
- package/templates/universal/rules/error-handling.md +37 -0
- package/templates/universal/rules/git-workflow.md +39 -0
- package/templates/universal/rules/security.md +39 -0
- package/templates/universal/rules/testing.md +38 -0
- package/templates/universal/template.json +6 -0
- package/ui/dist/assets/index-C5apzulu.css +32 -0
- package/ui/dist/assets/index-CBNCwCnY.js +489 -0
- package/ui/dist/index.html +14 -0
- package/ui/server.cjs +2237 -0
- package/ui/terminal-server.cjs +160 -0
package/config-loader.js
ADDED
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Configuration Loader
|
|
5
|
+
*
|
|
6
|
+
* Uses standard JSON format throughout - no custom YAML.
|
|
7
|
+
* Copy/paste MCP configs from anywhere.
|
|
8
|
+
*
|
|
9
|
+
* Files:
|
|
10
|
+
* ~/.claude-config/mcp-registry.json - All available MCPs (copy/paste friendly)
|
|
11
|
+
* ~/.claude-config/templates/ - Rule and command templates
|
|
12
|
+
* project/.claude/mcps.json - Which MCPs this project uses
|
|
13
|
+
* project/.claude/rules/*.md - Project rules (from templates)
|
|
14
|
+
* project/.claude/commands/*.md - Project commands (from templates)
|
|
15
|
+
* project/.mcp.json - Generated output for Claude Code
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const VERSION = '0.14.16';
|
|
23
|
+
|
|
24
|
+
class ClaudeConfigManager {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.installDir = process.env.CLAUDE_CONFIG_HOME || path.join(process.env.HOME || '', '.claude-config');
|
|
27
|
+
|
|
28
|
+
// Look for registry in multiple places
|
|
29
|
+
const possiblePaths = [
|
|
30
|
+
path.join(__dirname, 'shared', 'mcp-registry.json'),
|
|
31
|
+
path.join(__dirname, 'mcp-registry.json'),
|
|
32
|
+
path.join(this.installDir, 'shared', 'mcp-registry.json')
|
|
33
|
+
];
|
|
34
|
+
this.registryPath = possiblePaths.find(p => fs.existsSync(p)) || possiblePaths[0];
|
|
35
|
+
|
|
36
|
+
// Template directory
|
|
37
|
+
const templatePaths = [
|
|
38
|
+
path.join(__dirname, 'templates'),
|
|
39
|
+
path.join(this.installDir, 'templates')
|
|
40
|
+
];
|
|
41
|
+
this.templatesDir = templatePaths.find(p => fs.existsSync(p)) || templatePaths[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load JSON file
|
|
46
|
+
*/
|
|
47
|
+
loadJson(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(filePath)) return null;
|
|
50
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`Error loading ${filePath}:`, error.message);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save JSON file
|
|
59
|
+
*/
|
|
60
|
+
saveJson(filePath, data) {
|
|
61
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load environment variables from .env file
|
|
66
|
+
*/
|
|
67
|
+
loadEnvFile(envPath) {
|
|
68
|
+
if (!fs.existsSync(envPath)) return {};
|
|
69
|
+
const envVars = {};
|
|
70
|
+
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const trimmed = line.trim();
|
|
73
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
74
|
+
const eqIndex = trimmed.indexOf('=');
|
|
75
|
+
if (eqIndex > 0) {
|
|
76
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
77
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
78
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
80
|
+
value = value.slice(1, -1);
|
|
81
|
+
}
|
|
82
|
+
envVars[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return envVars;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Interpolate ${VAR} in object values
|
|
91
|
+
*/
|
|
92
|
+
interpolate(obj, env) {
|
|
93
|
+
if (typeof obj === 'string') {
|
|
94
|
+
return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
95
|
+
return env[varName] || process.env[varName] || match;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(obj)) {
|
|
99
|
+
return obj.map(v => this.interpolate(v, env));
|
|
100
|
+
}
|
|
101
|
+
if (obj !== null && typeof obj === 'object') {
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
104
|
+
result[k] = this.interpolate(v, env);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
return obj;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find project root (has .claude/ directory)
|
|
113
|
+
*/
|
|
114
|
+
findProjectRoot(startDir = process.cwd()) {
|
|
115
|
+
let dir = path.resolve(startDir);
|
|
116
|
+
const root = path.parse(dir).root;
|
|
117
|
+
while (dir !== root) {
|
|
118
|
+
if (fs.existsSync(path.join(dir, '.claude'))) {
|
|
119
|
+
return dir;
|
|
120
|
+
}
|
|
121
|
+
dir = path.dirname(dir);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find ALL .claude/mcps.json configs from cwd up to root (and ~/.claude)
|
|
128
|
+
* Returns array from root to leaf (so child overrides parent when merged)
|
|
129
|
+
*/
|
|
130
|
+
findAllConfigs(startDir = process.cwd()) {
|
|
131
|
+
const configs = [];
|
|
132
|
+
let dir = path.resolve(startDir);
|
|
133
|
+
const root = path.parse(dir).root;
|
|
134
|
+
const homeDir = process.env.HOME || '';
|
|
135
|
+
|
|
136
|
+
// Walk up directory tree
|
|
137
|
+
while (dir !== root) {
|
|
138
|
+
const configPath = path.join(dir, '.claude', 'mcps.json');
|
|
139
|
+
if (fs.existsSync(configPath)) {
|
|
140
|
+
configs.unshift({ dir, configPath }); // Add at beginning (root first)
|
|
141
|
+
}
|
|
142
|
+
dir = path.dirname(dir);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Also check ~/.claude/mcps.json (global user config)
|
|
146
|
+
const homeConfig = path.join(homeDir, '.claude', 'mcps.json');
|
|
147
|
+
if (fs.existsSync(homeConfig)) {
|
|
148
|
+
// Only add if not already included
|
|
149
|
+
if (!configs.some(c => c.configPath === homeConfig)) {
|
|
150
|
+
configs.unshift({ dir: homeDir, configPath: homeConfig });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return configs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Merge multiple configs (later ones override earlier)
|
|
159
|
+
*/
|
|
160
|
+
mergeConfigs(configs) {
|
|
161
|
+
const merged = {
|
|
162
|
+
include: [],
|
|
163
|
+
mcpServers: {},
|
|
164
|
+
template: null
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
for (const { config } of configs) {
|
|
168
|
+
if (!config) continue;
|
|
169
|
+
|
|
170
|
+
// Merge include arrays (dedupe)
|
|
171
|
+
if (config.include && Array.isArray(config.include)) {
|
|
172
|
+
for (const mcp of config.include) {
|
|
173
|
+
if (!merged.include.includes(mcp)) {
|
|
174
|
+
merged.include.push(mcp);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Merge mcpServers (override)
|
|
180
|
+
if (config.mcpServers) {
|
|
181
|
+
Object.assign(merged.mcpServers, config.mcpServers);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Take the most specific template
|
|
185
|
+
if (config.template) {
|
|
186
|
+
merged.template = config.template;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return merged;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get project config path
|
|
195
|
+
*/
|
|
196
|
+
getConfigPath(projectDir = null) {
|
|
197
|
+
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
198
|
+
return path.join(dir, '.claude', 'mcps.json');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collect files (rules or commands) from all directories in hierarchy
|
|
203
|
+
* Returns array of { file, source, fullPath } with child files overriding parent
|
|
204
|
+
*/
|
|
205
|
+
collectFilesFromHierarchy(configLocations, subdir) {
|
|
206
|
+
const fileMap = new Map(); // filename -> { file, source, fullPath }
|
|
207
|
+
|
|
208
|
+
// Process from root to leaf (so child overrides parent)
|
|
209
|
+
for (const { dir } of configLocations) {
|
|
210
|
+
const dirPath = path.join(dir, '.claude', subdir);
|
|
211
|
+
if (fs.existsSync(dirPath)) {
|
|
212
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
fileMap.set(file, {
|
|
215
|
+
file,
|
|
216
|
+
source: dir,
|
|
217
|
+
fullPath: path.join(dirPath, file)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return Array.from(fileMap.values());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all rules from hierarchy (for external use)
|
|
228
|
+
*/
|
|
229
|
+
getAllRules(startDir = process.cwd()) {
|
|
230
|
+
const configLocations = this.findAllConfigs(startDir);
|
|
231
|
+
return this.collectFilesFromHierarchy(configLocations, 'rules');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all commands from hierarchy (for external use)
|
|
236
|
+
*/
|
|
237
|
+
getAllCommands(startDir = process.cwd()) {
|
|
238
|
+
const configLocations = this.findAllConfigs(startDir);
|
|
239
|
+
return this.collectFilesFromHierarchy(configLocations, 'commands');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
// TEMPLATE SYSTEM
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* List available templates
|
|
248
|
+
*/
|
|
249
|
+
listTemplates() {
|
|
250
|
+
console.log('\n📋 Available Templates:\n');
|
|
251
|
+
|
|
252
|
+
const categories = [
|
|
253
|
+
{ name: 'Frameworks', path: 'frameworks' },
|
|
254
|
+
{ name: 'Languages', path: 'languages' },
|
|
255
|
+
{ name: 'Composites (Monorepos)', path: 'composites' }
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
for (const category of categories) {
|
|
259
|
+
const categoryPath = path.join(this.templatesDir, category.path);
|
|
260
|
+
if (!fs.existsSync(categoryPath)) continue;
|
|
261
|
+
|
|
262
|
+
console.log(` ${category.name}:`);
|
|
263
|
+
const templates = fs.readdirSync(categoryPath).filter(f =>
|
|
264
|
+
fs.statSync(path.join(categoryPath, f)).isDirectory()
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
for (const template of templates) {
|
|
268
|
+
const templateJson = this.loadJson(path.join(categoryPath, template, 'template.json'));
|
|
269
|
+
const desc = templateJson?.description || '';
|
|
270
|
+
console.log(` • ${category.path}/${template}${desc ? ` - ${desc}` : ''}`);
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(' Usage: claude-config init --template <template-name>');
|
|
276
|
+
console.log(' Example: claude-config init --template fastapi');
|
|
277
|
+
console.log(' claude-config init --template fastapi-react-ts\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Find a template by name (searches all categories)
|
|
282
|
+
*/
|
|
283
|
+
findTemplate(name) {
|
|
284
|
+
// Direct path
|
|
285
|
+
if (name.includes('/')) {
|
|
286
|
+
const templatePath = path.join(this.templatesDir, name);
|
|
287
|
+
if (fs.existsSync(path.join(templatePath, 'template.json'))) {
|
|
288
|
+
return templatePath;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check root level first (for "universal")
|
|
293
|
+
const rootPath = path.join(this.templatesDir, name);
|
|
294
|
+
if (fs.existsSync(path.join(rootPath, 'template.json'))) {
|
|
295
|
+
return rootPath;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Search in categories
|
|
299
|
+
const categories = ['frameworks', 'languages', 'composites'];
|
|
300
|
+
for (const category of categories) {
|
|
301
|
+
const templatePath = path.join(this.templatesDir, category, name);
|
|
302
|
+
if (fs.existsSync(path.join(templatePath, 'template.json'))) {
|
|
303
|
+
return templatePath;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resolve all templates to include (following includes chain)
|
|
312
|
+
*/
|
|
313
|
+
resolveTemplateChain(templatePath, visited = new Set()) {
|
|
314
|
+
if (visited.has(templatePath)) return [];
|
|
315
|
+
visited.add(templatePath);
|
|
316
|
+
|
|
317
|
+
const templateJson = this.loadJson(path.join(templatePath, 'template.json'));
|
|
318
|
+
if (!templateJson) return [templatePath];
|
|
319
|
+
|
|
320
|
+
const chain = [];
|
|
321
|
+
|
|
322
|
+
// Process includes first (base templates)
|
|
323
|
+
if (templateJson.includes && Array.isArray(templateJson.includes)) {
|
|
324
|
+
for (const include of templateJson.includes) {
|
|
325
|
+
const includePath = this.findTemplate(include);
|
|
326
|
+
if (includePath) {
|
|
327
|
+
chain.push(...this.resolveTemplateChain(includePath, visited));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Then add this template
|
|
333
|
+
chain.push(templatePath);
|
|
334
|
+
|
|
335
|
+
return chain;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Copy template files to project (won't overwrite existing)
|
|
340
|
+
*/
|
|
341
|
+
copyTemplateFiles(templatePath, projectDir, options = {}) {
|
|
342
|
+
const { force = false, verbose = true } = options;
|
|
343
|
+
const rulesDir = path.join(templatePath, 'rules');
|
|
344
|
+
const commandsDir = path.join(templatePath, 'commands');
|
|
345
|
+
const projectRulesDir = path.join(projectDir, '.claude', 'rules');
|
|
346
|
+
const projectCommandsDir = path.join(projectDir, '.claude', 'commands');
|
|
347
|
+
|
|
348
|
+
let copied = 0;
|
|
349
|
+
let skipped = 0;
|
|
350
|
+
|
|
351
|
+
// Copy rules
|
|
352
|
+
if (fs.existsSync(rulesDir)) {
|
|
353
|
+
if (!fs.existsSync(projectRulesDir)) {
|
|
354
|
+
fs.mkdirSync(projectRulesDir, { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const file of fs.readdirSync(rulesDir)) {
|
|
358
|
+
if (!file.endsWith('.md')) continue;
|
|
359
|
+
const src = path.join(rulesDir, file);
|
|
360
|
+
const dest = path.join(projectRulesDir, file);
|
|
361
|
+
|
|
362
|
+
if (fs.existsSync(dest) && !force) {
|
|
363
|
+
skipped++;
|
|
364
|
+
if (verbose) console.log(` ⏭ rules/${file} (exists)`);
|
|
365
|
+
} else {
|
|
366
|
+
fs.copyFileSync(src, dest);
|
|
367
|
+
copied++;
|
|
368
|
+
if (verbose) console.log(` ✓ rules/${file}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Copy commands
|
|
374
|
+
if (fs.existsSync(commandsDir)) {
|
|
375
|
+
if (!fs.existsSync(projectCommandsDir)) {
|
|
376
|
+
fs.mkdirSync(projectCommandsDir, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const file of fs.readdirSync(commandsDir)) {
|
|
380
|
+
if (!file.endsWith('.md')) continue;
|
|
381
|
+
const src = path.join(commandsDir, file);
|
|
382
|
+
const dest = path.join(projectCommandsDir, file);
|
|
383
|
+
|
|
384
|
+
if (fs.existsSync(dest) && !force) {
|
|
385
|
+
skipped++;
|
|
386
|
+
if (verbose) console.log(` ⏭ commands/${file} (exists)`);
|
|
387
|
+
} else {
|
|
388
|
+
fs.copyFileSync(src, dest);
|
|
389
|
+
copied++;
|
|
390
|
+
if (verbose) console.log(` ✓ commands/${file}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { copied, skipped };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ===========================================================================
|
|
399
|
+
// CORE COMMANDS
|
|
400
|
+
// ===========================================================================
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate .mcp.json for a project (with hierarchical config merging)
|
|
404
|
+
*/
|
|
405
|
+
apply(projectDir = null) {
|
|
406
|
+
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
407
|
+
|
|
408
|
+
const registry = this.loadJson(this.registryPath);
|
|
409
|
+
if (!registry) {
|
|
410
|
+
console.error('Error: Could not load MCP registry from', this.registryPath);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Find and load all configs in hierarchy
|
|
415
|
+
const configLocations = this.findAllConfigs(dir);
|
|
416
|
+
|
|
417
|
+
if (configLocations.length === 0) {
|
|
418
|
+
console.error(`No .claude/mcps.json found in ${dir} or parent directories`);
|
|
419
|
+
console.error('Run: claude-config init');
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Load all configs
|
|
424
|
+
const loadedConfigs = configLocations.map(loc => ({
|
|
425
|
+
...loc,
|
|
426
|
+
config: this.loadJson(loc.configPath)
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
// Show config hierarchy if multiple configs found
|
|
430
|
+
if (loadedConfigs.length > 1) {
|
|
431
|
+
console.log('📚 Config hierarchy (merged):');
|
|
432
|
+
for (const { dir: d, configPath } of loadedConfigs) {
|
|
433
|
+
const relPath = d === process.env.HOME ? '~' : path.relative(process.cwd(), d) || '.';
|
|
434
|
+
console.log(` • ${relPath}/.claude/mcps.json`);
|
|
435
|
+
}
|
|
436
|
+
console.log('');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Merge all configs
|
|
440
|
+
const mergedConfig = this.mergeConfigs(loadedConfigs);
|
|
441
|
+
|
|
442
|
+
// Collect env vars from all levels (child overrides parent)
|
|
443
|
+
const globalEnvPath = path.join(path.dirname(this.registryPath), '.env');
|
|
444
|
+
let env = this.loadEnvFile(globalEnvPath);
|
|
445
|
+
|
|
446
|
+
for (const { dir: d } of loadedConfigs) {
|
|
447
|
+
const envPath = path.join(d, '.claude', '.env');
|
|
448
|
+
env = { ...env, ...this.loadEnvFile(envPath) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const output = { mcpServers: {} };
|
|
452
|
+
|
|
453
|
+
// Add MCPs from include list
|
|
454
|
+
if (mergedConfig.include && Array.isArray(mergedConfig.include)) {
|
|
455
|
+
for (const name of mergedConfig.include) {
|
|
456
|
+
if (registry.mcpServers && registry.mcpServers[name]) {
|
|
457
|
+
output.mcpServers[name] = this.interpolate(registry.mcpServers[name], env);
|
|
458
|
+
} else {
|
|
459
|
+
console.warn(`Warning: MCP "${name}" not found in registry`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Add custom mcpServers (override registry)
|
|
465
|
+
if (mergedConfig.mcpServers) {
|
|
466
|
+
for (const [name, config] of Object.entries(mergedConfig.mcpServers)) {
|
|
467
|
+
if (name.startsWith('_')) continue;
|
|
468
|
+
output.mcpServers[name] = this.interpolate(config, env);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const outputPath = path.join(dir, '.mcp.json');
|
|
473
|
+
this.saveJson(outputPath, output);
|
|
474
|
+
|
|
475
|
+
const count = Object.keys(output.mcpServers).length;
|
|
476
|
+
console.log(`✓ Generated ${outputPath}`);
|
|
477
|
+
console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
|
|
478
|
+
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* List available MCPs
|
|
484
|
+
*/
|
|
485
|
+
list() {
|
|
486
|
+
const registry = this.loadJson(this.registryPath);
|
|
487
|
+
if (!registry || !registry.mcpServers) {
|
|
488
|
+
console.error('Error: Could not load MCP registry');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const dir = this.findProjectRoot();
|
|
493
|
+
const projectConfig = dir ? this.loadJson(path.join(dir, '.claude', 'mcps.json')) : null;
|
|
494
|
+
const included = projectConfig?.include || [];
|
|
495
|
+
|
|
496
|
+
console.log('\n📚 Available MCPs:\n');
|
|
497
|
+
for (const name of Object.keys(registry.mcpServers)) {
|
|
498
|
+
const active = included.includes(name) ? ' ✓' : '';
|
|
499
|
+
console.log(` • ${name}${active}`);
|
|
500
|
+
}
|
|
501
|
+
console.log(`\n Total: ${Object.keys(registry.mcpServers).length} in registry`);
|
|
502
|
+
if (included.length) {
|
|
503
|
+
console.log(` Active: ${included.join(', ')}`);
|
|
504
|
+
}
|
|
505
|
+
console.log('');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Initialize project with template
|
|
510
|
+
*/
|
|
511
|
+
init(projectDir = null, templateName = null) {
|
|
512
|
+
const dir = projectDir || process.cwd();
|
|
513
|
+
const claudeDir = path.join(dir, '.claude');
|
|
514
|
+
const configPath = path.join(claudeDir, 'mcps.json');
|
|
515
|
+
|
|
516
|
+
// Create .claude directory
|
|
517
|
+
if (!fs.existsSync(claudeDir)) {
|
|
518
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Determine MCPs to include
|
|
522
|
+
let mcpDefaults = ['github', 'filesystem'];
|
|
523
|
+
let templateChain = [];
|
|
524
|
+
|
|
525
|
+
if (templateName) {
|
|
526
|
+
const templatePath = this.findTemplate(templateName);
|
|
527
|
+
if (!templatePath) {
|
|
528
|
+
console.error(`Template not found: ${templateName}`);
|
|
529
|
+
console.log('Run "claude-config templates" to see available templates.');
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Resolve full template chain
|
|
534
|
+
templateChain = this.resolveTemplateChain(templatePath);
|
|
535
|
+
|
|
536
|
+
// Get MCP defaults from the main template
|
|
537
|
+
const templateJson = this.loadJson(path.join(templatePath, 'template.json'));
|
|
538
|
+
if (templateJson?.mcpDefaults) {
|
|
539
|
+
mcpDefaults = templateJson.mcpDefaults;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(`\n🎯 Using template: ${templateName}`);
|
|
543
|
+
console.log(` Includes: ${templateChain.map(p => path.basename(p)).join(' → ')}\n`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Create or update mcps.json
|
|
547
|
+
if (!fs.existsSync(configPath)) {
|
|
548
|
+
const template = {
|
|
549
|
+
"include": mcpDefaults,
|
|
550
|
+
"template": templateName || null,
|
|
551
|
+
"mcpServers": {}
|
|
552
|
+
};
|
|
553
|
+
this.saveJson(configPath, template);
|
|
554
|
+
console.log(`✓ Created ${configPath}`);
|
|
555
|
+
} else {
|
|
556
|
+
console.log(`⏭ ${configPath} already exists`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Copy template files
|
|
560
|
+
if (templateChain.length > 0) {
|
|
561
|
+
console.log('\nCopying template files:');
|
|
562
|
+
let totalCopied = 0;
|
|
563
|
+
let totalSkipped = 0;
|
|
564
|
+
|
|
565
|
+
for (const tplPath of templateChain) {
|
|
566
|
+
const { copied, skipped } = this.copyTemplateFiles(tplPath, dir);
|
|
567
|
+
totalCopied += copied;
|
|
568
|
+
totalSkipped += skipped;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
console.log(`\n Total: ${totalCopied} copied, ${totalSkipped} skipped (already exist)`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Create .env file
|
|
575
|
+
const envPath = path.join(claudeDir, '.env');
|
|
576
|
+
if (!fs.existsSync(envPath)) {
|
|
577
|
+
fs.writeFileSync(envPath, `# Project secrets (gitignored)
|
|
578
|
+
# GITHUB_TOKEN=ghp_xxx
|
|
579
|
+
# DATABASE_URL=postgres://...
|
|
580
|
+
`);
|
|
581
|
+
console.log(`✓ Created ${envPath}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Update .gitignore
|
|
585
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
586
|
+
if (fs.existsSync(gitignorePath)) {
|
|
587
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
588
|
+
if (!content.includes('.claude/.env')) {
|
|
589
|
+
fs.appendFileSync(gitignorePath, '\n.claude/.env\n');
|
|
590
|
+
console.log('✓ Updated .gitignore');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log('\n✅ Project initialized!');
|
|
595
|
+
console.log('Next steps:');
|
|
596
|
+
console.log(' 1. Edit .claude/mcps.json to customize MCPs');
|
|
597
|
+
console.log(' 2. Review .claude/rules/ and .claude/commands/');
|
|
598
|
+
console.log(' 3. Run: claude-config apply\n');
|
|
599
|
+
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Apply templates to existing project (add rules/commands without overwriting)
|
|
605
|
+
*/
|
|
606
|
+
applyTemplate(templateName, projectDir = null) {
|
|
607
|
+
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
608
|
+
|
|
609
|
+
if (!templateName) {
|
|
610
|
+
console.error('Usage: claude-config apply-template <template-name>');
|
|
611
|
+
console.log('Run "claude-config templates" to see available templates.');
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const templatePath = this.findTemplate(templateName);
|
|
616
|
+
if (!templatePath) {
|
|
617
|
+
console.error(`Template not found: ${templateName}`);
|
|
618
|
+
console.log('Run "claude-config templates" to see available templates.');
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Resolve full template chain
|
|
623
|
+
const templateChain = this.resolveTemplateChain(templatePath);
|
|
624
|
+
|
|
625
|
+
console.log(`\n🎯 Applying template: ${templateName}`);
|
|
626
|
+
console.log(` Includes: ${templateChain.map(p => path.basename(p)).join(' → ')}\n`);
|
|
627
|
+
|
|
628
|
+
console.log('Copying template files (won\'t overwrite existing):');
|
|
629
|
+
let totalCopied = 0;
|
|
630
|
+
let totalSkipped = 0;
|
|
631
|
+
|
|
632
|
+
for (const tplPath of templateChain) {
|
|
633
|
+
const { copied, skipped } = this.copyTemplateFiles(tplPath, dir);
|
|
634
|
+
totalCopied += copied;
|
|
635
|
+
totalSkipped += skipped;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
console.log(`\n✅ Applied template: ${totalCopied} files copied, ${totalSkipped} skipped\n`);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Show current project config (including hierarchy)
|
|
644
|
+
*/
|
|
645
|
+
show(projectDir = null) {
|
|
646
|
+
const dir = projectDir || this.findProjectRoot() || process.cwd();
|
|
647
|
+
|
|
648
|
+
// Find all configs in hierarchy
|
|
649
|
+
const configLocations = this.findAllConfigs(dir);
|
|
650
|
+
|
|
651
|
+
if (configLocations.length === 0) {
|
|
652
|
+
console.log('No .claude/mcps.json found in current directory or parents');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
console.log(`\n📁 Project: ${dir}`);
|
|
657
|
+
|
|
658
|
+
// Show each config in hierarchy
|
|
659
|
+
if (configLocations.length > 1) {
|
|
660
|
+
console.log('\n📚 Config Hierarchy (root → leaf):');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const { dir: d, configPath } of configLocations) {
|
|
664
|
+
const config = this.loadJson(configPath);
|
|
665
|
+
const relPath = d === process.env.HOME ? '~' : path.relative(process.cwd(), d) || '.';
|
|
666
|
+
|
|
667
|
+
console.log(`\n📄 ${relPath}/.claude/mcps.json:`);
|
|
668
|
+
console.log(JSON.stringify(config, null, 2));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Show merged result
|
|
672
|
+
if (configLocations.length > 1) {
|
|
673
|
+
const loadedConfigs = configLocations.map(loc => ({
|
|
674
|
+
...loc,
|
|
675
|
+
config: this.loadJson(loc.configPath)
|
|
676
|
+
}));
|
|
677
|
+
const merged = this.mergeConfigs(loadedConfigs);
|
|
678
|
+
console.log('\n🔀 Merged Config (effective):');
|
|
679
|
+
console.log(JSON.stringify(merged, null, 2));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Collect rules and commands from all levels in hierarchy
|
|
683
|
+
const allRules = this.collectFilesFromHierarchy(configLocations, 'rules');
|
|
684
|
+
const allCommands = this.collectFilesFromHierarchy(configLocations, 'commands');
|
|
685
|
+
|
|
686
|
+
if (allRules.length) {
|
|
687
|
+
console.log(`\n📜 Rules (${allRules.length} total):`);
|
|
688
|
+
for (const { file, source } of allRules) {
|
|
689
|
+
const sourceLabel = source === process.env.HOME ? '~' : path.relative(process.cwd(), source) || '.';
|
|
690
|
+
console.log(` • ${file} (${sourceLabel})`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (allCommands.length) {
|
|
695
|
+
console.log(`\n⚡ Commands (${allCommands.length} total):`);
|
|
696
|
+
for (const { file, source } of allCommands) {
|
|
697
|
+
const sourceLabel = source === process.env.HOME ? '~' : path.relative(process.cwd(), source) || '.';
|
|
698
|
+
console.log(` • ${file} (${sourceLabel})`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
console.log('');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ===========================================================================
|
|
705
|
+
// MCP EDIT COMMANDS
|
|
706
|
+
// ===========================================================================
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Add MCP(s) to current project
|
|
710
|
+
*/
|
|
711
|
+
add(mcpNames) {
|
|
712
|
+
if (!mcpNames || mcpNames.length === 0) {
|
|
713
|
+
console.error('Usage: claude-config add <mcp-name> [mcp-name...]');
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const configPath = this.getConfigPath();
|
|
718
|
+
let config = this.loadJson(configPath);
|
|
719
|
+
|
|
720
|
+
if (!config) {
|
|
721
|
+
console.error('No .claude/mcps.json found. Run: claude-config init');
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const registry = this.loadJson(this.registryPath);
|
|
726
|
+
if (!config.include) config.include = [];
|
|
727
|
+
|
|
728
|
+
const added = [];
|
|
729
|
+
const notFound = [];
|
|
730
|
+
const alreadyExists = [];
|
|
731
|
+
|
|
732
|
+
for (const name of mcpNames) {
|
|
733
|
+
if (config.include.includes(name)) {
|
|
734
|
+
alreadyExists.push(name);
|
|
735
|
+
} else if (registry?.mcpServers?.[name]) {
|
|
736
|
+
config.include.push(name);
|
|
737
|
+
added.push(name);
|
|
738
|
+
} else {
|
|
739
|
+
notFound.push(name);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (added.length) {
|
|
744
|
+
this.saveJson(configPath, config);
|
|
745
|
+
console.log(`✓ Added: ${added.join(', ')}`);
|
|
746
|
+
}
|
|
747
|
+
if (alreadyExists.length) {
|
|
748
|
+
console.log(`Already included: ${alreadyExists.join(', ')}`);
|
|
749
|
+
}
|
|
750
|
+
if (notFound.length) {
|
|
751
|
+
console.log(`Not in registry: ${notFound.join(', ')}`);
|
|
752
|
+
console.log(' (Use "claude-config list" to see available MCPs)');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (added.length) {
|
|
756
|
+
console.log('\nRun "claude-config apply" to regenerate .mcp.json');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return added.length > 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Remove MCP(s) from current project
|
|
764
|
+
*/
|
|
765
|
+
remove(mcpNames) {
|
|
766
|
+
if (!mcpNames || mcpNames.length === 0) {
|
|
767
|
+
console.error('Usage: claude-config remove <mcp-name> [mcp-name...]');
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const configPath = this.getConfigPath();
|
|
772
|
+
let config = this.loadJson(configPath);
|
|
773
|
+
|
|
774
|
+
if (!config) {
|
|
775
|
+
console.error('No .claude/mcps.json found');
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!config.include) config.include = [];
|
|
780
|
+
|
|
781
|
+
const removed = [];
|
|
782
|
+
const notFound = [];
|
|
783
|
+
|
|
784
|
+
for (const name of mcpNames) {
|
|
785
|
+
const idx = config.include.indexOf(name);
|
|
786
|
+
if (idx !== -1) {
|
|
787
|
+
config.include.splice(idx, 1);
|
|
788
|
+
removed.push(name);
|
|
789
|
+
} else {
|
|
790
|
+
notFound.push(name);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (removed.length) {
|
|
795
|
+
this.saveJson(configPath, config);
|
|
796
|
+
console.log(`✓ Removed: ${removed.join(', ')}`);
|
|
797
|
+
console.log('\nRun "claude-config apply" to regenerate .mcp.json');
|
|
798
|
+
}
|
|
799
|
+
if (notFound.length) {
|
|
800
|
+
console.log(`Not in project: ${notFound.join(', ')}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return removed.length > 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ===========================================================================
|
|
807
|
+
// REGISTRY COMMANDS
|
|
808
|
+
// ===========================================================================
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Add MCP to global registry
|
|
812
|
+
*/
|
|
813
|
+
registryAdd(name, configJson) {
|
|
814
|
+
if (!name || !configJson) {
|
|
815
|
+
console.error('Usage: claude-config registry-add <name> \'{"command":"...","args":[...]}\'');
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let mcpConfig;
|
|
820
|
+
try {
|
|
821
|
+
mcpConfig = JSON.parse(configJson);
|
|
822
|
+
} catch (e) {
|
|
823
|
+
console.error('Invalid JSON:', e.message);
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const registry = this.loadJson(this.registryPath) || { mcpServers: {} };
|
|
828
|
+
registry.mcpServers[name] = mcpConfig;
|
|
829
|
+
this.saveJson(this.registryPath, registry);
|
|
830
|
+
|
|
831
|
+
console.log(`✓ Added "${name}" to registry`);
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Remove MCP from global registry
|
|
837
|
+
*/
|
|
838
|
+
registryRemove(name) {
|
|
839
|
+
if (!name) {
|
|
840
|
+
console.error('Usage: claude-config registry-remove <name>');
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const registry = this.loadJson(this.registryPath);
|
|
845
|
+
if (!registry?.mcpServers?.[name]) {
|
|
846
|
+
console.error(`"${name}" not found in registry`);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
delete registry.mcpServers[name];
|
|
851
|
+
this.saveJson(this.registryPath, registry);
|
|
852
|
+
|
|
853
|
+
console.log(`✓ Removed "${name}" from registry`);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ===========================================================================
|
|
858
|
+
// UPDATE COMMAND
|
|
859
|
+
// ===========================================================================
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Update claude-config from source
|
|
863
|
+
*/
|
|
864
|
+
update(sourcePath) {
|
|
865
|
+
if (!sourcePath) {
|
|
866
|
+
console.error('Usage: claude-config update /path/to/claude-config');
|
|
867
|
+
console.log('\nThis copies updated files from the source to your installation.');
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (!fs.existsSync(sourcePath)) {
|
|
872
|
+
console.error(`Source not found: ${sourcePath}`);
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const files = [
|
|
877
|
+
'config-loader.js',
|
|
878
|
+
'shared/mcp-registry.json',
|
|
879
|
+
'shell/claude-config.zsh'
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
let updated = 0;
|
|
883
|
+
for (const file of files) {
|
|
884
|
+
const src = path.join(sourcePath, file);
|
|
885
|
+
const dest = path.join(this.installDir, file);
|
|
886
|
+
|
|
887
|
+
if (fs.existsSync(src)) {
|
|
888
|
+
const destDir = path.dirname(dest);
|
|
889
|
+
if (!fs.existsSync(destDir)) {
|
|
890
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
891
|
+
}
|
|
892
|
+
fs.copyFileSync(src, dest);
|
|
893
|
+
console.log(`✓ Updated ${file}`);
|
|
894
|
+
updated++;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Copy templates directory
|
|
899
|
+
const srcTemplates = path.join(sourcePath, 'templates');
|
|
900
|
+
const destTemplates = path.join(this.installDir, 'templates');
|
|
901
|
+
if (fs.existsSync(srcTemplates)) {
|
|
902
|
+
this.copyDirRecursive(srcTemplates, destTemplates);
|
|
903
|
+
console.log(`✓ Updated templates/`);
|
|
904
|
+
updated++;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (updated > 0) {
|
|
908
|
+
console.log(`\n✅ Updated ${updated} item(s)`);
|
|
909
|
+
console.log('Restart your shell or run: source ~/.zshrc');
|
|
910
|
+
} else {
|
|
911
|
+
console.log('No files found to update');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return updated > 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Recursively copy directory
|
|
919
|
+
*/
|
|
920
|
+
copyDirRecursive(src, dest) {
|
|
921
|
+
if (!fs.existsSync(dest)) {
|
|
922
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
for (const item of fs.readdirSync(src)) {
|
|
926
|
+
const srcPath = path.join(src, item);
|
|
927
|
+
const destPath = path.join(dest, item);
|
|
928
|
+
|
|
929
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
930
|
+
this.copyDirRecursive(srcPath, destPath);
|
|
931
|
+
} else {
|
|
932
|
+
fs.copyFileSync(srcPath, destPath);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Show version
|
|
939
|
+
*/
|
|
940
|
+
version() {
|
|
941
|
+
console.log(`claude-config v${VERSION}`);
|
|
942
|
+
console.log(`Install: ${this.installDir}`);
|
|
943
|
+
console.log(`Registry: ${this.registryPath}`);
|
|
944
|
+
console.log(`Templates: ${this.templatesDir}`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ===========================================================================
|
|
948
|
+
// MEMORY COMMANDS
|
|
949
|
+
// ===========================================================================
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Show memory status and contents
|
|
953
|
+
*/
|
|
954
|
+
memoryList(projectDir = process.cwd()) {
|
|
955
|
+
const homeDir = process.env.HOME || '';
|
|
956
|
+
const globalMemoryDir = path.join(homeDir, '.claude', 'memory');
|
|
957
|
+
const projectMemoryDir = path.join(projectDir, '.claude', 'memory');
|
|
958
|
+
|
|
959
|
+
console.log('\n📝 Memory System\n');
|
|
960
|
+
|
|
961
|
+
// Global memory
|
|
962
|
+
console.log('Global (~/.claude/memory/):');
|
|
963
|
+
if (fs.existsSync(globalMemoryDir)) {
|
|
964
|
+
const files = ['preferences.md', 'corrections.md', 'facts.md'];
|
|
965
|
+
for (const file of files) {
|
|
966
|
+
const filePath = path.join(globalMemoryDir, file);
|
|
967
|
+
if (fs.existsSync(filePath)) {
|
|
968
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
969
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).length;
|
|
970
|
+
console.log(` ✓ ${file} (${lines} entries)`);
|
|
971
|
+
} else {
|
|
972
|
+
console.log(` ○ ${file} (not created)`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
console.log(' Not initialized');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Project memory
|
|
980
|
+
console.log(`\nProject (${projectDir}/.claude/memory/):`);
|
|
981
|
+
if (fs.existsSync(projectMemoryDir)) {
|
|
982
|
+
const files = ['context.md', 'patterns.md', 'decisions.md', 'issues.md', 'history.md'];
|
|
983
|
+
for (const file of files) {
|
|
984
|
+
const filePath = path.join(projectMemoryDir, file);
|
|
985
|
+
if (fs.existsSync(filePath)) {
|
|
986
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
987
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).length;
|
|
988
|
+
console.log(` ✓ ${file} (${lines} entries)`);
|
|
989
|
+
} else {
|
|
990
|
+
console.log(` ○ ${file} (not created)`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
console.log(' Not initialized. Run: claude-config memory init');
|
|
995
|
+
}
|
|
996
|
+
console.log();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Initialize project memory
|
|
1001
|
+
*/
|
|
1002
|
+
memoryInit(projectDir = process.cwd()) {
|
|
1003
|
+
const memoryDir = path.join(projectDir, '.claude', 'memory');
|
|
1004
|
+
|
|
1005
|
+
if (fs.existsSync(memoryDir)) {
|
|
1006
|
+
console.log('Project memory already initialized at', memoryDir);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
1011
|
+
|
|
1012
|
+
const files = {
|
|
1013
|
+
'context.md': '# Project Context\n\n<!-- Project overview and key information -->\n',
|
|
1014
|
+
'patterns.md': '# Code Patterns\n\n<!-- Established patterns in this codebase -->\n',
|
|
1015
|
+
'decisions.md': '# Architecture Decisions\n\n<!-- Key decisions and their rationale -->\n',
|
|
1016
|
+
'issues.md': '# Known Issues\n\n<!-- Current issues and workarounds -->\n',
|
|
1017
|
+
'history.md': '# Session History\n\n<!-- Notable changes and milestones -->\n'
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
for (const [file, content] of Object.entries(files)) {
|
|
1021
|
+
fs.writeFileSync(path.join(memoryDir, file), content);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
console.log(`✓ Initialized project memory at ${memoryDir}`);
|
|
1025
|
+
console.log('\nCreated:');
|
|
1026
|
+
for (const file of Object.keys(files)) {
|
|
1027
|
+
console.log(` ${file}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Add entry to memory
|
|
1033
|
+
*/
|
|
1034
|
+
memoryAdd(type, content, projectDir = process.cwd()) {
|
|
1035
|
+
if (!type || !content) {
|
|
1036
|
+
console.error('Usage: claude-config memory add <type> "<content>"');
|
|
1037
|
+
console.log('\nTypes:');
|
|
1038
|
+
console.log(' Global: preference, correction, fact');
|
|
1039
|
+
console.log(' Project: context, pattern, decision, issue, history');
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const homeDir = process.env.HOME || '';
|
|
1044
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
1045
|
+
|
|
1046
|
+
// Map type to file
|
|
1047
|
+
const typeMap = {
|
|
1048
|
+
// Global
|
|
1049
|
+
preference: { dir: path.join(homeDir, '.claude', 'memory'), file: 'preferences.md' },
|
|
1050
|
+
correction: { dir: path.join(homeDir, '.claude', 'memory'), file: 'corrections.md' },
|
|
1051
|
+
fact: { dir: path.join(homeDir, '.claude', 'memory'), file: 'facts.md' },
|
|
1052
|
+
// Project
|
|
1053
|
+
context: { dir: path.join(projectDir, '.claude', 'memory'), file: 'context.md' },
|
|
1054
|
+
pattern: { dir: path.join(projectDir, '.claude', 'memory'), file: 'patterns.md' },
|
|
1055
|
+
decision: { dir: path.join(projectDir, '.claude', 'memory'), file: 'decisions.md' },
|
|
1056
|
+
issue: { dir: path.join(projectDir, '.claude', 'memory'), file: 'issues.md' },
|
|
1057
|
+
history: { dir: path.join(projectDir, '.claude', 'memory'), file: 'history.md' }
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
const target = typeMap[type];
|
|
1061
|
+
if (!target) {
|
|
1062
|
+
console.error(`Unknown type: ${type}`);
|
|
1063
|
+
console.log('Valid types: preference, correction, fact, context, pattern, decision, issue, history');
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Ensure directory exists
|
|
1068
|
+
if (!fs.existsSync(target.dir)) {
|
|
1069
|
+
fs.mkdirSync(target.dir, { recursive: true });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const filePath = path.join(target.dir, target.file);
|
|
1073
|
+
|
|
1074
|
+
// Create file with header if it doesn't exist
|
|
1075
|
+
if (!fs.existsSync(filePath)) {
|
|
1076
|
+
const headers = {
|
|
1077
|
+
'preferences.md': '# Preferences\n',
|
|
1078
|
+
'corrections.md': '# Corrections\n',
|
|
1079
|
+
'facts.md': '# Facts\n',
|
|
1080
|
+
'context.md': '# Project Context\n',
|
|
1081
|
+
'patterns.md': '# Code Patterns\n',
|
|
1082
|
+
'decisions.md': '# Architecture Decisions\n',
|
|
1083
|
+
'issues.md': '# Known Issues\n',
|
|
1084
|
+
'history.md': '# Session History\n'
|
|
1085
|
+
};
|
|
1086
|
+
fs.writeFileSync(filePath, headers[target.file] || '');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Append entry
|
|
1090
|
+
const entry = `\n- [${timestamp}] ${content}\n`;
|
|
1091
|
+
fs.appendFileSync(filePath, entry);
|
|
1092
|
+
|
|
1093
|
+
console.log(`✓ Added ${type} to ${target.file}`);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Search memory files
|
|
1098
|
+
*/
|
|
1099
|
+
memorySearch(query, projectDir = process.cwd()) {
|
|
1100
|
+
if (!query) {
|
|
1101
|
+
console.error('Usage: claude-config memory search <query>');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const homeDir = process.env.HOME || '';
|
|
1106
|
+
const searchDirs = [
|
|
1107
|
+
{ label: 'Global', dir: path.join(homeDir, '.claude', 'memory') },
|
|
1108
|
+
{ label: 'Project', dir: path.join(projectDir, '.claude', 'memory') }
|
|
1109
|
+
];
|
|
1110
|
+
|
|
1111
|
+
const results = [];
|
|
1112
|
+
const queryLower = query.toLowerCase();
|
|
1113
|
+
|
|
1114
|
+
for (const { label, dir } of searchDirs) {
|
|
1115
|
+
if (!fs.existsSync(dir)) continue;
|
|
1116
|
+
|
|
1117
|
+
for (const file of fs.readdirSync(dir)) {
|
|
1118
|
+
if (!file.endsWith('.md')) continue;
|
|
1119
|
+
const filePath = path.join(dir, file);
|
|
1120
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1121
|
+
const lines = content.split('\n');
|
|
1122
|
+
|
|
1123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1124
|
+
if (lines[i].toLowerCase().includes(queryLower)) {
|
|
1125
|
+
results.push({
|
|
1126
|
+
location: `${label}/${file}`,
|
|
1127
|
+
line: i + 1,
|
|
1128
|
+
content: lines[i].trim()
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (results.length === 0) {
|
|
1136
|
+
console.log(`No matches found for "${query}"`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
console.log(`\n🔍 Found ${results.length} match(es) for "${query}":\n`);
|
|
1141
|
+
for (const r of results) {
|
|
1142
|
+
console.log(` ${r.location}:${r.line}`);
|
|
1143
|
+
console.log(` ${r.content}\n`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// ===========================================================================
|
|
1148
|
+
// ENV COMMANDS
|
|
1149
|
+
// ===========================================================================
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* List environment variables
|
|
1153
|
+
*/
|
|
1154
|
+
envList(projectDir = process.cwd()) {
|
|
1155
|
+
const envPath = path.join(projectDir, '.claude', '.env');
|
|
1156
|
+
|
|
1157
|
+
console.log(`\n🔐 Environment Variables (${projectDir}/.claude/.env)\n`);
|
|
1158
|
+
|
|
1159
|
+
if (!fs.existsSync(envPath)) {
|
|
1160
|
+
console.log(' No .env file found.');
|
|
1161
|
+
console.log(' Create with: claude-config env set <KEY> <value>\n');
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
1166
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
1167
|
+
|
|
1168
|
+
if (lines.length === 0) {
|
|
1169
|
+
console.log(' No variables set.\n');
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
for (const line of lines) {
|
|
1174
|
+
const [key] = line.split('=');
|
|
1175
|
+
if (key) {
|
|
1176
|
+
console.log(` ${key}=****`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
console.log(`\n Total: ${lines.length} variable(s)\n`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Set environment variable
|
|
1184
|
+
*/
|
|
1185
|
+
envSet(key, value, projectDir = process.cwd()) {
|
|
1186
|
+
if (!key || value === undefined) {
|
|
1187
|
+
console.error('Usage: claude-config env set <KEY> <value>');
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
1192
|
+
const envPath = path.join(claudeDir, '.env');
|
|
1193
|
+
|
|
1194
|
+
// Ensure .claude directory exists
|
|
1195
|
+
if (!fs.existsSync(claudeDir)) {
|
|
1196
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Read existing content
|
|
1200
|
+
let lines = [];
|
|
1201
|
+
if (fs.existsSync(envPath)) {
|
|
1202
|
+
lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Update or add the variable
|
|
1206
|
+
const keyUpper = key.toUpperCase();
|
|
1207
|
+
let found = false;
|
|
1208
|
+
lines = lines.map(line => {
|
|
1209
|
+
if (line.startsWith(`${keyUpper}=`)) {
|
|
1210
|
+
found = true;
|
|
1211
|
+
return `${keyUpper}=${value}`;
|
|
1212
|
+
}
|
|
1213
|
+
return line;
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
if (!found) {
|
|
1217
|
+
lines.push(`${keyUpper}=${value}`);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Write back
|
|
1221
|
+
fs.writeFileSync(envPath, lines.filter(l => l.trim()).join('\n') + '\n');
|
|
1222
|
+
|
|
1223
|
+
console.log(`✓ Set ${keyUpper} in .claude/.env`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Unset environment variable
|
|
1228
|
+
*/
|
|
1229
|
+
envUnset(key, projectDir = process.cwd()) {
|
|
1230
|
+
if (!key) {
|
|
1231
|
+
console.error('Usage: claude-config env unset <KEY>');
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const envPath = path.join(projectDir, '.claude', '.env');
|
|
1236
|
+
|
|
1237
|
+
if (!fs.existsSync(envPath)) {
|
|
1238
|
+
console.log('No .env file found.');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const keyUpper = key.toUpperCase();
|
|
1243
|
+
let lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
1244
|
+
const originalLength = lines.length;
|
|
1245
|
+
|
|
1246
|
+
lines = lines.filter(line => !line.startsWith(`${keyUpper}=`));
|
|
1247
|
+
|
|
1248
|
+
if (lines.length === originalLength) {
|
|
1249
|
+
console.log(`Variable ${keyUpper} not found.`);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
fs.writeFileSync(envPath, lines.filter(l => l.trim()).join('\n') + '\n');
|
|
1254
|
+
console.log(`✓ Removed ${keyUpper} from .claude/.env`);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// ===========================================================================
|
|
1258
|
+
// PROJECT REGISTRY (for UI project switching)
|
|
1259
|
+
// ===========================================================================
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Get projects registry path
|
|
1263
|
+
*/
|
|
1264
|
+
getProjectsRegistryPath() {
|
|
1265
|
+
return path.join(this.installDir, 'projects.json');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Load projects registry
|
|
1270
|
+
*/
|
|
1271
|
+
loadProjectsRegistry() {
|
|
1272
|
+
const registryPath = this.getProjectsRegistryPath();
|
|
1273
|
+
if (fs.existsSync(registryPath)) {
|
|
1274
|
+
try {
|
|
1275
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
return { projects: [], activeProjectId: null };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return { projects: [], activeProjectId: null };
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Save projects registry
|
|
1285
|
+
*/
|
|
1286
|
+
saveProjectsRegistry(registry) {
|
|
1287
|
+
const registryPath = this.getProjectsRegistryPath();
|
|
1288
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* List registered projects
|
|
1293
|
+
*/
|
|
1294
|
+
projectList() {
|
|
1295
|
+
const registry = this.loadProjectsRegistry();
|
|
1296
|
+
|
|
1297
|
+
if (registry.projects.length === 0) {
|
|
1298
|
+
console.log('\nNo projects registered.');
|
|
1299
|
+
console.log('Add one with: claude-config project add [path]\n');
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
console.log('\n📁 Registered Projects:\n');
|
|
1304
|
+
for (const p of registry.projects) {
|
|
1305
|
+
const active = p.id === registry.activeProjectId ? '→ ' : ' ';
|
|
1306
|
+
const exists = fs.existsSync(p.path) ? '' : ' (not found)';
|
|
1307
|
+
console.log(`${active}${p.name}${exists}`);
|
|
1308
|
+
console.log(` ${p.path}`);
|
|
1309
|
+
}
|
|
1310
|
+
console.log('');
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Add project to registry
|
|
1315
|
+
*/
|
|
1316
|
+
projectAdd(projectPath = process.cwd(), name = null) {
|
|
1317
|
+
const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
|
|
1318
|
+
|
|
1319
|
+
if (!fs.existsSync(absPath)) {
|
|
1320
|
+
console.error(`Path not found: ${absPath}`);
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const registry = this.loadProjectsRegistry();
|
|
1325
|
+
|
|
1326
|
+
// Check for duplicate
|
|
1327
|
+
if (registry.projects.some(p => p.path === absPath)) {
|
|
1328
|
+
console.log(`Already registered: ${absPath}`);
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const project = {
|
|
1333
|
+
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
|
|
1334
|
+
name: name || path.basename(absPath),
|
|
1335
|
+
path: absPath,
|
|
1336
|
+
addedAt: new Date().toISOString(),
|
|
1337
|
+
lastOpened: null
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
registry.projects.push(project);
|
|
1341
|
+
|
|
1342
|
+
// If first project, make it active
|
|
1343
|
+
if (!registry.activeProjectId) {
|
|
1344
|
+
registry.activeProjectId = project.id;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
this.saveProjectsRegistry(registry);
|
|
1348
|
+
console.log(`✓ Added project: ${project.name}`);
|
|
1349
|
+
console.log(` ${absPath}`);
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Remove project from registry
|
|
1355
|
+
*/
|
|
1356
|
+
projectRemove(nameOrPath) {
|
|
1357
|
+
if (!nameOrPath) {
|
|
1358
|
+
console.error('Usage: claude-config project remove <name|path>');
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const registry = this.loadProjectsRegistry();
|
|
1363
|
+
const absPath = path.resolve(nameOrPath.replace(/^~/, process.env.HOME || ''));
|
|
1364
|
+
|
|
1365
|
+
const idx = registry.projects.findIndex(
|
|
1366
|
+
p => p.name === nameOrPath || p.path === absPath
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
if (idx === -1) {
|
|
1370
|
+
console.error(`Project not found: ${nameOrPath}`);
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const removed = registry.projects.splice(idx, 1)[0];
|
|
1375
|
+
|
|
1376
|
+
// If removed active project, select first remaining
|
|
1377
|
+
if (registry.activeProjectId === removed.id) {
|
|
1378
|
+
registry.activeProjectId = registry.projects[0]?.id || null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
this.saveProjectsRegistry(registry);
|
|
1382
|
+
console.log(`✓ Removed project: ${removed.name}`);
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// =============================================================================
|
|
1388
|
+
// CLI
|
|
1389
|
+
// =============================================================================
|
|
1390
|
+
|
|
1391
|
+
if (require.main === module) {
|
|
1392
|
+
const args = process.argv.slice(2);
|
|
1393
|
+
const command = args[0];
|
|
1394
|
+
const manager = new ClaudeConfigManager();
|
|
1395
|
+
|
|
1396
|
+
// Parse --template flag for init
|
|
1397
|
+
const templateIndex = args.indexOf('--template');
|
|
1398
|
+
const templateArg = templateIndex !== -1 ? args[templateIndex + 1] : null;
|
|
1399
|
+
|
|
1400
|
+
switch (command) {
|
|
1401
|
+
// Core
|
|
1402
|
+
case 'init':
|
|
1403
|
+
if (templateArg) {
|
|
1404
|
+
// Remove --template and its value from args for path detection
|
|
1405
|
+
const filteredArgs = args.filter((_, i) => i !== templateIndex && i !== templateIndex + 1);
|
|
1406
|
+
manager.init(filteredArgs[1], templateArg);
|
|
1407
|
+
} else {
|
|
1408
|
+
manager.init(args[1]);
|
|
1409
|
+
}
|
|
1410
|
+
break;
|
|
1411
|
+
case 'apply':
|
|
1412
|
+
manager.apply(args[1]);
|
|
1413
|
+
break;
|
|
1414
|
+
case 'apply-template':
|
|
1415
|
+
manager.applyTemplate(args[1], args[2]);
|
|
1416
|
+
break;
|
|
1417
|
+
case 'show':
|
|
1418
|
+
manager.show(args[1]);
|
|
1419
|
+
break;
|
|
1420
|
+
case 'list':
|
|
1421
|
+
case 'mcps':
|
|
1422
|
+
manager.list();
|
|
1423
|
+
break;
|
|
1424
|
+
case 'templates':
|
|
1425
|
+
manager.listTemplates();
|
|
1426
|
+
break;
|
|
1427
|
+
|
|
1428
|
+
// Edit MCPs
|
|
1429
|
+
case 'add':
|
|
1430
|
+
manager.add(args.slice(1));
|
|
1431
|
+
break;
|
|
1432
|
+
case 'remove':
|
|
1433
|
+
case 'rm':
|
|
1434
|
+
manager.remove(args.slice(1));
|
|
1435
|
+
break;
|
|
1436
|
+
|
|
1437
|
+
// Registry management
|
|
1438
|
+
case 'registry-add':
|
|
1439
|
+
manager.registryAdd(args[1], args[2]);
|
|
1440
|
+
break;
|
|
1441
|
+
case 'registry-remove':
|
|
1442
|
+
case 'registry-rm':
|
|
1443
|
+
manager.registryRemove(args[1]);
|
|
1444
|
+
break;
|
|
1445
|
+
|
|
1446
|
+
// Memory
|
|
1447
|
+
case 'memory':
|
|
1448
|
+
if (args[1] === 'init') {
|
|
1449
|
+
manager.memoryInit(args[2]);
|
|
1450
|
+
} else if (args[1] === 'add') {
|
|
1451
|
+
manager.memoryAdd(args[2], args.slice(3).join(' '));
|
|
1452
|
+
} else if (args[1] === 'search') {
|
|
1453
|
+
manager.memorySearch(args.slice(2).join(' '));
|
|
1454
|
+
} else {
|
|
1455
|
+
manager.memoryList();
|
|
1456
|
+
}
|
|
1457
|
+
break;
|
|
1458
|
+
|
|
1459
|
+
// Environment
|
|
1460
|
+
case 'env':
|
|
1461
|
+
if (args[1] === 'set') {
|
|
1462
|
+
manager.envSet(args[2], args[3]);
|
|
1463
|
+
} else if (args[1] === 'unset') {
|
|
1464
|
+
manager.envUnset(args[2]);
|
|
1465
|
+
} else {
|
|
1466
|
+
manager.envList();
|
|
1467
|
+
}
|
|
1468
|
+
break;
|
|
1469
|
+
|
|
1470
|
+
// Project registry (for UI)
|
|
1471
|
+
case 'project':
|
|
1472
|
+
case 'projects':
|
|
1473
|
+
if (args[1] === 'add') {
|
|
1474
|
+
const nameIdx = args.indexOf('--name');
|
|
1475
|
+
const name = nameIdx !== -1 ? args[nameIdx + 1] : null;
|
|
1476
|
+
const projectPath = args[2] && !args[2].startsWith('--') ? args[2] : process.cwd();
|
|
1477
|
+
manager.projectAdd(projectPath, name);
|
|
1478
|
+
} else if (args[1] === 'remove' || args[1] === 'rm') {
|
|
1479
|
+
manager.projectRemove(args[2]);
|
|
1480
|
+
} else {
|
|
1481
|
+
manager.projectList();
|
|
1482
|
+
}
|
|
1483
|
+
break;
|
|
1484
|
+
|
|
1485
|
+
// Maintenance
|
|
1486
|
+
case 'update':
|
|
1487
|
+
manager.update(args[1]);
|
|
1488
|
+
break;
|
|
1489
|
+
case 'ui': {
|
|
1490
|
+
const UIServer = require('./ui/server.cjs');
|
|
1491
|
+
const port = parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1] || '3333');
|
|
1492
|
+
const uiDir = args.find(a => !a.startsWith('--') && a !== 'ui') || process.cwd();
|
|
1493
|
+
const uiServer = new UIServer(port, uiDir, manager);
|
|
1494
|
+
uiServer.start();
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
case 'version':
|
|
1498
|
+
case '-v':
|
|
1499
|
+
case '--version':
|
|
1500
|
+
manager.version();
|
|
1501
|
+
break;
|
|
1502
|
+
|
|
1503
|
+
default:
|
|
1504
|
+
console.log(`
|
|
1505
|
+
claude-config v${VERSION}
|
|
1506
|
+
|
|
1507
|
+
Usage:
|
|
1508
|
+
claude-config <command> [args]
|
|
1509
|
+
|
|
1510
|
+
Project Commands:
|
|
1511
|
+
init [--template <name>] Initialize project (optionally with template)
|
|
1512
|
+
apply Generate .mcp.json from config
|
|
1513
|
+
apply-template <name> Add template rules/commands to existing project
|
|
1514
|
+
show Show current project config
|
|
1515
|
+
list List available MCPs (✓ = active)
|
|
1516
|
+
templates List available templates
|
|
1517
|
+
add <mcp> [mcp...] Add MCP(s) to project
|
|
1518
|
+
remove <mcp> [mcp...] Remove MCP(s) from project
|
|
1519
|
+
|
|
1520
|
+
Memory Commands:
|
|
1521
|
+
memory Show memory status
|
|
1522
|
+
memory init Initialize project memory
|
|
1523
|
+
memory add <type> <content> Add entry (types: preference, correction, fact,
|
|
1524
|
+
context, pattern, decision, issue, history)
|
|
1525
|
+
memory search <query> Search all memory files
|
|
1526
|
+
|
|
1527
|
+
Environment Commands:
|
|
1528
|
+
env List environment variables
|
|
1529
|
+
env set <KEY> <value> Set variable in .claude/.env
|
|
1530
|
+
env unset <KEY> Remove variable
|
|
1531
|
+
|
|
1532
|
+
Project Commands (for UI):
|
|
1533
|
+
project List registered projects
|
|
1534
|
+
project add [path] Add project (defaults to cwd)
|
|
1535
|
+
project add [path] --name X Add with custom display name
|
|
1536
|
+
project remove <name|path> Remove project from registry
|
|
1537
|
+
|
|
1538
|
+
Registry Commands:
|
|
1539
|
+
registry-add <name> '<json>' Add MCP to global registry
|
|
1540
|
+
registry-remove <name> Remove MCP from registry
|
|
1541
|
+
|
|
1542
|
+
Maintenance:
|
|
1543
|
+
ui [--port=3333] Open web UI
|
|
1544
|
+
version Show version info
|
|
1545
|
+
|
|
1546
|
+
Examples:
|
|
1547
|
+
claude-config init --template fastapi
|
|
1548
|
+
claude-config add postgres github
|
|
1549
|
+
claude-config memory add preference "Use TypeScript for new files"
|
|
1550
|
+
claude-config env set GITHUB_TOKEN ghp_xxx
|
|
1551
|
+
claude-config apply
|
|
1552
|
+
`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
module.exports = ClaudeConfigManager;
|