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,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Context Engineering - Smart Merge Module
|
|
3
|
+
*
|
|
4
|
+
* Intelligently merges existing documentation with new analysis results.
|
|
5
|
+
* Preserves user customizations while updating stale references and adding new content.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Decision types for merge operations
|
|
13
|
+
*/
|
|
14
|
+
const DECISION_TYPE = {
|
|
15
|
+
PRESERVE: 'preserve', // Keep existing value (user customized)
|
|
16
|
+
UPDATE: 'update', // Replace with new value
|
|
17
|
+
CONFLICT: 'conflict', // Values differ, needs resolution
|
|
18
|
+
PRESERVE_SECTION: 'preserve_section', // Keep custom section
|
|
19
|
+
REMOVE_STALE_REF: 'remove_stale_ref', // Remove reference to deleted file
|
|
20
|
+
UPDATE_REF: 'update_ref', // Update line reference
|
|
21
|
+
ADD_WORKFLOW: 'add_workflow', // Add newly discovered workflow
|
|
22
|
+
ADD_ENTRY_POINT: 'add_entry_point' // Add new entry point
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract content from an existing documentation file
|
|
27
|
+
* @param {string} filePath - Path to existing file
|
|
28
|
+
* @param {string} templatePath - Path to template for comparison
|
|
29
|
+
* @returns {object} Extracted content
|
|
30
|
+
*/
|
|
31
|
+
function extractExistingContent(filePath, templatePath = null) {
|
|
32
|
+
if (!fs.existsSync(filePath)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
37
|
+
|
|
38
|
+
const result = {
|
|
39
|
+
raw: content,
|
|
40
|
+
sections: parseMarkdownSections(content),
|
|
41
|
+
placeholders: extractPlaceholderValues(content),
|
|
42
|
+
lineReferences: extractLineReferences(content),
|
|
43
|
+
customSections: [],
|
|
44
|
+
frontmatter: extractFrontmatter(content)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// If template provided, identify custom sections
|
|
48
|
+
if (templatePath && fs.existsSync(templatePath)) {
|
|
49
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
50
|
+
const templateSections = parseMarkdownSections(templateContent);
|
|
51
|
+
const templateHeadings = new Set(templateSections.map(s => s.heading.toLowerCase()));
|
|
52
|
+
|
|
53
|
+
result.customSections = result.sections.filter(
|
|
54
|
+
s => !templateHeadings.has(s.heading.toLowerCase())
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse markdown into sections by headings
|
|
63
|
+
* @param {string} content - Markdown content
|
|
64
|
+
* @returns {Array} Array of sections
|
|
65
|
+
*/
|
|
66
|
+
function parseMarkdownSections(content) {
|
|
67
|
+
const sections = [];
|
|
68
|
+
const lines = content.split('\n');
|
|
69
|
+
let currentSection = { heading: 'root', level: 0, content: [], startLine: 0 };
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
74
|
+
|
|
75
|
+
if (headingMatch) {
|
|
76
|
+
// Save previous section if it has content
|
|
77
|
+
if (currentSection.content.length > 0 || currentSection.heading !== 'root') {
|
|
78
|
+
currentSection.content = currentSection.content.join('\n').trim();
|
|
79
|
+
sections.push(currentSection);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
currentSection = {
|
|
83
|
+
heading: headingMatch[2].trim(),
|
|
84
|
+
level: headingMatch[1].length,
|
|
85
|
+
content: [],
|
|
86
|
+
startLine: i
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
currentSection.content.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Save final section
|
|
94
|
+
currentSection.content = currentSection.content.join('\n').trim();
|
|
95
|
+
sections.push(currentSection);
|
|
96
|
+
|
|
97
|
+
return sections;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract placeholder values from content
|
|
102
|
+
* @param {string} content - File content
|
|
103
|
+
* @returns {object} Map of placeholder name to value
|
|
104
|
+
*/
|
|
105
|
+
function extractPlaceholderValues(content) {
|
|
106
|
+
const values = {};
|
|
107
|
+
|
|
108
|
+
// Known placeholder patterns and their extraction contexts
|
|
109
|
+
const patterns = [
|
|
110
|
+
{ name: 'PROJECT_NAME', regex: /\*\*Project(?:\s*Name)?:\*\*\s*(.+?)(?:\n|$)/i },
|
|
111
|
+
{ name: 'PROJECT_DESCRIPTION', regex: /\*\*(?:Platform|Description):\*\*\s*(.+?)(?:\n|$)/i },
|
|
112
|
+
{ name: 'TECH_STACK', regex: /\*\*Tech Stack:\*\*\s*(.+?)(?:\n|$)/i },
|
|
113
|
+
{ name: 'PRODUCTION_URL', regex: /\*\*(?:Domain|URL):\*\*\s*(.+?)(?:\n|$)/i },
|
|
114
|
+
{ name: 'API_URL', regex: /\*\*API:\*\*\s*(.+?)(?:\n|$)/i },
|
|
115
|
+
{ name: 'REPO_URL', regex: /\*\*Repo:\*\*\s*(.+?)(?:\n|$)/i }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const { name, regex } of patterns) {
|
|
119
|
+
const match = content.match(regex);
|
|
120
|
+
if (match && match[1]) {
|
|
121
|
+
const value = match[1].trim();
|
|
122
|
+
// Skip if still a placeholder
|
|
123
|
+
if (!value.match(/\{\{[A-Z_]+\}\}/)) {
|
|
124
|
+
values[name] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return values;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract line references from content
|
|
134
|
+
* @param {string} content - File content
|
|
135
|
+
* @returns {Array} Array of line references
|
|
136
|
+
*/
|
|
137
|
+
function extractLineReferences(content) {
|
|
138
|
+
const refs = [];
|
|
139
|
+
const pattern = /([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+):(\d+)(?:-(\d+))?/g;
|
|
140
|
+
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
143
|
+
refs.push({
|
|
144
|
+
file: match[1].replace(/\\/g, '/'),
|
|
145
|
+
line: parseInt(match[2], 10),
|
|
146
|
+
endLine: match[3] ? parseInt(match[3], 10) : null,
|
|
147
|
+
original: match[0],
|
|
148
|
+
position: match.index
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return refs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract YAML frontmatter from markdown
|
|
157
|
+
* @param {string} content - File content
|
|
158
|
+
* @returns {object|null} Parsed frontmatter
|
|
159
|
+
*/
|
|
160
|
+
function extractFrontmatter(content) {
|
|
161
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Simple YAML parsing for common cases
|
|
166
|
+
const yaml = {};
|
|
167
|
+
const lines = match[1].split('\n');
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
170
|
+
if (kvMatch) {
|
|
171
|
+
yaml[kvMatch[1]] = kvMatch[2].trim();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return yaml;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate merge decisions
|
|
182
|
+
* @param {object} existing - Extracted existing content
|
|
183
|
+
* @param {object} newAnalysis - New analysis results
|
|
184
|
+
* @param {object} options - Merge options
|
|
185
|
+
* @returns {Array} Array of merge decisions
|
|
186
|
+
*/
|
|
187
|
+
function decideMerge(existing, newAnalysis, options = {}) {
|
|
188
|
+
const {
|
|
189
|
+
preserveCustom = true,
|
|
190
|
+
updateRefs = false,
|
|
191
|
+
defaultPlaceholders = {}
|
|
192
|
+
} = options;
|
|
193
|
+
|
|
194
|
+
const decisions = [];
|
|
195
|
+
|
|
196
|
+
if (!existing) {
|
|
197
|
+
// No existing content, use all new values
|
|
198
|
+
return [{
|
|
199
|
+
type: DECISION_TYPE.UPDATE,
|
|
200
|
+
reason: 'No existing content'
|
|
201
|
+
}];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 1. Placeholder decisions
|
|
205
|
+
for (const [name, existingValue] of Object.entries(existing.placeholders || {})) {
|
|
206
|
+
const newValue = newAnalysis?.values?.[name] || defaultPlaceholders[name];
|
|
207
|
+
const defaultValue = defaultPlaceholders[name];
|
|
208
|
+
|
|
209
|
+
// Check if value was customized (different from default)
|
|
210
|
+
const isCustomized = existingValue !== defaultValue && existingValue !== `{{${name}}}`;
|
|
211
|
+
|
|
212
|
+
if (isCustomized && preserveCustom) {
|
|
213
|
+
decisions.push({
|
|
214
|
+
type: DECISION_TYPE.PRESERVE,
|
|
215
|
+
placeholder: name,
|
|
216
|
+
value: existingValue,
|
|
217
|
+
reason: 'User customized'
|
|
218
|
+
});
|
|
219
|
+
} else if (newValue && newValue !== existingValue) {
|
|
220
|
+
decisions.push({
|
|
221
|
+
type: DECISION_TYPE.UPDATE,
|
|
222
|
+
placeholder: name,
|
|
223
|
+
oldValue: existingValue,
|
|
224
|
+
newValue,
|
|
225
|
+
reason: 'Updated from analysis'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 2. Custom section decisions
|
|
231
|
+
for (const section of existing.customSections || []) {
|
|
232
|
+
decisions.push({
|
|
233
|
+
type: DECISION_TYPE.PRESERVE_SECTION,
|
|
234
|
+
heading: section.heading,
|
|
235
|
+
content: section.content,
|
|
236
|
+
position: section.startLine,
|
|
237
|
+
reason: 'Custom section not in template'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Line reference decisions
|
|
242
|
+
if (updateRefs) {
|
|
243
|
+
for (const ref of existing.lineReferences || []) {
|
|
244
|
+
const fullPath = path.join(process.cwd(), ref.file);
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(fullPath)) {
|
|
247
|
+
decisions.push({
|
|
248
|
+
type: DECISION_TYPE.REMOVE_STALE_REF,
|
|
249
|
+
reference: ref.original,
|
|
250
|
+
reason: 'File no longer exists'
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
try {
|
|
254
|
+
const fileContent = fs.readFileSync(fullPath, 'utf-8');
|
|
255
|
+
const lineCount = fileContent.split('\n').length;
|
|
256
|
+
|
|
257
|
+
if (ref.line > lineCount) {
|
|
258
|
+
decisions.push({
|
|
259
|
+
type: DECISION_TYPE.UPDATE_REF,
|
|
260
|
+
oldRef: ref.original,
|
|
261
|
+
newRef: `${ref.file}:${lineCount}`,
|
|
262
|
+
reason: `Line ${ref.line} exceeds file length (${lineCount} lines)`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Can't read file, skip
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 4. New workflow decisions
|
|
273
|
+
for (const workflow of newAnalysis?.workflows || []) {
|
|
274
|
+
const existsInDocs = existing.sections?.some(
|
|
275
|
+
s => s.heading.toLowerCase().includes(workflow.name?.toLowerCase() || '')
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!existsInDocs) {
|
|
279
|
+
decisions.push({
|
|
280
|
+
type: DECISION_TYPE.ADD_WORKFLOW,
|
|
281
|
+
workflow,
|
|
282
|
+
reason: 'Newly discovered workflow'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return decisions;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate merged content from decisions
|
|
292
|
+
* @param {string} templateContent - Template content
|
|
293
|
+
* @param {Array} decisions - Merge decisions
|
|
294
|
+
* @param {object} existing - Existing content
|
|
295
|
+
* @returns {string} Merged content
|
|
296
|
+
*/
|
|
297
|
+
function generateMergedContent(templateContent, decisions, existing) {
|
|
298
|
+
let content = templateContent;
|
|
299
|
+
|
|
300
|
+
// Apply decisions
|
|
301
|
+
for (const decision of decisions) {
|
|
302
|
+
switch (decision.type) {
|
|
303
|
+
case DECISION_TYPE.PRESERVE:
|
|
304
|
+
case DECISION_TYPE.UPDATE:
|
|
305
|
+
// Replace placeholder with value
|
|
306
|
+
if (decision.placeholder && (decision.value || decision.newValue)) {
|
|
307
|
+
const value = decision.value || decision.newValue;
|
|
308
|
+
const placeholder = `{{${decision.placeholder}}}`;
|
|
309
|
+
content = content.replace(new RegExp(escapeRegex(placeholder), 'g'), value);
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case DECISION_TYPE.PRESERVE_SECTION:
|
|
314
|
+
// Insert custom section at appropriate position
|
|
315
|
+
if (decision.heading && decision.content) {
|
|
316
|
+
const level = '#'.repeat(decision.level || 2);
|
|
317
|
+
const sectionContent = `\n${level} ${decision.heading}\n\n${decision.content}\n`;
|
|
318
|
+
// Append before the last section or at end
|
|
319
|
+
const lastHeadingMatch = content.match(/\n(#{1,6}\s+[^\n]+)\n[^#]*$/);
|
|
320
|
+
if (lastHeadingMatch) {
|
|
321
|
+
content = content.replace(lastHeadingMatch[0], sectionContent + lastHeadingMatch[0]);
|
|
322
|
+
} else {
|
|
323
|
+
content += sectionContent;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case DECISION_TYPE.UPDATE_REF:
|
|
329
|
+
// Update line reference
|
|
330
|
+
if (decision.oldRef && decision.newRef) {
|
|
331
|
+
content = content.replace(
|
|
332
|
+
new RegExp(escapeRegex(decision.oldRef), 'g'),
|
|
333
|
+
decision.newRef
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case DECISION_TYPE.REMOVE_STALE_REF:
|
|
339
|
+
// Comment out or remove stale reference
|
|
340
|
+
if (decision.reference) {
|
|
341
|
+
content = content.replace(
|
|
342
|
+
new RegExp(escapeRegex(decision.reference), 'g'),
|
|
343
|
+
`<!-- REMOVED: ${decision.reference} -->`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return content;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generate diff between old and new content
|
|
355
|
+
* @param {string} oldContent - Original content
|
|
356
|
+
* @param {string} newContent - New content
|
|
357
|
+
* @param {Array} decisions - Merge decisions
|
|
358
|
+
* @returns {object} Diff summary
|
|
359
|
+
*/
|
|
360
|
+
function generateDiff(oldContent, newContent, decisions) {
|
|
361
|
+
const diff = {
|
|
362
|
+
summary: {
|
|
363
|
+
preserved: 0,
|
|
364
|
+
updated: 0,
|
|
365
|
+
added: 0,
|
|
366
|
+
removed: 0,
|
|
367
|
+
conflicts: 0
|
|
368
|
+
},
|
|
369
|
+
changes: [],
|
|
370
|
+
migrationNotes: []
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
for (const decision of decisions) {
|
|
374
|
+
switch (decision.type) {
|
|
375
|
+
case DECISION_TYPE.PRESERVE:
|
|
376
|
+
case DECISION_TYPE.PRESERVE_SECTION:
|
|
377
|
+
diff.summary.preserved++;
|
|
378
|
+
diff.changes.push({
|
|
379
|
+
type: 'preserve',
|
|
380
|
+
location: decision.placeholder || decision.heading,
|
|
381
|
+
reason: decision.reason
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
|
|
385
|
+
case DECISION_TYPE.UPDATE:
|
|
386
|
+
case DECISION_TYPE.UPDATE_REF:
|
|
387
|
+
diff.summary.updated++;
|
|
388
|
+
diff.changes.push({
|
|
389
|
+
type: 'update',
|
|
390
|
+
location: decision.placeholder || decision.oldRef,
|
|
391
|
+
oldValue: decision.oldValue || decision.oldRef,
|
|
392
|
+
newValue: decision.newValue || decision.newRef
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case DECISION_TYPE.ADD_WORKFLOW:
|
|
397
|
+
case DECISION_TYPE.ADD_ENTRY_POINT:
|
|
398
|
+
diff.summary.added++;
|
|
399
|
+
diff.changes.push({
|
|
400
|
+
type: 'add',
|
|
401
|
+
description: decision.workflow?.name || decision.entryPoint?.file
|
|
402
|
+
});
|
|
403
|
+
break;
|
|
404
|
+
|
|
405
|
+
case DECISION_TYPE.REMOVE_STALE_REF:
|
|
406
|
+
diff.summary.removed++;
|
|
407
|
+
diff.migrationNotes.push({
|
|
408
|
+
type: 'removed',
|
|
409
|
+
reference: decision.reference,
|
|
410
|
+
reason: decision.reason
|
|
411
|
+
});
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case DECISION_TYPE.CONFLICT:
|
|
415
|
+
diff.summary.conflicts++;
|
|
416
|
+
diff.changes.push({
|
|
417
|
+
type: 'conflict',
|
|
418
|
+
location: decision.placeholder,
|
|
419
|
+
existing: decision.existingValue,
|
|
420
|
+
proposed: decision.newValue
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return diff;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Escape regex special characters
|
|
431
|
+
*/
|
|
432
|
+
function escapeRegex(string) {
|
|
433
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Smart merge a file with new analysis
|
|
438
|
+
* @param {string} filePath - Path to existing file
|
|
439
|
+
* @param {string} templatePath - Path to template
|
|
440
|
+
* @param {object} analysis - New analysis results
|
|
441
|
+
* @param {object} options - Merge options
|
|
442
|
+
* @returns {object} Merge result
|
|
443
|
+
*/
|
|
444
|
+
async function smartMergeFile(filePath, templatePath, analysis, options = {}) {
|
|
445
|
+
const {
|
|
446
|
+
dryRun = false,
|
|
447
|
+
backup = false,
|
|
448
|
+
preserveCustom = true,
|
|
449
|
+
updateRefs = false
|
|
450
|
+
} = options;
|
|
451
|
+
|
|
452
|
+
// Check if file exists
|
|
453
|
+
const fileExists = fs.existsSync(filePath);
|
|
454
|
+
const templateExists = fs.existsSync(templatePath);
|
|
455
|
+
|
|
456
|
+
if (!templateExists) {
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
error: 'Template file not found'
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
464
|
+
|
|
465
|
+
// If file doesn't exist, just use template
|
|
466
|
+
if (!fileExists) {
|
|
467
|
+
if (!dryRun) {
|
|
468
|
+
fs.writeFileSync(filePath, templateContent);
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
success: true,
|
|
472
|
+
isNew: true,
|
|
473
|
+
decisions: []
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Extract existing content
|
|
478
|
+
const existing = extractExistingContent(filePath, templatePath);
|
|
479
|
+
|
|
480
|
+
// Generate merge decisions
|
|
481
|
+
const decisions = decideMerge(existing, analysis, {
|
|
482
|
+
preserveCustom,
|
|
483
|
+
updateRefs,
|
|
484
|
+
defaultPlaceholders: options.defaultPlaceholders || {}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Generate merged content
|
|
488
|
+
const mergedContent = generateMergedContent(templateContent, decisions, existing);
|
|
489
|
+
|
|
490
|
+
// Generate diff
|
|
491
|
+
const diff = generateDiff(existing.raw, mergedContent, decisions);
|
|
492
|
+
|
|
493
|
+
if (dryRun) {
|
|
494
|
+
return {
|
|
495
|
+
success: true,
|
|
496
|
+
dryRun: true,
|
|
497
|
+
decisions,
|
|
498
|
+
diff,
|
|
499
|
+
wouldWrite: mergedContent !== existing.raw
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Backup if requested
|
|
504
|
+
if (backup) {
|
|
505
|
+
const backupPath = filePath + '.backup-' + Date.now();
|
|
506
|
+
fs.writeFileSync(backupPath, existing.raw);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Write merged content
|
|
510
|
+
fs.writeFileSync(filePath, mergedContent);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
decisions,
|
|
515
|
+
diff,
|
|
516
|
+
preserved: diff.summary.preserved,
|
|
517
|
+
updated: diff.summary.updated,
|
|
518
|
+
added: diff.summary.added,
|
|
519
|
+
removed: diff.summary.removed,
|
|
520
|
+
migrationNotes: diff.migrationNotes
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
module.exports = {
|
|
525
|
+
// Core functions
|
|
526
|
+
extractExistingContent,
|
|
527
|
+
decideMerge,
|
|
528
|
+
generateMergedContent,
|
|
529
|
+
generateDiff,
|
|
530
|
+
smartMergeFile,
|
|
531
|
+
|
|
532
|
+
// Parsing functions
|
|
533
|
+
parseMarkdownSections,
|
|
534
|
+
extractPlaceholderValues,
|
|
535
|
+
extractLineReferences,
|
|
536
|
+
extractFrontmatter,
|
|
537
|
+
|
|
538
|
+
// Constants
|
|
539
|
+
DECISION_TYPE
|
|
540
|
+
};
|
package/lib/spinner.js
CHANGED
package/package.json
CHANGED