ccsetup 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +100 -342
  2. package/bin/create-project.js +1616 -60
  3. package/bin/lib/claudeInterface.js +209 -0
  4. package/bin/lib/contextGenerator.js +287 -0
  5. package/bin/lib/scanner/index.js +28 -0
  6. package/bin/scan.js +367 -0
  7. package/lib/aiAgentSelector.js +155 -0
  8. package/lib/aiMergeHelper.js +112 -0
  9. package/lib/contextGenerator.js +574 -0
  10. package/lib/contextMerger.js +812 -0
  11. package/lib/progressReporter.js +88 -0
  12. package/lib/scanConfig.js +200 -0
  13. package/lib/scanner/fileAnalyzer.js +605 -0
  14. package/lib/scanner/index.js +164 -0
  15. package/lib/scanner/patterns.js +277 -0
  16. package/lib/scanner/projectDetector.js +147 -0
  17. package/lib/templates/README.md +176 -0
  18. package/lib/templates/catalog.js +230 -0
  19. package/lib/templates/filter.js +257 -0
  20. package/lib/templates/index.js +45 -0
  21. package/lib/templates/metadata/agents.json +413 -0
  22. package/lib/templates/metadata-extractor.js +329 -0
  23. package/lib/templates/search.js +356 -0
  24. package/package.json +11 -4
  25. package/template/{agents → .claude/agents}/checker.md +29 -0
  26. package/template/.claude/settings.json +15 -0
  27. package/template/.claude/skills/prd/SKILL.md +343 -0
  28. package/template/.claude/skills/ralph/SKILL.md +339 -0
  29. package/template/CLAUDE.md +39 -21
  30. package/template/CONTRIBUTING.md +37 -0
  31. package/template/GEMINI.md +126 -0
  32. package/template/agents/README.md +15 -171
  33. package/template/docs/ROADMAP.md +0 -36
  34. package/template/docs/agent-orchestration.md +24 -141
  35. package/template/hooks/workflow-selector/index.js +398 -0
  36. package/template/scripts/ralph/CLAUDE.md +174 -0
  37. package/template/scripts/ralph/ralph.sh +127 -0
  38. package/template/tickets/ticket-list.md +17 -68
  39. package/template/agents/ai-engineer.md +0 -31
  40. package/template/agents/api-documenter.md +0 -31
  41. package/template/agents/architect-review.md +0 -42
  42. package/template/agents/backend-architect.md +0 -29
  43. package/template/agents/business-analyst.md +0 -34
  44. package/template/agents/c-pro.md +0 -34
  45. package/template/agents/cloud-architect.md +0 -31
  46. package/template/agents/code-reviewer.md +0 -28
  47. package/template/agents/content-marketer.md +0 -34
  48. package/template/agents/context-manager.md +0 -63
  49. package/template/agents/cpp-pro.md +0 -37
  50. package/template/agents/customer-support.md +0 -34
  51. package/template/agents/data-engineer.md +0 -31
  52. package/template/agents/data-scientist.md +0 -28
  53. package/template/agents/database-admin.md +0 -31
  54. package/template/agents/database-optimizer.md +0 -31
  55. package/template/agents/debugger.md +0 -29
  56. package/template/agents/deployment-engineer.md +0 -31
  57. package/template/agents/devops-troubleshooter.md +0 -31
  58. package/template/agents/dx-optimizer.md +0 -62
  59. package/template/agents/error-detective.md +0 -31
  60. package/template/agents/frontend-developer.md +0 -30
  61. package/template/agents/golang-pro.md +0 -31
  62. package/template/agents/graphql-architect.md +0 -31
  63. package/template/agents/incident-responder.md +0 -73
  64. package/template/agents/javascript-pro.md +0 -34
  65. package/template/agents/legacy-modernizer.md +0 -31
  66. package/template/agents/ml-engineer.md +0 -31
  67. package/template/agents/mlops-engineer.md +0 -56
  68. package/template/agents/mobile-developer.md +0 -31
  69. package/template/agents/network-engineer.md +0 -31
  70. package/template/agents/payment-integration.md +0 -31
  71. package/template/agents/performance-engineer.md +0 -31
  72. package/template/agents/prompt-engineer.md +0 -58
  73. package/template/agents/python-pro.md +0 -31
  74. package/template/agents/quant-analyst.md +0 -31
  75. package/template/agents/risk-manager.md +0 -40
  76. package/template/agents/rust-pro.md +0 -34
  77. package/template/agents/sales-automator.md +0 -34
  78. package/template/agents/search-specialist.md +0 -58
  79. package/template/agents/security-auditor.md +0 -31
  80. package/template/agents/sql-pro.md +0 -34
  81. package/template/agents/terraform-specialist.md +0 -34
  82. package/template/agents/test-automator.md +0 -31
  83. /package/template/{agents → .claude/agents}/backend.md +0 -0
  84. /package/template/{agents → .claude/agents}/blockchain.md +0 -0
  85. /package/template/{agents → .claude/agents}/coder.md +0 -0
  86. /package/template/{agents → .claude/agents}/frontend.md +0 -0
  87. /package/template/{agents → .claude/agents}/planner.md +0 -0
  88. /package/template/{agents → .claude/agents}/researcher.md +0 -0
  89. /package/template/{agents → .claude/agents}/shadcn.md +0 -0
