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.
- package/README.md +55 -23
- package/bin/create-ai-context.js +159 -1
- package/lib/adapters/claude.js +180 -29
- package/lib/doc-discovery.js +741 -0
- package/lib/drift-checker.js +920 -0
- package/lib/index.js +89 -7
- package/lib/installer.js +1 -0
- package/lib/placeholder.js +11 -1
- package/lib/prompts.js +55 -1
- package/lib/smart-merge.js +540 -0
- package/lib/spinner.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|