@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/cli.js +260 -0
  4. package/config-loader.js +1556 -0
  5. package/package.json +62 -0
  6. package/scripts/postinstall.js +50 -0
  7. package/scripts/sync-version.js +65 -0
  8. package/shared/mcp-registry.json +117 -0
  9. package/templates/composites/fastapi-react-js/rules/backend-python.md +54 -0
  10. package/templates/composites/fastapi-react-js/rules/frontend-react.md +69 -0
  11. package/templates/composites/fastapi-react-js/rules/monorepo.md +77 -0
  12. package/templates/composites/fastapi-react-js/template.json +7 -0
  13. package/templates/composites/fastapi-react-ts/rules/backend-python.md +54 -0
  14. package/templates/composites/fastapi-react-ts/rules/frontend-react.md +64 -0
  15. package/templates/composites/fastapi-react-ts/rules/monorepo.md +82 -0
  16. package/templates/composites/fastapi-react-ts/template.json +7 -0
  17. package/templates/frameworks/fastapi/rules/dependencies.md +89 -0
  18. package/templates/frameworks/fastapi/rules/endpoints.md +86 -0
  19. package/templates/frameworks/fastapi/rules/errors.md +101 -0
  20. package/templates/frameworks/fastapi/rules/structure.md +97 -0
  21. package/templates/frameworks/fastapi/template.json +6 -0
  22. package/templates/frameworks/mcp-python/rules/resources.md +93 -0
  23. package/templates/frameworks/mcp-python/rules/structure.md +74 -0
  24. package/templates/frameworks/mcp-python/rules/tools.md +80 -0
  25. package/templates/frameworks/mcp-python/template.json +6 -0
  26. package/templates/frameworks/python-cli/rules/commands.md +103 -0
  27. package/templates/frameworks/python-cli/rules/output.md +107 -0
  28. package/templates/frameworks/python-cli/rules/structure.md +91 -0
  29. package/templates/frameworks/python-cli/template.json +6 -0
  30. package/templates/frameworks/react-js/rules/components.md +84 -0
  31. package/templates/frameworks/react-js/rules/hooks.md +98 -0
  32. package/templates/frameworks/react-js/template.json +6 -0
  33. package/templates/frameworks/react-ts/rules/components.md +72 -0
  34. package/templates/frameworks/react-ts/rules/hooks.md +87 -0
  35. package/templates/frameworks/react-ts/rules/state.md +93 -0
  36. package/templates/frameworks/react-ts/template.json +6 -0
  37. package/templates/languages/javascript/rules/patterns.md +126 -0
  38. package/templates/languages/javascript/rules/style.md +92 -0
  39. package/templates/languages/javascript/template.json +6 -0
  40. package/templates/languages/python/rules/dependencies.md +77 -0
  41. package/templates/languages/python/rules/patterns.md +95 -0
  42. package/templates/languages/python/rules/style.md +63 -0
  43. package/templates/languages/python/template.json +6 -0
  44. package/templates/languages/typescript/rules/config.md +95 -0
  45. package/templates/languages/typescript/rules/patterns.md +119 -0
  46. package/templates/languages/typescript/rules/style.md +82 -0
  47. package/templates/languages/typescript/template.json +6 -0
  48. package/templates/universal/commands/commit.md +53 -0
  49. package/templates/universal/commands/debug.md +53 -0
  50. package/templates/universal/commands/document.md +54 -0
  51. package/templates/universal/commands/review.md +45 -0
  52. package/templates/universal/commands/security-review.md +52 -0
  53. package/templates/universal/commands/test.md +46 -0
  54. package/templates/universal/rules/api-design.md +38 -0
  55. package/templates/universal/rules/code-quality.md +40 -0
  56. package/templates/universal/rules/documentation.md +38 -0
  57. package/templates/universal/rules/error-handling.md +37 -0
  58. package/templates/universal/rules/git-workflow.md +39 -0
  59. package/templates/universal/rules/security.md +39 -0
  60. package/templates/universal/rules/testing.md +38 -0
  61. package/templates/universal/template.json +6 -0
  62. package/ui/dist/assets/index-C5apzulu.css +32 -0
  63. package/ui/dist/assets/index-CBNCwCnY.js +489 -0
  64. package/ui/dist/index.html +14 -0
  65. package/ui/server.cjs +2237 -0
  66. package/ui/terminal-server.cjs +160 -0
@@ -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;