@@ -0,0 +1,812 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const AIMergeHelper = require('./aiMergeHelper');
4
+
5
+ class ContextMerger {
6
+ constructor(existingContent, newContent) {
7
+ this.existingContent = existingContent || '';
8
+ this.newContent = newContent || '';
9
+ this.aiHelper = new AIMergeHelper();
10
+
11
+ try {
12
+ this.isNewContentStructured = this.isStructuredContent(newContent);
13
+ this.isExistingContentStructured = this.isStructuredContent(existingContent);
14
+
15
+ if (this.isExistingContentStructured) {
16
+ this.existingSections = this.parseStructuredSections(existingContent);
17
+ } else {
18
+ this.existingSections = this.parseMarkdownSections(this.existingContent);
19
+ }
20
+
21
+ if (this.isNewContentStructured) {
22
+ this.newSections = this.parseStructuredSections(newContent);
23
+ } else {
24
+ this.newSections = this.parseMarkdownSections(this.newContent);
25
+ }
26
+ } catch (error) {
27
+ console.warn(`Warning: Failed to parse content sections: ${error.message}`);
28
+ // Fallback to simple string parsing
29
+ this.existingSections = this.parseMarkdownSections(this.existingContent);
30
+ this.newSections = this.parseMarkdownSections(this.newContent);
31
+ this.isNewContentStructured = false;
32
+ this.isExistingContentStructured = false;
33
+ }
34
+ }
35
+
36
+ isStructuredContent(content) {
37
+ return typeof content === 'object' && content !== null && !Array.isArray(content);
38
+ }
39
+
40
+ parseMarkdownSections(content) {
41
+ const sections = {};
42
+ const lines = content.split('\n');
43
+ let currentSection = null;
44
+ let currentContent = [];
45
+
46
+ for (const line of lines) {
47
+ const headerMatch = line.match(/^(##)\s+(.+)$/);
48
+ if (headerMatch) {
49
+ const [, hashes, title] = headerMatch;
50
+
51
+ if (currentSection) {
52
+ sections[currentSection] = currentContent.join('\n').trim();
53
+ }
54
+
55
+ currentSection = title.trim();
56
+ currentContent = [];
57
+ } else if (currentSection) {
58
+ currentContent.push(line);
59
+ }
60
+ }
61
+
62
+ if (currentSection) {
63
+ sections[currentSection] = currentContent.join('\n').trim();
64
+ }
65
+
66
+ return sections;
67
+ }
68
+
69
+ parseStructuredSections(structuredContent) {
70
+ if (!this.isStructuredContent(structuredContent)) {
71
+ return {};
72
+ }
73
+
74
+ const sections = {};
75
+
76
+ for (const [sectionName, sectionData] of Object.entries(structuredContent)) {
77
+ if (sectionData && typeof sectionData === 'object' && sectionData.content) {
78
+ sections[sectionName] = {
79
+ content: sectionData.content,
80
+ metadata: sectionData.metadata || {},
81
+ type: sectionData.type || 'text'
82
+ };
83
+ } else if (typeof sectionData === 'string') {
84
+ sections[sectionName] = {
85
+ content: sectionData,
86
+ metadata: {},
87
+ type: 'text'
88
+ };
89
+ }
90
+ }
91
+
92
+ return sections;
93
+ }
94
+
95
+ mergeSection(sectionName, existingSection, newSection, strategy = 'smart') {
96
+ if (!existingSection) {
97
+ return newSection;
98
+ }
99
+
100
+ if (!newSection) {
101
+ return existingSection;
102
+ }
103
+
104
+ const existingData = this.isStructuredContent(existingSection) ? existingSection : { content: existingSection, type: 'text' };
105
+ const newData = this.isStructuredContent(newSection) ? newSection : { content: newSection, type: 'text' };
106
+
107
+ switch (strategy) {
108
+ case 'smart':
109
+ return this.mergeWithSmartLogic(existingData, newData, sectionName);
110
+ case 'replace':
111
+ return newData;
112
+ default:
113
+ return this.mergeWithSmartLogic(existingData, newData, sectionName);
114
+ }
115
+ }
116
+
117
+ mergeWithSmartLogic(existing, newData, sectionName) {
118
+ if (sectionName === 'Tech Stack') {
119
+ return this.mergeTechStack(existing, newData);
120
+ } else if (sectionName === 'Key Commands') {
121
+ return this.mergeCommands(existing, newData);
122
+ } else if (sectionName === 'Project Structure' && existing.type === 'tree' && newData.type === 'tree') {
123
+ return this.mergeProjectStructure(existing, newData);
124
+ } else if (existing.type === 'list' || newData.type === 'list') {
125
+ return this.mergeList(existing, newData);
126
+ }
127
+
128
+ if (newData.metadata && newData.metadata.scanGenerated &&
129
+ (!existing.metadata || !existing.metadata.userGenerated)) {
130
+ return newData;
131
+ }
132
+
133
+ return existing;
134
+ }
135
+
136
+ mergeList(existing, newData) {
137
+ let existingItems = [];
138
+ let newItems = [];
139
+
140
+ if (Array.isArray(existing.content)) {
141
+ existingItems = existing.content;
142
+ } else if (typeof existing.content === 'string') {
143
+ existingItems = this.parseListFromString(existing.content);
144
+ }
145
+
146
+ if (Array.isArray(newData.content)) {
147
+ newItems = newData.content;
148
+ } else if (typeof newData.content === 'string') {
149
+ newItems = this.parseListFromString(newData.content);
150
+ }
151
+
152
+ const combined = [...existingItems];
153
+ for (const item of newItems) {
154
+ if (!this.listContainsItem(combined, item)) {
155
+ combined.push(item);
156
+ }
157
+ }
158
+
159
+ return {
160
+ content: combined,
161
+ type: 'list',
162
+ metadata: { ...existing.metadata, ...newData.metadata }
163
+ };
164
+ }
165
+
166
+ parseListFromString(content) {
167
+ return content.split('\n')
168
+ .map(line => line.replace(/^[-*+]\s*/, '').trim())
169
+ .filter(line => line.length > 0);
170
+ }
171
+
172
+ listContainsItem(list, item) {
173
+ const itemStr = typeof item === 'string' ? item : JSON.stringify(item);
174
+ return list.some(existing => {
175
+ const existingStr = typeof existing === 'string' ? existing : JSON.stringify(existing);
176
+ return existingStr.toLowerCase().includes(itemStr.toLowerCase()) ||
177
+ itemStr.toLowerCase().includes(existingStr.toLowerCase());
178
+ });
179
+ }
180
+
181
+ mergeCommands(existing, newData) {
182
+ const existingCommands = this.parseCommands(existing.content);
183
+ const newCommands = this.parseCommands(newData.content);
184
+
185
+ const merged = { ...existingCommands };
186
+
187
+ for (const [command, description] of Object.entries(newCommands)) {
188
+ if (!merged[command]) {
189
+ merged[command] = description;
190
+ } else if (description.length > merged[command].length) {
191
+ merged[command] = description;
192
+ }
193
+ }
194
+
195
+ const commandList = Object.entries(merged).map(([cmd, desc]) => `- \`${cmd}\` - ${desc}`);
196
+
197
+ return {
198
+ content: commandList.join('\n'),
199
+ type: 'commands',
200
+ metadata: { ...existing.metadata, ...newData.metadata }
201
+ };
202
+ }
203
+
204
+ parseCommands(content) {
205
+ const commands = {};
206
+ if (typeof content === 'string') {
207
+ const lines = content.split('\n');
208
+ for (const line of lines) {
209
+ const match = line.match(/^[-*+]?\s*`([^`]+)`\s*[-–—]\s*(.+)$/);
210
+ if (match) {
211
+ commands[match[1]] = match[2];
212
+ }
213
+ }
214
+ } else if (typeof content === 'object' && content.content) {
215
+ return this.parseCommands(content.content);
216
+ }
217
+ return commands;
218
+ }
219
+
220
+ mergeTechStack(existing, newData) {
221
+ const existingStack = this.parseTechStack(existing.content);
222
+ const newStack = this.parseTechStack(newData.content);
223
+
224
+ const merged = { ...existingStack };
225
+
226
+ for (const [category, technologies] of Object.entries(newStack)) {
227
+ if (!merged[category]) {
228
+ merged[category] = technologies;
229
+ } else {
230
+ const existingTechs = Array.isArray(merged[category]) ? merged[category] : [merged[category]];
231
+ const newTechs = Array.isArray(technologies) ? technologies : [technologies];
232
+
233
+ const combinedTechs = [...existingTechs];
234
+ for (const tech of newTechs) {
235
+ if (!this.listContainsItem(combinedTechs, tech)) {
236
+ combinedTechs.push(tech);
237
+ }
238
+ }
239
+ merged[category] = combinedTechs;
240
+ }
241
+ }
242
+
243
+ let content = '';
244
+ for (const [category, technologies] of Object.entries(merged)) {
245
+ content += `### ${category}\n`;
246
+ const techList = Array.isArray(technologies) ? technologies : [technologies];
247
+ for (const tech of techList) {
248
+ content += `- ${tech}\n`;
249
+ }
250
+ content += '\n';
251
+ }
252
+
253
+ return {
254
+ content: content.trim(),
255
+ type: 'tech-stack',
256
+ metadata: { ...existing.metadata, ...newData.metadata }
257
+ };
258
+ }
259
+
260
+ parseTechStack(content) {
261
+ const stack = {};
262
+ if (typeof content === 'string') {
263
+ const lines = content.split('\n');
264
+ let currentCategory = null;
265
+
266
+ for (const line of lines) {
267
+ const categoryMatch = line.match(/^###\s+(.+)$/);
268
+ if (categoryMatch) {
269
+ currentCategory = categoryMatch[1].trim();
270
+ stack[currentCategory] = [];
271
+ } else if (currentCategory && line.match(/^[-*+]\s+(.+)$/)) {
272
+ const tech = line.replace(/^[-*+]\s+/, '').trim();
273
+ stack[currentCategory].push(tech);
274
+ }
275
+ }
276
+ } else if (typeof content === 'object' && content.content) {
277
+ return this.parseTechStack(content.content);
278
+ }
279
+ return stack;
280
+ }
281
+
282
+ mergeProjectStructure(existing, newData) {
283
+ return {
284
+ content: newData.content,
285
+ type: 'tree',
286
+ metadata: {
287
+ ...existing.metadata,
288
+ ...newData.metadata,
289
+ lastUpdated: new Date().toISOString()
290
+ }
291
+ };
292
+ }
293
+
294
+ detectChanges() {
295
+ const changes = {
296
+ added: [],
297
+ modified: [],
298
+ removed: [],
299
+ unchanged: []
300
+ };
301
+
302
+ const normalizedExisting = this.normalizeSections(this.existingSections);
303
+ const normalizedNew = this.normalizeSections(this.newSections);
304
+
305
+ const newSectionKeys = Object.keys(normalizedNew);
306
+ const existingSectionKeys = Object.keys(normalizedExisting);
307
+
308
+ for (const sectionKey of newSectionKeys) {
309
+ if (!normalizedExisting[sectionKey]) {
310
+ changes.added.push({
311
+ section: sectionKey,
312
+ content: normalizedNew[sectionKey]
313
+ });
314
+ } else if (!this.sectionsEqual(normalizedExisting[sectionKey], normalizedNew[sectionKey])) {
315
+ changes.modified.push({
316
+ section: sectionKey,
317
+ oldContent: normalizedExisting[sectionKey],
318
+ newContent: normalizedNew[sectionKey]
319
+ });
320
+ } else {
321
+ changes.unchanged.push({
322
+ section: sectionKey,
323
+ content: normalizedExisting[sectionKey]
324
+ });
325
+ }
326
+ }
327
+
328
+ for (const sectionKey of existingSectionKeys) {
329
+ if (!normalizedNew[sectionKey]) {
330
+ changes.removed.push({
331
+ section: sectionKey,
332
+ content: normalizedExisting[sectionKey]
333
+ });
334
+ }
335
+ }
336
+
337
+ return changes;
338
+ }
339
+
340
+ normalizeSections(sections) {
341
+ const normalized = {};
342
+ for (const [key, value] of Object.entries(sections)) {
343
+ if (this.isStructuredContent(value)) {
344
+ normalized[key] = value;
345
+ } else {
346
+ normalized[key] = { content: value, type: 'text' };
347
+ }
348
+ }
349
+ return normalized;
350
+ }
351
+
352
+ sectionsEqual(section1, section2) {
353
+ if (typeof section1 === 'string' && typeof section2 === 'string') {
354
+ return section1.trim() === section2.trim();
355
+ }
356
+
357
+ if (this.isStructuredContent(section1) && this.isStructuredContent(section2)) {
358
+ return JSON.stringify(section1.content) === JSON.stringify(section2.content);
359
+ }
360
+
361
+ return false;
362
+ }
363
+
364
+ generateDiff() {
365
+ const changes = this.detectChanges();
366
+ let output = '';
367
+
368
+ if (changes.added.length > 0) {
369
+ output += '📋 New Sections:\n';
370
+ for (const add of changes.added) {
371
+ output += `+ ${add.section}\n`;
372
+ }
373
+ output += '\n';
374
+ }
375
+
376
+ if (changes.modified.length > 0) {
377
+ output += '📝 Modified Sections:\n';
378
+ for (const mod of changes.modified) {
379
+ output += `~ ${mod.section}\n`;
380
+ }
381
+ output += '\n';
382
+ }
383
+
384
+ if (changes.removed.length > 0) {
385
+ output += '🗑️ Sections That Would Be Removed:\n';
386
+ for (const rem of changes.removed) {
387
+ output += `- ${rem.section}\n`;
388
+ }
389
+ output += '\n';
390
+ }
391
+
392
+ if (changes.unchanged.length > 0) {
393
+ output += `✓ ${changes.unchanged.length} sections unchanged\n\n`;
394
+ }
395
+
396
+ return output;
397
+ }
398
+
399
+ async interactiveMerge() {
400
+ try {
401
+ const normalizedExisting = this.normalizeSections(this.existingSections);
402
+ const normalizedNew = this.normalizeSections(this.newSections);
403
+
404
+ const mergedSections = {};
405
+ const allSectionNames = new Set([
406
+ ...Object.keys(normalizedExisting),
407
+ ...Object.keys(normalizedNew)
408
+ ]);
409
+
410
+ const conflicts = [];
411
+
412
+ // First pass: identify conflicts
413
+ for (const sectionName of allSectionNames) {
414
+ const existingSection = normalizedExisting[sectionName];
415
+ const newSection = normalizedNew[sectionName];
416
+
417
+ if (existingSection && newSection && !this.sectionsEqual(existingSection, newSection)) {
418
+ conflicts.push({
419
+ sectionName,
420
+ existing: existingSection,
421
+ new: newSection
422
+ });
423
+ } else if (!existingSection && newSection) {
424
+ // New section - auto-accept
425
+ mergedSections[sectionName] = newSection;
426
+ } else if (existingSection && !newSection) {
427
+ // Section only in existing - preserve
428
+ mergedSections[sectionName] = existingSection;
429
+ } else {
430
+ // Sections are equal
431
+ mergedSections[sectionName] = existingSection;
432
+ }
433
+ }
434
+
435
+ // Interactive resolution for conflicts
436
+ if (conflicts.length > 0) {
437
+ console.log(`\n🔍 Found ${conflicts.length} section(s) with conflicts:\n`);
438
+
439
+ // Dynamic import for inquirer
440
+ const selectModule = await import('@inquirer/select');
441
+ const select = selectModule.default;
442
+
443
+ for (const conflict of conflicts) {
444
+ console.log(`\n${'═'.repeat(60)}`);
445
+ console.log(`📋 Conflict in section: ${conflict.sectionName}`);
446
+ console.log(`${'─'.repeat(60)}\n`);
447
+
448
+ console.log('📄 Your current content:');
449
+ console.log(this.formatSectionForDisplay(conflict.existing, ' '));
450
+
451
+ console.log('\n🔍 New content from scan:');
452
+ console.log(this.formatSectionForDisplay(conflict.new, ' '));
453
+ console.log(`\n${'─'.repeat(60)}`);
454
+
455
+ // Get AI suggestion if Claude Code is available
456
+ const aiSuggestion = await this.aiHelper.analyzeConflict(conflict);
457
+ if (aiSuggestion && aiSuggestion.analysis) {
458
+ console.log('\n🤖 AI Analysis:');
459
+ console.log(` ${aiSuggestion.analysis}`);
460
+ if (aiSuggestion.recommendation) {
461
+ const recommendations = {
462
+ 'keep-existing': '✅ Keep existing',
463
+ 'use-new': '🔄 Use new',
464
+ 'merge-both': '🤝 Merge both',
465
+ 'skip': '❌ Skip'
466
+ };
467
+ console.log(` 💡 Recommendation: ${recommendations[aiSuggestion.recommendation] || aiSuggestion.recommendation}`);
468
+ }
469
+ console.log(`${'─'.repeat(60)}`);
470
+ }
471
+
472
+ const choice = await select({
473
+ message: `How would you like to handle this section?`,
474
+ choices: [
475
+ {
476
+ name: '✅ Keep mine - Use my existing content',
477
+ value: 'keep',
478
+ description: 'Your current content will be preserved unchanged'
479
+ },
480
+ {
481
+ name: '🔄 Use new - Replace with scan results',
482
+ value: 'replace',
483
+ description: 'Replace your content with the newly scanned information'
484
+ },
485
+ {
486
+ name: '🤝 Merge both - Combine intelligently',
487
+ value: 'merge',
488
+ description: 'Attempts to preserve your content while adding new findings'
489
+ },
490
+ {
491
+ name: '❌ Skip - Don\'t include this section',
492
+ value: 'skip',
493
+ description: 'Remove this section entirely from the file'
494
+ }
495
+ ]
496
+ });
497
+
498
+ switch (choice) {
499
+ case 'keep':
500
+ mergedSections[conflict.sectionName] = conflict.existing;
501
+ break;
502
+ case 'replace':
503
+ mergedSections[conflict.sectionName] = conflict.new;
504
+ break;
505
+ case 'merge':
506
+ // Try AI-powered merge first
507
+ const aiMerged = await this.aiHelper.generateMergedContent(conflict);
508
+ if (aiMerged) {
509
+ console.log('\n🤖 Using AI-generated merge...');
510
+ mergedSections[conflict.sectionName] = {
511
+ content: aiMerged,
512
+ type: 'text',
513
+ metadata: { aiMerged: true }
514
+ };
515
+ } else {
516
+ // Fallback to rule-based merge
517
+ mergedSections[conflict.sectionName] = this.mergeSection(
518
+ conflict.sectionName,
519
+ conflict.existing,
520
+ conflict.new,
521
+ 'smart'
522
+ );
523
+ }
524
+ break;
525
+ case 'skip':
526
+ // Don't include this section
527
+ break;
528
+ }
529
+ }
530
+ }
531
+
532
+ if (conflicts.length === 0) {
533
+ console.log('✅ No conflicts found - all new sections will be added automatically.\n');
534
+ }
535
+
536
+ return await this.reconstructContent(mergedSections);
537
+ } catch (error) {
538
+ // Handle specific error cases
539
+ if (error.message && error.message.includes('User force closed')) {
540
+ console.log('\n❌ Interactive merge cancelled by user.');
541
+ console.log('💡 Tip: Your CLAUDE.md remains unchanged. Run the command again when ready.\n');
542
+ throw new Error('Interactive merge cancelled');
543
+ } else if (error.code === 'MODULE_NOT_FOUND') {
544
+ console.error('\n❌ Required module @inquirer/select not found.');
545
+ console.log('💡 Please run: npm install @inquirer/select\n');
546
+ throw new Error('Missing required dependency');
547
+ } else {
548
+ console.warn(`\n⚠️ Interactive merge encountered an error: ${error.message}`);
549
+ console.log('Falling back to smart merge...\n');
550
+ return this.smartMerge('smart');
551
+ }
552
+ }
553
+ }
554
+
555
+ formatSectionForDisplay(section, indent = '') {
556
+ const maxLines = 10;
557
+ let content = '';
558
+
559
+ if (typeof section === 'string') {
560
+ content = section;
561
+ } else if (section.content) {
562
+ content = this.extractContent(section);
563
+ }
564
+
565
+ const lines = content.split('\n');
566
+ const displayLines = lines.slice(0, maxLines).map(line => indent + line);
567
+
568
+ if (lines.length > maxLines) {
569
+ return displayLines.join('\n') + `\n${indent}... (${lines.length - maxLines} more lines)`;
570
+ }
571
+ return displayLines.join('\n');
572
+ }
573
+
574
+ async smartMerge(strategy = 'smart') {
575
+ try {
576
+ const normalizedExisting = this.normalizeSections(this.existingSections);
577
+ const normalizedNew = this.normalizeSections(this.newSections);
578
+
579
+ const mergedSections = {};
580
+ const allSectionNames = new Set([
581
+ ...Object.keys(normalizedExisting),
582
+ ...Object.keys(normalizedNew)
583
+ ]);
584
+
585
+ for (const sectionName of allSectionNames) {
586
+ const existingSection = normalizedExisting[sectionName];
587
+ const newSection = normalizedNew[sectionName];
588
+
589
+ mergedSections[sectionName] = this.mergeSection(
590
+ sectionName,
591
+ existingSection,
592
+ newSection,
593
+ strategy
594
+ );
595
+ }
596
+
597
+ return await this.reconstructContent(mergedSections);
598
+ } catch (error) {
599
+ console.warn(`Warning: Smart merge failed: ${error.message}`);
600
+ // Fallback to simple content replacement
601
+ if (this.isNewContentStructured && this.newContent.formatForClaude) {
602
+ return this.newContent.formatForClaude();
603
+ }
604
+ return this.newContent || this.existingContent;
605
+ }
606
+ }
607
+
608
+ async merge(strategy = 'smart') {
609
+ if (strategy === 'interactive') {
610
+ return await this.interactiveMerge();
611
+ }
612
+ if (strategy === 'replace') {
613
+ return this.smartMerge('replace');
614
+ }
615
+ return this.smartMerge('smart');
616
+ }
617
+
618
+ shouldUpdateSection(sectionName, oldContent, newContent) {
619
+ const autoUpdateSections = [
620
+ 'Tech Stack',
621
+ 'Key Commands',
622
+ 'Project Structure',
623
+ 'Architecture & Patterns',
624
+ 'Scan Information'
625
+ ];
626
+
627
+ return autoUpdateSections.includes(sectionName);
628
+ }
629
+
630
+ async reconstructContent(sections) {
631
+ // Always return markdown for CLI integration unless explicitly requested otherwise
632
+ if (this.isNewContentStructured && this.preserveStructuredFormat) {
633
+ return this.reconstructStructuredContent(sections);
634
+ } else {
635
+ return await this.reconstructMarkdown(sections);
636
+ }
637
+ }
638
+
639
+ reconstructStructuredContent(sections) {
640
+ const structured = {};
641
+ for (const [sectionName, sectionData] of Object.entries(sections)) {
642
+ if (this.isStructuredContent(sectionData)) {
643
+ structured[sectionName] = sectionData;
644
+ } else {
645
+ structured[sectionName] = {
646
+ content: sectionData,
647
+ type: 'text',
648
+ metadata: {}
649
+ };
650
+ }
651
+ }
652
+ return structured;
653
+ }
654
+
655
+ async reconstructMarkdown(sections) {
656
+ let content = '';
657
+
658
+ const sectionOrder = [
659
+ 'Project Overview',
660
+ 'Tech Stack',
661
+ 'Key Commands',
662
+ 'Project Structure',
663
+ 'Architecture & Patterns',
664
+ 'Important Context',
665
+ 'Scan Information'
666
+ ];
667
+
668
+ const usedSections = new Set();
669
+
670
+ for (const sectionName of sectionOrder) {
671
+ if (sections[sectionName]) {
672
+ const sectionContent = this.extractContent(sections[sectionName]);
673
+ content += `## ${sectionName}\n${sectionContent}\n\n`;
674
+ usedSections.add(sectionName);
675
+ }
676
+ }
677
+
678
+ for (const [sectionName, sectionData] of Object.entries(sections)) {
679
+ if (!usedSections.has(sectionName)) {
680
+ const sectionContent = this.extractContent(sectionData);
681
+ content += `## ${sectionName}\n${sectionContent}\n\n`;
682
+ }
683
+ }
684
+
685
+ return content.trim();
686
+ }
687
+
688
+ extractContent(sectionData) {
689
+ if (typeof sectionData === 'string') {
690
+ return sectionData;
691
+ } else if (this.isStructuredContent(sectionData) && sectionData.content) {
692
+ const content = sectionData.content;
693
+
694
+ if (Array.isArray(content)) {
695
+ return content.map(item => {
696
+ if (typeof item === 'object' && item.path && item.description) {
697
+ const prefix = item.type === 'directory' ? '📁' : '📄';
698
+ return `- ${prefix} \`/${item.path}\` - ${item.description}`;
699
+ } else if (typeof item === 'object' && item.command && item.description) {
700
+ return `- \`${item.command}\` - ${item.description}`;
701
+ }
702
+ return `- ${item}`;
703
+ }).join('\n');
704
+ } else if (typeof content === 'object') {
705
+ // Handle structured objects like tech stack, commands
706
+ let result = '';
707
+ for (const [key, value] of Object.entries(content)) {
708
+ if (key === 'scanDate' || key === 'duration' || key === 'filesAnalyzed') {
709
+ // Special handling for scan information
710
+ if (key === 'scanDate') result += `- Scanned on: ${value}\n`;
711
+ else if (key === 'duration') result += `- Scan duration: ${value}\n`;
712
+ else if (key === 'filesAnalyzed') result += `- Files analyzed: ${value}\n`;
713
+ } else if (Array.isArray(value) && value.length > 0) {
714
+ result += `- **${key.charAt(0).toUpperCase() + key.slice(1)}**: ${value.join(', ')}\n`;
715
+ } else if (typeof value === 'string' && value.length > 0) {
716
+ result += `- **${key.charAt(0).toUpperCase() + key.slice(1)}**: ${value}\n`;
717
+ } else if (typeof value === 'object') {
718
+ // Handle nested objects (like commands grouped by category)
719
+ for (const [subKey, subValue] of Object.entries(value)) {
720
+ if (Array.isArray(subValue)) {
721
+ subValue.forEach(item => {
722
+ if (typeof item === 'object' && item.command && item.description) {
723
+ result += `- \`${item.command}\` - ${item.description}\n`;
724
+ }
725
+ });
726
+ }
727
+ }
728
+ }
729
+ }
730
+ return result.trim();
731
+ }
732
+ return String(content);
733
+ }
734
+ return '';
735
+ }
736
+
737
+ async updateFile(filePath, strategy = 'smart', dryRun = false) {
738
+ const mergedContent = await this.merge(strategy);
739
+
740
+ if (dryRun) {
741
+ console.log('\n📄 Merged content preview:');
742
+ console.log('━'.repeat(60));
743
+ if (this.isStructuredContent(mergedContent)) {
744
+ console.log(JSON.stringify(mergedContent, null, 2));
745
+ } else {
746
+ console.log(mergedContent);
747
+ }
748
+ console.log('━'.repeat(60));
749
+ return mergedContent;
750
+ }
751
+
752
+ if (this.isStructuredContent(mergedContent)) {
753
+ return mergedContent;
754
+ }
755
+
756
+ const existingHeader = this.extractHeaderContent();
757
+ const fullContent = existingHeader + '\n\n' + mergedContent;
758
+
759
+ fs.writeFileSync(filePath, fullContent, 'utf8');
760
+ return fullContent;
761
+ }
762
+
763
+ extractHeaderContent() {
764
+ const lines = this.existingContent.split('\n');
765
+ const headerLines = [];
766
+
767
+ for (const line of lines) {
768
+ if (line.match(/^##\s+/)) {
769
+ break;
770
+ }
771
+ headerLines.push(line);
772
+ }
773
+
774
+ return headerLines.join('\n').trim();
775
+ }
776
+
777
+ hasConflicts() {
778
+ const changes = this.detectChanges();
779
+ return changes.modified.length > 0 || changes.removed.length > 0;
780
+ }
781
+
782
+ getChangesSummary() {
783
+ const changes = this.detectChanges();
784
+ return {
785
+ added: changes.added.length,
786
+ modified: changes.modified.length,
787
+ removed: changes.removed.length,
788
+ unchanged: changes.unchanged.length,
789
+ hasChanges: changes.added.length > 0 || changes.modified.length > 0 || changes.removed.length > 0
790
+ };
791
+ }
792
+
793
+ static async mergeContextIntoFile(filePath, newContext, options = {}) {
794
+ const { strategy = 'smart', dryRun = false, backup = true } = options;
795
+
796
+ let existingContent = '';
797
+ if (fs.existsSync(filePath)) {
798
+ existingContent = fs.readFileSync(filePath, 'utf8');
799
+
800
+ if (backup && !dryRun) {
801
+ const backupPath = `${filePath}.backup.${Date.now()}`;
802
+ fs.writeFileSync(backupPath, existingContent, 'utf8');
803
+ console.log(`📋 Created backup: ${path.basename(backupPath)}`);
804
+ }
805
+ }
806
+
807
+ const merger = new ContextMerger(existingContent, newContext);
808
+ return await merger.updateFile(filePath, strategy, dryRun);
809
+ }
810
+ }
811
+
812
+ module.exports = ContextMerger;