create-universal-ai-context 2.0.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,741 @@
1
+ /**
2
+ * AI Context Engineering - Documentation Discovery Module
3
+ *
4
+ * Scans for existing AI context files and documentation before initialization.
5
+ * Detects which AI tools are already configured and extracts values from existing docs.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { glob } = require('glob');
11
+
12
+ /**
13
+ * AI tool detection signatures
14
+ */
15
+ const AI_TOOL_SIGNATURES = {
16
+ claude: {
17
+ v1: {
18
+ directory: '.claude',
19
+ entryFile: 'CLAUDE.md',
20
+ markers: ['.claude/agents/', '.claude/commands/', '.claude/context/']
21
+ },
22
+ v2: {
23
+ directory: '.ai-context',
24
+ entryFile: 'AI_CONTEXT.md',
25
+ markers: ['.ai-context/agents/', '.ai-context/commands/', '.ai-context/context/']
26
+ }
27
+ },
28
+ copilot: {
29
+ paths: ['.github/copilot-instructions.md'],
30
+ markers: ['copilot-instructions']
31
+ },
32
+ cline: {
33
+ paths: ['.clinerules'],
34
+ markers: ['.clinerules']
35
+ },
36
+ antigravity: {
37
+ paths: ['.agent/'],
38
+ markers: ['.agent/knowledge/', '.agent/config/', '.agent/rules/']
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Common documentation locations
44
+ */
45
+ const COMMON_DOC_PATTERNS = {
46
+ readme: ['README.md', 'readme.md', 'README.markdown', 'docs/README.md'],
47
+ architecture: ['ARCHITECTURE.md', 'docs/ARCHITECTURE.md', 'docs/architecture.md', 'DESIGN.md'],
48
+ contributing: ['CONTRIBUTING.md', 'docs/CONTRIBUTING.md'],
49
+ changelog: ['CHANGELOG.md', 'HISTORY.md', 'CHANGES.md'],
50
+ docsDir: ['docs/', 'documentation/', 'doc/']
51
+ };
52
+
53
+ /**
54
+ * Patterns to extract filled placeholder values from existing docs
55
+ */
56
+ const VALUE_EXTRACTION_PATTERNS = {
57
+ PROJECT_NAME: [
58
+ /\*\*Project(?:\s*Name)?:\*\*\s*(.+?)(?:\n|$)/i,
59
+ /^#\s+(.+?)(?:\n|$)/m
60
+ ],
61
+ PROJECT_DESCRIPTION: [
62
+ /\*\*(?:Platform|Description):\*\*\s*(.+?)(?:\n|$)/i,
63
+ /^#[^#].*?\n\n(.+?)(?:\n\n|$)/ms
64
+ ],
65
+ TECH_STACK: [
66
+ /\*\*Tech Stack:\*\*\s*(.+?)(?:\n|$)/i,
67
+ /(?:built with|using|technologies?):\s*(.+?)(?:\n|$)/i
68
+ ],
69
+ PRODUCTION_URL: [
70
+ /\*\*(?:Domain|URL|Production):\*\*\s*(.+?)(?:\n|$)/i,
71
+ /https?:\/\/[^\s\)]+/
72
+ ],
73
+ API_URL: [
74
+ /\*\*API:\*\*\s*(.+?)(?:\n|$)/i
75
+ ],
76
+ REPO_URL: [
77
+ /\*\*Repo(?:sitory)?:\*\*\s*(.+?)(?:\n|$)/i,
78
+ /github\.com\/[\w\-]+\/[\w\-]+/
79
+ ],
80
+ INSTALL_COMMAND: [
81
+ /```(?:bash|sh)?\s*\n([^`]*(?:npm install|pip install|cargo build|go mod)[^`]*)/i
82
+ ],
83
+ TEST_COMMAND: [
84
+ /```(?:bash|sh)?\s*\n([^`]*(?:npm test|pytest|cargo test|go test)[^`]*)/i
85
+ ]
86
+ };
87
+
88
+ /**
89
+ * Main entry point - discover all existing documentation
90
+ * @param {string} projectRoot - Project root directory
91
+ * @returns {Promise<object>} Discovery result
92
+ */
93
+ async function discoverExistingDocs(projectRoot) {
94
+ const result = {
95
+ hasExistingDocs: false,
96
+ tools: {
97
+ claude: null,
98
+ copilot: null,
99
+ cline: null,
100
+ antigravity: null
101
+ },
102
+ commonDocs: {
103
+ readme: null,
104
+ architecture: null,
105
+ contributing: null,
106
+ changelog: null,
107
+ docsDir: null
108
+ },
109
+ extractedValues: {},
110
+ detectedPatterns: {
111
+ techStack: null,
112
+ projectName: null,
113
+ projectDescription: null,
114
+ workflows: [],
115
+ architecture: null
116
+ },
117
+ conflicts: [],
118
+ recommendations: []
119
+ };
120
+
121
+ // 1. Detect AI tools
122
+ result.tools = detectAITools(projectRoot);
123
+
124
+ // 2. Find common docs
125
+ result.commonDocs = await findCommonDocs(projectRoot);
126
+
127
+ // 3. Check if any docs exist
128
+ result.hasExistingDocs =
129
+ Object.values(result.tools).some(t => t?.exists) ||
130
+ Object.values(result.commonDocs).some(d => d !== null);
131
+
132
+ if (!result.hasExistingDocs) {
133
+ return result;
134
+ }
135
+
136
+ // 4. Parse and extract values from each source
137
+ const valueSources = [];
138
+
139
+ // Extract from Claude context file (v1 or v2)
140
+ if (result.tools.claude?.exists) {
141
+ const claudeValues = parseContextFile(result.tools.claude.entryPath);
142
+ if (claudeValues) {
143
+ valueSources.push({ source: 'claude', values: claudeValues });
144
+ }
145
+ }
146
+
147
+ // Extract from README
148
+ if (result.commonDocs.readme) {
149
+ const readmeValues = parseReadme(result.commonDocs.readme.path);
150
+ if (readmeValues) {
151
+ valueSources.push({ source: 'readme', values: readmeValues });
152
+ }
153
+ }
154
+
155
+ // Extract from Copilot instructions
156
+ if (result.tools.copilot?.exists) {
157
+ const copilotValues = parseCopilotInstructions(result.tools.copilot.path);
158
+ if (copilotValues) {
159
+ valueSources.push({ source: 'copilot', values: copilotValues });
160
+ }
161
+ }
162
+
163
+ // Extract from Cline rules
164
+ if (result.tools.cline?.exists) {
165
+ const clineValues = parseClinerules(result.tools.cline.path);
166
+ if (clineValues) {
167
+ valueSources.push({ source: 'cline', values: clineValues });
168
+ }
169
+ }
170
+
171
+ // 5. Merge extracted values, tracking conflicts
172
+ const { merged, conflicts } = mergeExtractedValues(valueSources);
173
+ result.extractedValues = merged;
174
+ result.conflicts = conflicts;
175
+
176
+ // 6. Generate recommendations
177
+ result.recommendations = calculateRecommendations(result);
178
+
179
+ return result;
180
+ }
181
+
182
+ /**
183
+ * Detect which AI tools have existing context
184
+ * @param {string} projectRoot - Project root directory
185
+ * @returns {object} Tool detection results
186
+ */
187
+ function detectAITools(projectRoot) {
188
+ const tools = {
189
+ claude: null,
190
+ copilot: null,
191
+ cline: null,
192
+ antigravity: null
193
+ };
194
+
195
+ // Detect Claude (v1 and v2)
196
+ const claudeV1Dir = path.join(projectRoot, AI_TOOL_SIGNATURES.claude.v1.directory);
197
+ const claudeV1File = path.join(projectRoot, AI_TOOL_SIGNATURES.claude.v1.entryFile);
198
+ const claudeV2Dir = path.join(projectRoot, AI_TOOL_SIGNATURES.claude.v2.directory);
199
+ const claudeV2File = path.join(projectRoot, AI_TOOL_SIGNATURES.claude.v2.entryFile);
200
+
201
+ const hasV1Dir = fs.existsSync(claudeV1Dir);
202
+ const hasV1File = fs.existsSync(claudeV1File);
203
+ const hasV2Dir = fs.existsSync(claudeV2Dir);
204
+ const hasV2File = fs.existsSync(claudeV2File);
205
+
206
+ if (hasV1Dir || hasV1File || hasV2Dir || hasV2File) {
207
+ // Prefer v2 if both exist
208
+ const version = (hasV2Dir || hasV2File) ? 'v2' : 'v1';
209
+ const dirPath = version === 'v2' ? claudeV2Dir : claudeV1Dir;
210
+ const entryPath = version === 'v2' ? claudeV2File : claudeV1File;
211
+
212
+ tools.claude = {
213
+ exists: true,
214
+ version,
215
+ dirPath: fs.existsSync(dirPath) ? dirPath : null,
216
+ entryPath: fs.existsSync(entryPath) ? entryPath : null,
217
+ hasV1: hasV1Dir || hasV1File,
218
+ hasV2: hasV2Dir || hasV2File,
219
+ needsMigration: (hasV1Dir || hasV1File) && !(hasV2Dir || hasV2File)
220
+ };
221
+ }
222
+
223
+ // Detect GitHub Copilot
224
+ for (const relPath of AI_TOOL_SIGNATURES.copilot.paths) {
225
+ const fullPath = path.join(projectRoot, relPath);
226
+ if (fs.existsSync(fullPath)) {
227
+ tools.copilot = {
228
+ exists: true,
229
+ path: fullPath,
230
+ relativePath: relPath
231
+ };
232
+ break;
233
+ }
234
+ }
235
+
236
+ // Detect Cline
237
+ for (const relPath of AI_TOOL_SIGNATURES.cline.paths) {
238
+ const fullPath = path.join(projectRoot, relPath);
239
+ if (fs.existsSync(fullPath)) {
240
+ tools.cline = {
241
+ exists: true,
242
+ path: fullPath,
243
+ relativePath: relPath
244
+ };
245
+ break;
246
+ }
247
+ }
248
+
249
+ // Detect Antigravity
250
+ for (const relPath of AI_TOOL_SIGNATURES.antigravity.paths) {
251
+ const fullPath = path.join(projectRoot, relPath);
252
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
253
+ tools.antigravity = {
254
+ exists: true,
255
+ path: fullPath,
256
+ relativePath: relPath
257
+ };
258
+ break;
259
+ }
260
+ }
261
+
262
+ return tools;
263
+ }
264
+
265
+ /**
266
+ * Find common documentation files
267
+ * @param {string} projectRoot - Project root directory
268
+ * @returns {Promise<object>} Common docs found
269
+ */
270
+ async function findCommonDocs(projectRoot) {
271
+ const docs = {
272
+ readme: null,
273
+ architecture: null,
274
+ contributing: null,
275
+ changelog: null,
276
+ docsDir: null
277
+ };
278
+
279
+ // Find README
280
+ for (const relPath of COMMON_DOC_PATTERNS.readme) {
281
+ const fullPath = path.join(projectRoot, relPath);
282
+ if (fs.existsSync(fullPath)) {
283
+ docs.readme = { path: fullPath, relativePath: relPath };
284
+ break;
285
+ }
286
+ }
287
+
288
+ // Find Architecture doc
289
+ for (const relPath of COMMON_DOC_PATTERNS.architecture) {
290
+ const fullPath = path.join(projectRoot, relPath);
291
+ if (fs.existsSync(fullPath)) {
292
+ docs.architecture = { path: fullPath, relativePath: relPath };
293
+ break;
294
+ }
295
+ }
296
+
297
+ // Find Contributing doc
298
+ for (const relPath of COMMON_DOC_PATTERNS.contributing) {
299
+ const fullPath = path.join(projectRoot, relPath);
300
+ if (fs.existsSync(fullPath)) {
301
+ docs.contributing = { path: fullPath, relativePath: relPath };
302
+ break;
303
+ }
304
+ }
305
+
306
+ // Find Changelog
307
+ for (const relPath of COMMON_DOC_PATTERNS.changelog) {
308
+ const fullPath = path.join(projectRoot, relPath);
309
+ if (fs.existsSync(fullPath)) {
310
+ docs.changelog = { path: fullPath, relativePath: relPath };
311
+ break;
312
+ }
313
+ }
314
+
315
+ // Find docs directory
316
+ for (const relPath of COMMON_DOC_PATTERNS.docsDir) {
317
+ const fullPath = path.join(projectRoot, relPath);
318
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
319
+ // Count markdown files in docs directory
320
+ try {
321
+ const mdFiles = await glob('**/*.md', { cwd: fullPath, nodir: true });
322
+ docs.docsDir = {
323
+ path: fullPath,
324
+ relativePath: relPath,
325
+ fileCount: mdFiles.length,
326
+ files: mdFiles.slice(0, 10) // First 10 for preview
327
+ };
328
+ } catch {
329
+ docs.docsDir = { path: fullPath, relativePath: relPath, fileCount: 0 };
330
+ }
331
+ break;
332
+ }
333
+ }
334
+
335
+ return docs;
336
+ }
337
+
338
+ /**
339
+ * Parse existing AI_CONTEXT.md/CLAUDE.md to extract values
340
+ * @param {string} filePath - Path to context file
341
+ * @returns {object|null} Extracted values
342
+ */
343
+ function parseContextFile(filePath) {
344
+ if (!filePath || !fs.existsSync(filePath)) {
345
+ return null;
346
+ }
347
+
348
+ try {
349
+ const content = fs.readFileSync(filePath, 'utf-8');
350
+ return extractValuesFromContent(content);
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Extract project info from README.md
358
+ * @param {string} filePath - Path to README
359
+ * @returns {object|null} Extracted project info
360
+ */
361
+ function parseReadme(filePath) {
362
+ if (!filePath || !fs.existsSync(filePath)) {
363
+ return null;
364
+ }
365
+
366
+ try {
367
+ const content = fs.readFileSync(filePath, 'utf-8');
368
+ const values = {};
369
+
370
+ // Extract title as project name (first h1)
371
+ const titleMatch = content.match(/^#\s+(.+?)(?:\n|$)/m);
372
+ if (titleMatch) {
373
+ // Remove badges and links from title
374
+ const cleanTitle = titleMatch[1].replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, '').trim();
375
+ if (cleanTitle && !cleanTitle.match(/\{\{.*?\}\}/)) {
376
+ values.PROJECT_NAME = cleanTitle;
377
+ }
378
+ }
379
+
380
+ // Extract description (first paragraph after title)
381
+ const descMatch = content.match(/^#[^#].*?\n\n(.+?)(?:\n\n|$)/ms);
382
+ if (descMatch && descMatch[1].length < 500) {
383
+ const cleanDesc = descMatch[1].trim();
384
+ if (cleanDesc && !cleanDesc.match(/\{\{.*?\}\}/) && !cleanDesc.startsWith('![')) {
385
+ values.PROJECT_DESCRIPTION = cleanDesc;
386
+ }
387
+ }
388
+
389
+ // Extract repo URL from badges or links
390
+ const repoMatch = content.match(/github\.com\/([\w\-]+\/[\w\-]+)/);
391
+ if (repoMatch) {
392
+ values.REPO_URL = `https://github.com/${repoMatch[1]}`;
393
+ }
394
+
395
+ return Object.keys(values).length > 0 ? values : null;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Parse .clinerules for existing configurations
403
+ * @param {string} filePath - Path to .clinerules
404
+ * @returns {object|null} Cline configurations
405
+ */
406
+ function parseClinerules(filePath) {
407
+ if (!filePath || !fs.existsSync(filePath)) {
408
+ return null;
409
+ }
410
+
411
+ try {
412
+ const content = fs.readFileSync(filePath, 'utf-8');
413
+ return extractValuesFromContent(content);
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Parse copilot-instructions.md for existing context
421
+ * @param {string} filePath - Path to copilot instructions
422
+ * @returns {object|null} Copilot configurations
423
+ */
424
+ function parseCopilotInstructions(filePath) {
425
+ if (!filePath || !fs.existsSync(filePath)) {
426
+ return null;
427
+ }
428
+
429
+ try {
430
+ const content = fs.readFileSync(filePath, 'utf-8');
431
+ return extractValuesFromContent(content);
432
+ } catch {
433
+ return null;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Extract values from markdown content using patterns
439
+ * @param {string} content - File content
440
+ * @returns {object} Map of placeholder name to extracted value
441
+ */
442
+ function extractValuesFromContent(content) {
443
+ const values = {};
444
+
445
+ for (const [placeholder, patterns] of Object.entries(VALUE_EXTRACTION_PATTERNS)) {
446
+ for (const pattern of patterns) {
447
+ const match = content.match(pattern);
448
+ if (match && match[1]) {
449
+ const value = match[1].trim();
450
+ // Skip if it's still a placeholder
451
+ if (!value.match(/\{\{[A-Z_]+\}\}/) && value.length < 500) {
452
+ values[placeholder] = value;
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ // Track unfilled placeholders
460
+ const unfilledPattern = /\{\{([A-Z_]+)\}\}/g;
461
+ const unfilled = [];
462
+ let match;
463
+ while ((match = unfilledPattern.exec(content)) !== null) {
464
+ unfilled.push(match[1]);
465
+ }
466
+ if (unfilled.length > 0) {
467
+ values._unfilledPlaceholders = [...new Set(unfilled)];
468
+ }
469
+
470
+ return Object.keys(values).length > 0 ? values : null;
471
+ }
472
+
473
+ /**
474
+ * Merge extracted values from multiple sources, detecting conflicts
475
+ * @param {Array} sources - Array of { source, values } objects
476
+ * @returns {object} { merged, conflicts }
477
+ */
478
+ function mergeExtractedValues(sources) {
479
+ const merged = {};
480
+ const conflicts = [];
481
+ const seenKeys = {};
482
+
483
+ for (const { source, values } of sources) {
484
+ if (!values) continue;
485
+
486
+ for (const [key, value] of Object.entries(values)) {
487
+ if (key.startsWith('_')) continue; // Skip internal keys
488
+
489
+ if (seenKeys[key]) {
490
+ // Check for conflict
491
+ if (seenKeys[key].value !== value) {
492
+ conflicts.push({
493
+ key,
494
+ existingValue: seenKeys[key].value,
495
+ existingSource: seenKeys[key].source,
496
+ newValue: value,
497
+ newSource: source
498
+ });
499
+ }
500
+ } else {
501
+ merged[key] = value;
502
+ seenKeys[key] = { value, source };
503
+ }
504
+ }
505
+ }
506
+
507
+ return { merged, conflicts };
508
+ }
509
+
510
+ /**
511
+ * Calculate recommendations based on discovery results
512
+ * @param {object} discovery - Discovery results
513
+ * @returns {Array} Recommendations
514
+ */
515
+ function calculateRecommendations(discovery) {
516
+ const recommendations = [];
517
+
518
+ // Check for v1 → v2 migration
519
+ if (discovery.tools.claude?.needsMigration) {
520
+ recommendations.push({
521
+ type: 'migration',
522
+ priority: 'high',
523
+ message: 'Claude context v1.x detected. Migration to v2.0 recommended.',
524
+ action: 'Run with --mode merge to migrate and preserve customizations'
525
+ });
526
+ }
527
+
528
+ // Check for multiple AI tools
529
+ const existingTools = Object.entries(discovery.tools)
530
+ .filter(([_, t]) => t?.exists)
531
+ .map(([name, _]) => name);
532
+
533
+ if (existingTools.length > 1) {
534
+ recommendations.push({
535
+ type: 'multi-tool',
536
+ priority: 'info',
537
+ message: `Multiple AI tool configs found: ${existingTools.join(', ')}`,
538
+ action: 'Existing configs will be preserved unless --mode overwrite is used'
539
+ });
540
+ }
541
+
542
+ // Check for conflicts
543
+ if (discovery.conflicts.length > 0) {
544
+ recommendations.push({
545
+ type: 'conflicts',
546
+ priority: 'medium',
547
+ message: `${discovery.conflicts.length} value conflict(s) detected between sources`,
548
+ action: 'Use --mode interactive to resolve conflicts manually'
549
+ });
550
+ }
551
+
552
+ // Check for unfilled placeholders in existing docs
553
+ const totalExtracted = Object.keys(discovery.extractedValues).length;
554
+ if (totalExtracted > 0) {
555
+ recommendations.push({
556
+ type: 'extracted',
557
+ priority: 'info',
558
+ message: `Extracted ${totalExtracted} values from existing documentation`,
559
+ action: 'These values will be preserved in merge mode'
560
+ });
561
+ }
562
+
563
+ return recommendations;
564
+ }
565
+
566
+ /**
567
+ * Generate user prompts for handling existing docs
568
+ * @param {object} discovery - Discovery results
569
+ * @returns {Array} Enquirer prompt configurations
570
+ */
571
+ function generateDiscoveryPrompts(discovery) {
572
+ if (!discovery.hasExistingDocs) {
573
+ return [];
574
+ }
575
+
576
+ const prompts = [];
577
+
578
+ // Build summary of what was found
579
+ const foundItems = [];
580
+
581
+ if (discovery.tools.claude?.exists) {
582
+ const v = discovery.tools.claude.version;
583
+ foundItems.push(`Claude context (${v})`);
584
+ }
585
+ if (discovery.tools.copilot?.exists) {
586
+ foundItems.push('GitHub Copilot');
587
+ }
588
+ if (discovery.tools.cline?.exists) {
589
+ foundItems.push('Cline');
590
+ }
591
+ if (discovery.tools.antigravity?.exists) {
592
+ foundItems.push('Antigravity');
593
+ }
594
+ if (discovery.commonDocs.readme) {
595
+ foundItems.push('README.md');
596
+ }
597
+ if (discovery.commonDocs.docsDir) {
598
+ foundItems.push(`docs/ (${discovery.commonDocs.docsDir.fileCount} files)`);
599
+ }
600
+
601
+ // Main strategy prompt
602
+ prompts.push({
603
+ type: 'select',
604
+ name: 'existingDocsStrategy',
605
+ message: `Found existing documentation: ${foundItems.join(', ')}. How to proceed?`,
606
+ choices: [
607
+ {
608
+ name: 'merge',
609
+ message: 'Merge: Use existing docs as base, add new structure (recommended)',
610
+ hint: 'Preserves your customizations'
611
+ },
612
+ {
613
+ name: 'fresh',
614
+ message: 'Fresh: Start fresh but import key values',
615
+ hint: 'New structure, keeps extracted values'
616
+ },
617
+ {
618
+ name: 'overwrite',
619
+ message: 'Overwrite: Replace everything with new templates',
620
+ hint: 'Warning: existing customizations will be lost'
621
+ },
622
+ {
623
+ name: 'skip',
624
+ message: 'Skip: Cancel initialization',
625
+ hint: 'No changes will be made'
626
+ }
627
+ ],
628
+ initial: 0
629
+ });
630
+
631
+ // If conflicts detected, add conflict resolution prompt
632
+ if (discovery.conflicts.length > 0) {
633
+ prompts.push({
634
+ type: 'select',
635
+ name: 'conflictResolution',
636
+ message: `Found ${discovery.conflicts.length} conflicting value(s). Which source should take priority?`,
637
+ choices: [
638
+ { name: 'existing', message: 'Keep existing values (from older docs)' },
639
+ { name: 'detected', message: 'Use newly detected values' },
640
+ { name: 'ask', message: 'Ask for each conflict' }
641
+ ],
642
+ skip() {
643
+ return this.state.answers.existingDocsStrategy === 'overwrite' ||
644
+ this.state.answers.existingDocsStrategy === 'skip';
645
+ }
646
+ });
647
+ }
648
+
649
+ return prompts;
650
+ }
651
+
652
+ /**
653
+ * Format discovery summary for display
654
+ * @param {object} discovery - Discovery results
655
+ * @returns {string} Formatted summary string
656
+ */
657
+ function formatDiscoverySummary(discovery) {
658
+ const lines = [];
659
+
660
+ // AI Tools
661
+ if (discovery.tools.claude?.exists) {
662
+ const v = discovery.tools.claude.version;
663
+ const migration = discovery.tools.claude.needsMigration ? ' (needs migration)' : '';
664
+ lines.push(` Claude ${v}${migration}`);
665
+ }
666
+ if (discovery.tools.copilot?.exists) {
667
+ lines.push(` GitHub Copilot: ${discovery.tools.copilot.relativePath}`);
668
+ }
669
+ if (discovery.tools.cline?.exists) {
670
+ lines.push(` Cline: ${discovery.tools.cline.relativePath}`);
671
+ }
672
+ if (discovery.tools.antigravity?.exists) {
673
+ lines.push(` Antigravity: ${discovery.tools.antigravity.relativePath}`);
674
+ }
675
+
676
+ // Common docs
677
+ if (discovery.commonDocs.readme) {
678
+ lines.push(` README: ${discovery.commonDocs.readme.relativePath}`);
679
+ }
680
+ if (discovery.commonDocs.docsDir) {
681
+ lines.push(` Docs: ${discovery.commonDocs.docsDir.relativePath} (${discovery.commonDocs.docsDir.fileCount} files)`);
682
+ }
683
+
684
+ // Extracted values
685
+ const valueCount = Object.keys(discovery.extractedValues).length;
686
+ if (valueCount > 0) {
687
+ lines.push(` Extracted ${valueCount} value(s) from existing docs`);
688
+ }
689
+
690
+ // Conflicts
691
+ if (discovery.conflicts.length > 0) {
692
+ lines.push(` ${discovery.conflicts.length} conflict(s) between sources`);
693
+ }
694
+
695
+ return lines.join('\n');
696
+ }
697
+
698
+ /**
699
+ * Build merged values from discovery and chosen strategy
700
+ * @param {object} discovery - Discovery results
701
+ * @param {string} strategy - 'merge' | 'fresh' | 'overwrite'
702
+ * @param {object} conflictResolutions - Optional conflict resolutions
703
+ * @returns {object} Merged placeholder values
704
+ */
705
+ function buildMergedValues(discovery, strategy, conflictResolutions = {}) {
706
+ if (strategy === 'overwrite') {
707
+ return {}; // Start fresh, use defaults
708
+ }
709
+
710
+ const values = { ...discovery.extractedValues };
711
+
712
+ // Apply conflict resolutions
713
+ for (const conflict of discovery.conflicts) {
714
+ const resolution = conflictResolutions[conflict.key];
715
+ if (resolution === 'existing') {
716
+ values[conflict.key] = conflict.existingValue;
717
+ } else if (resolution === 'new' || resolution === 'detected') {
718
+ values[conflict.key] = conflict.newValue;
719
+ }
720
+ // If no resolution, keep the first seen value (already in values)
721
+ }
722
+
723
+ return values;
724
+ }
725
+
726
+ module.exports = {
727
+ discoverExistingDocs,
728
+ detectAITools,
729
+ findCommonDocs,
730
+ parseContextFile,
731
+ parseReadme,
732
+ parseClinerules,
733
+ parseCopilotInstructions,
734
+ extractValuesFromContent,
735
+ generateDiscoveryPrompts,
736
+ formatDiscoverySummary,
737
+ buildMergedValues,
738
+ AI_TOOL_SIGNATURES,
739
+ COMMON_DOC_PATTERNS,
740
+ VALUE_EXTRACTION_PATTERNS
741
+ };