create-universal-ai-context 2.4.0 → 2.6.0-final

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 (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +331 -294
  3. package/bin/create-ai-context.js +1507 -764
  4. package/lib/adapters/aider.js +131 -131
  5. package/lib/adapters/antigravity.js +205 -205
  6. package/lib/adapters/claude.js +397 -397
  7. package/lib/adapters/cline.js +125 -125
  8. package/lib/adapters/continue.js +138 -138
  9. package/lib/adapters/copilot.js +131 -131
  10. package/lib/adapters/index.js +78 -78
  11. package/lib/adapters/windsurf.js +138 -138
  12. package/lib/ai-context-generator.js +234 -234
  13. package/lib/ai-orchestrator.js +432 -432
  14. package/lib/call-tracer.js +444 -444
  15. package/lib/content-preservation.js +243 -243
  16. package/lib/cross-tool-sync/file-watcher.js +274 -274
  17. package/lib/cross-tool-sync/index.js +41 -40
  18. package/lib/cross-tool-sync/sync-manager.js +540 -512
  19. package/lib/cross-tool-sync/sync-service.js +297 -297
  20. package/lib/detector.js +726 -726
  21. package/lib/doc-discovery.js +741 -741
  22. package/lib/drift-checker.js +920 -920
  23. package/lib/environment-detector.js +239 -239
  24. package/lib/index.js +399 -399
  25. package/lib/install-hooks.js +82 -82
  26. package/lib/installer.js +419 -419
  27. package/lib/migrate.js +328 -328
  28. package/lib/placeholder.js +632 -632
  29. package/lib/prompts.js +341 -341
  30. package/lib/smart-merge.js +540 -540
  31. package/lib/spinner.js +60 -60
  32. package/lib/static-analyzer.js +729 -729
  33. package/lib/template-coordination.js +148 -148
  34. package/lib/template-populator.js +843 -843
  35. package/lib/template-renderer.js +392 -392
  36. package/lib/utils/fs-wrapper.js +79 -79
  37. package/lib/utils/path-utils.js +60 -60
  38. package/lib/validate.js +155 -155
  39. package/package.json +1 -1
  40. package/templates/AI_CONTEXT.md.template +245 -245
  41. package/templates/base/README.md +260 -257
  42. package/templates/base/RPI_WORKFLOW_PLAN.md +325 -320
  43. package/templates/base/agents/api-developer.md +76 -76
  44. package/templates/base/agents/context-engineer.md +525 -525
  45. package/templates/base/agents/core-architect.md +76 -76
  46. package/templates/base/agents/database-ops.md +76 -76
  47. package/templates/base/agents/deployment-ops.md +76 -76
  48. package/templates/base/agents/integration-hub.md +76 -76
  49. package/templates/base/analytics/README.md +114 -114
  50. package/templates/base/automation/config.json +58 -58
  51. package/templates/base/automation/generators/code-mapper.js +308 -308
  52. package/templates/base/automation/generators/index-builder.js +321 -321
  53. package/templates/base/automation/hooks/post-commit.sh +83 -83
  54. package/templates/base/automation/hooks/pre-commit.sh +103 -103
  55. package/templates/base/ci-templates/README.md +108 -108
  56. package/templates/base/ci-templates/github-actions/context-check.yml +144 -144
  57. package/templates/base/ci-templates/github-actions/validate-docs.yml +105 -105
  58. package/templates/base/commands/analytics.md +238 -238
  59. package/templates/base/commands/auto-sync.md +172 -172
  60. package/templates/base/commands/collab.md +194 -194
  61. package/templates/base/commands/context-optimize.md +226 -0
  62. package/templates/base/commands/help.md +485 -450
  63. package/templates/base/commands/rpi-implement.md +164 -115
  64. package/templates/base/commands/rpi-plan.md +147 -93
  65. package/templates/base/commands/rpi-research.md +145 -88
  66. package/templates/base/commands/session-resume.md +144 -144
  67. package/templates/base/commands/session-save.md +112 -112
  68. package/templates/base/commands/validate-all.md +77 -77
  69. package/templates/base/commands/verify-docs-current.md +86 -86
  70. package/templates/base/config/base.json +57 -57
  71. package/templates/base/config/environments/development.json +13 -13
  72. package/templates/base/config/environments/production.json +17 -17
  73. package/templates/base/config/environments/staging.json +13 -13
  74. package/templates/base/config/local.json.example +21 -21
  75. package/templates/base/context/.meta/generated-at.json +18 -18
  76. package/templates/base/context/ARCHITECTURE_SNAPSHOT.md +156 -156
  77. package/templates/base/context/CODE_TO_WORKFLOW_MAP.md +94 -94
  78. package/templates/base/context/FILE_OWNERSHIP.md +57 -57
  79. package/templates/base/context/INTEGRATION_POINTS.md +92 -92
  80. package/templates/base/context/KNOWN_GOTCHAS.md +195 -195
  81. package/templates/base/context/TESTING_MAP.md +95 -95
  82. package/templates/base/context/WORKFLOW_INDEX.md +129 -129
  83. package/templates/base/context/workflows/WORKFLOW_TEMPLATE.md +294 -294
  84. package/templates/base/indexes/agents/CAPABILITY_MATRIX.md +255 -255
  85. package/templates/base/indexes/agents/CATEGORY_INDEX.md +44 -44
  86. package/templates/base/indexes/code/CATEGORY_INDEX.md +38 -38
  87. package/templates/base/indexes/routing/CATEGORY_INDEX.md +39 -39
  88. package/templates/base/indexes/search/CATEGORY_INDEX.md +39 -39
  89. package/templates/base/indexes/workflows/CATEGORY_INDEX.md +38 -38
  90. package/templates/base/knowledge/README.md +98 -98
  91. package/templates/base/knowledge/sessions/README.md +88 -88
  92. package/templates/base/knowledge/sessions/TEMPLATE.md +150 -150
  93. package/templates/base/knowledge/shared/decisions/0001-adopt-context-engineering.md +144 -144
  94. package/templates/base/knowledge/shared/decisions/README.md +49 -49
  95. package/templates/base/knowledge/shared/decisions/TEMPLATE.md +123 -123
  96. package/templates/base/knowledge/shared/patterns/README.md +62 -62
  97. package/templates/base/knowledge/shared/patterns/TEMPLATE.md +120 -120
  98. package/templates/base/plans/PLAN_TEMPLATE.md +316 -250
  99. package/templates/base/research/RESEARCH_TEMPLATE.md +245 -153
  100. package/templates/base/schemas/agent.schema.json +141 -141
  101. package/templates/base/schemas/anchors.schema.json +54 -54
  102. package/templates/base/schemas/automation.schema.json +93 -93
  103. package/templates/base/schemas/command.schema.json +134 -134
  104. package/templates/base/schemas/hashes.schema.json +40 -40
  105. package/templates/base/schemas/manifest.schema.json +117 -117
  106. package/templates/base/schemas/plan.schema.json +136 -136
  107. package/templates/base/schemas/research.schema.json +115 -115
  108. package/templates/base/schemas/roles.schema.json +34 -34
  109. package/templates/base/schemas/session.schema.json +77 -77
  110. package/templates/base/schemas/settings.schema.json +244 -244
  111. package/templates/base/schemas/staleness.schema.json +53 -53
  112. package/templates/base/schemas/team-config.schema.json +42 -42
  113. package/templates/base/schemas/workflow.schema.json +126 -126
  114. package/templates/base/session/checkpoints/.gitkeep +2 -2
  115. package/templates/base/session/current/state.json +20 -20
  116. package/templates/base/session/history/.gitkeep +2 -2
  117. package/templates/base/settings.json +3 -3
  118. package/templates/base/standards/COMPATIBILITY.md +219 -219
  119. package/templates/base/standards/EXTENSION_GUIDELINES.md +280 -280
  120. package/templates/base/standards/QUALITY_CHECKLIST.md +211 -211
  121. package/templates/base/standards/README.md +66 -66
  122. package/templates/base/sync/anchors.json +6 -6
  123. package/templates/base/sync/hashes.json +6 -6
  124. package/templates/base/sync/staleness.json +10 -10
  125. package/templates/base/team/README.md +168 -168
  126. package/templates/base/team/config.json +79 -79
  127. package/templates/base/team/roles.json +145 -145
  128. package/templates/base/tools/bin/claude-context.js +151 -151
  129. package/templates/base/tools/lib/anchor-resolver.js +276 -276
  130. package/templates/base/tools/lib/config-loader.js +363 -363
  131. package/templates/base/tools/lib/detector.js +350 -350
  132. package/templates/base/tools/lib/diagnose.js +206 -206
  133. package/templates/base/tools/lib/drift-detector.js +373 -373
  134. package/templates/base/tools/lib/errors.js +199 -199
  135. package/templates/base/tools/lib/index.js +36 -36
  136. package/templates/base/tools/lib/init.js +192 -192
  137. package/templates/base/tools/lib/logger.js +230 -230
  138. package/templates/base/tools/lib/placeholder.js +201 -201
  139. package/templates/base/tools/lib/session-manager.js +354 -354
  140. package/templates/base/tools/lib/validate.js +521 -521
  141. package/templates/base/tools/package.json +49 -49
  142. package/templates/handlebars/aider-config.hbs +146 -80
  143. package/templates/handlebars/antigravity.hbs +377 -377
  144. package/templates/handlebars/claude.hbs +183 -183
  145. package/templates/handlebars/cline.hbs +62 -62
  146. package/templates/handlebars/continue-config.hbs +116 -116
  147. package/templates/handlebars/copilot.hbs +130 -130
  148. package/templates/handlebars/partials/gotcha-list.hbs +11 -11
  149. package/templates/handlebars/partials/header.hbs +3 -3
  150. package/templates/handlebars/partials/workflow-summary.hbs +16 -16
  151. package/templates/handlebars/windsurf-rules.hbs +69 -69
  152. package/templates/hooks/post-commit.hbs +28 -29
  153. package/templates/hooks/pre-commit.hbs +46 -46
@@ -1,512 +1,540 @@
1
- /**
2
- * Cross-Tool Sync Manager
3
- *
4
- * Orchestrates automatic synchronization between AI tool contexts.
5
- * Detects changes in one tool's context and propagates to others.
6
- */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const crypto = require('crypto');
11
- const { getAdapter, getAllAdapters, getAdapterNames } = require('../adapters');
12
- const { analyzeCodebase } = require('../static-analyzer');
13
- const { generateAll, initialize: initGenerator } = require('../ai-context-generator');
14
-
15
- /**
16
- * Context file paths for each AI tool
17
- */
18
- const TOOL_CONTEXT_FILES = {
19
- claude: ['AI_CONTEXT.md', '.claude/'],
20
- copilot: ['.github/copilot-instructions.md'],
21
- cline: ['.clinerules'],
22
- antigravity: ['.agent/']
23
- };
24
-
25
- /**
26
- * Conflict resolution strategies
27
- */
28
- const CONFLICT_STRATEGY = {
29
- SOURCE_WINS: 'source_wins', // Changed file always wins
30
- REGENERATE_ALL: 'regenerate_all', // Regenerate all from codebase
31
- MANUAL: 'manual', // Require manual resolution
32
- NEWEST: 'newest' // File with newest modification time wins
33
- };
34
-
35
- /**
36
- * Sync state storage path
37
- */
38
- function getSyncStatePath(projectRoot) {
39
- return path.join(projectRoot, '.ai-context', 'sync-state.json');
40
- }
41
-
42
- /**
43
- * Initialize sync state (creates new if doesn't exist, loads existing if present)
44
- */
45
- function initSyncState(projectRoot) {
46
- const statePath = getSyncStatePath(projectRoot);
47
- const stateDir = path.dirname(statePath);
48
-
49
- if (!fs.existsSync(stateDir)) {
50
- fs.mkdirSync(stateDir, { recursive: true });
51
- }
52
-
53
- if (!fs.existsSync(statePath)) {
54
- const initialState = {
55
- version: '1.0.0',
56
- lastSync: null,
57
- toolHashes: {},
58
- conflicts: [],
59
- syncHistory: []
60
- };
61
- fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2));
62
- return initialState;
63
- }
64
-
65
- try {
66
- return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
67
- } catch (error) {
68
- // File exists but is corrupted, create new state
69
- const initialState = {
70
- version: '1.0.0',
71
- lastSync: null,
72
- toolHashes: {},
73
- conflicts: [],
74
- syncHistory: []
75
- };
76
- fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2));
77
- return initialState;
78
- }
79
- }
80
-
81
- /**
82
- * Load sync state (always loads from disk, doesn't create new)
83
- */
84
- function loadSyncState(projectRoot) {
85
- const statePath = getSyncStatePath(projectRoot);
86
-
87
- if (!fs.existsSync(statePath)) {
88
- return null;
89
- }
90
-
91
- try {
92
- return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
93
- } catch (error) {
94
- return null;
95
- }
96
- }
97
-
98
- /**
99
- * Save sync state
100
- */
101
- function saveSyncState(projectRoot, state) {
102
- const statePath = getSyncStatePath(projectRoot);
103
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
104
- }
105
-
106
- /**
107
- * Calculate file hash for change detection
108
- */
109
- function calculateFileHash(filePath) {
110
- if (!fs.existsSync(filePath)) {
111
- return null;
112
- }
113
-
114
- const content = fs.readFileSync(filePath, 'utf-8');
115
- return crypto.createHash('sha256').update(content).digest('hex');
116
- }
117
-
118
- /**
119
- * Get all context files for a tool
120
- */
121
- function getToolContextFiles(toolName, projectRoot) {
122
- const files = TOOL_CONTEXT_FILES[toolName] || [];
123
- const results = [];
124
-
125
- for (const file of files) {
126
- const fullPath = path.join(projectRoot, file);
127
-
128
- if (file.endsWith('/')) {
129
- // Directory - calculate combined hash of all files
130
- if (fs.existsSync(fullPath)) {
131
- const dirHash = hashDirectory(fullPath);
132
- results.push({ path: file, hash: dirHash, isDirectory: true });
133
- }
134
- } else {
135
- // Single file
136
- if (fs.existsSync(fullPath)) {
137
- results.push({ path: file, hash: calculateFileHash(fullPath), isDirectory: false });
138
- }
139
- }
140
- }
141
-
142
- return results;
143
- }
144
-
145
- /**
146
- * Calculate hash for a directory
147
- */
148
- function hashDirectory(dirPath) {
149
- const hash = crypto.createHash('sha256');
150
- const files = getAllFiles(dirPath);
151
-
152
- for (const file of files.sort()) {
153
- const content = fs.readFileSync(file, 'utf-8');
154
- hash.update(content);
155
- }
156
-
157
- return hash.digest('hex');
158
- }
159
-
160
- /**
161
- * Get all files in directory recursively
162
- */
163
- function getAllFiles(dirPath) {
164
- const files = [];
165
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
166
-
167
- for (const entry of entries) {
168
- const fullPath = path.join(dirPath, entry.name);
169
- if (entry.isDirectory()) {
170
- files.push(...getAllFiles(fullPath));
171
- } else {
172
- files.push(fullPath);
173
- }
174
- }
175
-
176
- return files;
177
- }
178
-
179
- /**
180
- * Detect which tool's context has changed
181
- */
182
- function detectChangedTool(projectRoot, state) {
183
- const currentHashes = {};
184
- const changedTools = [];
185
-
186
- for (const toolName of getAdapterNames()) {
187
- const files = getToolContextFiles(toolName, projectRoot);
188
- const toolHash = files.map(f => f.hash).filter(Boolean).join('|');
189
-
190
- currentHashes[toolName] = toolHash;
191
-
192
- // Use hasOwnProperty check to handle empty string case
193
- const hasStoredHash = Object.prototype.hasOwnProperty.call(state.toolHashes, toolName);
194
- const storedHash = state.toolHashes[toolName];
195
-
196
- if (hasStoredHash && storedHash !== toolHash) {
197
- changedTools.push({
198
- tool: toolName,
199
- previousHash: storedHash,
200
- currentHash: toolHash
201
- });
202
- }
203
- }
204
-
205
- return { changedTools, currentHashes };
206
- }
207
-
208
- /**
209
- * Propagate context change from source tool to all other tools
210
- */
211
- async function propagateContextChange(sourceTool, projectRoot, config, strategy = CONFLICT_STRATEGY.SOURCE_WINS) {
212
- const results = {
213
- sourceTool,
214
- strategy,
215
- timestamp: new Date().toISOString(),
216
- propagated: [],
217
- skipped: [],
218
- errors: []
219
- };
220
-
221
- // 1. Re-analyze codebase to get fresh analysis
222
- let analysis;
223
- try {
224
- analysis = await analyzeCodebase(projectRoot, config);
225
- } catch (error) {
226
- results.errors.push({
227
- message: `Failed to analyze project: ${error.message}`
228
- });
229
- return results;
230
- }
231
-
232
- // 2. Get all adapters except source
233
- const allAdapters = getAllAdapters();
234
- const targetAdapters = allAdapters.filter(a => a.name !== sourceTool);
235
-
236
- // 3. Generate contexts for all target tools
237
- initGenerator();
238
-
239
- for (const adapter of targetAdapters) {
240
- try {
241
- const result = await adapter.generate(analysis, config, projectRoot);
242
-
243
- if (result.success) {
244
- results.propagated.push({
245
- tool: adapter.name,
246
- displayName: adapter.displayName,
247
- files: result.files
248
- });
249
- } else {
250
- results.errors.push({
251
- tool: adapter.name,
252
- errors: result.errors
253
- });
254
- }
255
- } catch (error) {
256
- results.errors.push({
257
- tool: adapter.name,
258
- message: error.message
259
- });
260
- }
261
- }
262
-
263
- // 4. Update sync state
264
- const state = initSyncState(projectRoot);
265
- const { currentHashes } = detectChangedTool(projectRoot, state);
266
- state.toolHashes = currentHashes;
267
- state.lastSync = new Date().toISOString();
268
- state.syncHistory.push({
269
- timestamp: new Date().toISOString(),
270
- sourceTool,
271
- strategy,
272
- propagatedCount: results.propagated.length,
273
- errorCount: results.errors.length
274
- });
275
- saveSyncState(projectRoot, state);
276
-
277
- return results;
278
- }
279
-
280
- /**
281
- * Check if contexts are out of sync
282
- */
283
- function checkSyncStatus(projectRoot) {
284
- const state = loadSyncState(projectRoot) || initSyncState(projectRoot);
285
- const status = {
286
- inSync: true,
287
- tools: {},
288
- lastSync: state.lastSync,
289
- conflicts: []
290
- };
291
-
292
- const { changedTools, currentHashes } = detectChangedTool(projectRoot, state);
293
-
294
- for (const toolName of getAdapterNames()) {
295
- const files = getToolContextFiles(toolName, projectRoot);
296
- const exists = files.length > 0;
297
- const hasChanges = changedTools.some(c => c.tool === toolName);
298
-
299
- status.tools[toolName] = {
300
- exists,
301
- hasChanges,
302
- hash: currentHashes[toolName],
303
- previousHash: state.toolHashes[toolName]
304
- };
305
-
306
- if (hasChanges && !state.lastSync) {
307
- status.inSync = false;
308
- }
309
- }
310
-
311
- if (changedTools.length > 0 && state.lastSync) {
312
- status.inSync = false;
313
- status.changedTools = changedTools;
314
- }
315
-
316
- return status;
317
- }
318
-
319
- /**
320
- * Sync all tools from codebase (fresh regeneration)
321
- */
322
- async function syncAllFromCodebase(projectRoot, config) {
323
- const results = {
324
- timestamp: new Date().toISOString(),
325
- tools: [],
326
- errors: []
327
- };
328
-
329
- try {
330
- // Analyze project
331
- const analysis = await analyzeCodebase(projectRoot, config);
332
-
333
- // Generate for all tools
334
- const generateResults = await generateAll(analysis, config, projectRoot, {
335
- aiTools: getAdapterNames()
336
- });
337
-
338
- results.tools = generateResults.generated.map(g => ({
339
- tool: g.adapter,
340
- displayName: g.displayName,
341
- fileCount: g.files.length
342
- }));
343
-
344
- results.errors = generateResults.errors;
345
-
346
- // Update sync state
347
- const state = initSyncState(projectRoot);
348
- const { currentHashes } = detectChangedTool(projectRoot, state);
349
- state.toolHashes = currentHashes;
350
- state.lastSync = new Date().toISOString();
351
- state.syncHistory.push({
352
- timestamp: new Date().toISOString(),
353
- source: 'codebase',
354
- strategy: 'regenerate_all',
355
- propagatedCount: results.tools.length,
356
- errorCount: results.errors.length
357
- });
358
- saveSyncState(projectRoot, state);
359
-
360
- } catch (error) {
361
- results.errors.push({
362
- message: `Sync failed: ${error.message}`,
363
- stack: error.stack
364
- });
365
- }
366
-
367
- return results;
368
- }
369
-
370
- /**
371
- * Resolve conflict between tools
372
- */
373
- async function resolveConflict(projectRoot, config, strategy, preferredTool = null) {
374
- const status = checkSyncStatus(projectRoot);
375
-
376
- if (status.inSync) {
377
- return {
378
- resolved: true,
379
- message: 'No conflicts to resolve'
380
- };
381
- }
382
-
383
- switch (strategy) {
384
- case CONFLICT_STRATEGY.SOURCE_WINS:
385
- if (!preferredTool) {
386
- return {
387
- resolved: false,
388
- message: 'Source strategy requires a preferred tool'
389
- };
390
- }
391
- return await propagateContextChange(preferredTool, projectRoot, config, strategy);
392
-
393
- case CONFLICT_STRATEGY.REGENERATE_ALL:
394
- return await syncAllFromCodebase(projectRoot, config);
395
-
396
- case CONFLICT_STRATEGY.NEWEST:
397
- // Find tool with most recent change
398
- const newestTool = findNewestTool(projectRoot, status);
399
- return await propagateContextChange(newestTool, projectRoot, config, strategy);
400
-
401
- case CONFLICT_STRATEGY.MANUAL:
402
- return {
403
- resolved: false,
404
- message: 'Manual resolution required',
405
- status
406
- };
407
-
408
- default:
409
- return {
410
- resolved: false,
411
- message: `Unknown strategy: ${strategy}`
412
- };
413
- }
414
- }
415
-
416
- /**
417
- * Find tool with most recently modified context
418
- */
419
- function findNewestTool(projectRoot, status) {
420
- let newestTool = null;
421
- let newestTime = 0;
422
-
423
- for (const [toolName, toolStatus] of Object.entries(status.tools)) {
424
- const files = getToolContextFiles(toolName, projectRoot);
425
-
426
- for (const file of files) {
427
- const fullPath = path.join(projectRoot, file.path);
428
-
429
- if (file.isDirectory) {
430
- continue; // Skip directories for mtime check
431
- }
432
-
433
- const stats = fs.statSync(fullPath);
434
- if (stats.mtimeMs > newestTime) {
435
- newestTime = stats.mtimeMs;
436
- newestTool = toolName;
437
- }
438
- }
439
- }
440
-
441
- return newestTool;
442
- }
443
-
444
- /**
445
- * Get sync history
446
- */
447
- function getSyncHistory(projectRoot, limit = 10) {
448
- const state = initSyncState(projectRoot);
449
- return state.syncHistory.slice(-limit);
450
- }
451
-
452
- /**
453
- * Format sync status for display
454
- */
455
- function formatSyncStatus(status) {
456
- const lines = [];
457
-
458
- lines.push('');
459
- lines.push('Cross-Tool Sync Status');
460
- lines.push('='.repeat(50));
461
- lines.push('');
462
-
463
- const statusEmoji = status.inSync ? '✓' : '⚠';
464
- lines.push(`Overall: ${status.inSync ? 'In Sync' : 'Out of Sync'} ${statusEmoji}`);
465
- lines.push('');
466
-
467
- if (status.lastSync) {
468
- lines.push(`Last Sync: ${new Date(status.lastSync).toLocaleString()}`);
469
- lines.push('');
470
- }
471
-
472
- lines.push('Tools:');
473
- for (const [toolName, toolStatus] of Object.entries(status.tools)) {
474
- const existsEmoji = toolStatus.exists ? '✓' : '✗';
475
- const changesEmoji = toolStatus.hasChanges ? '⚠' : '';
476
- lines.push(` ${toolName}: ${existsEmoji} ${changesEmoji}`);
477
- }
478
-
479
- if (status.changedTools && status.changedTools.length > 0) {
480
- lines.push('');
481
- lines.push('Changed Tools:');
482
- for (const change of status.changedTools) {
483
- lines.push(` - ${change.tool}`);
484
- }
485
- }
486
-
487
- lines.push('');
488
-
489
- return lines.join('\n');
490
- }
491
-
492
- module.exports = {
493
- // Core functions
494
- detectChangedTool,
495
- propagateContextChange,
496
- checkSyncStatus,
497
- syncAllFromCodebase,
498
- resolveConflict,
499
- getSyncHistory,
500
-
501
- // Utilities
502
- initSyncState,
503
- loadSyncState,
504
- saveSyncState,
505
- calculateFileHash,
506
- getToolContextFiles,
507
- formatSyncStatus,
508
-
509
- // Constants
510
- CONFLICT_STRATEGY,
511
- TOOL_CONTEXT_FILES
512
- };
1
+ /**
2
+ * Cross-Tool Sync Manager
3
+ *
4
+ * Orchestrates automatic synchronization between AI tool contexts.
5
+ * Detects changes in one tool's context and propagates to others.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+ const { getAdapter, getAllAdapters, getAdapterNames } = require('../adapters');
12
+ const { analyzeCodebase } = require('../static-analyzer');
13
+ const { generateAll, initialize: initGenerator } = require('../ai-context-generator');
14
+
15
+ /**
16
+ * Context file paths for each AI tool
17
+ */
18
+ const TOOL_CONTEXT_FILES = {
19
+ claude: ['AI_CONTEXT.md', '.claude/'],
20
+ copilot: ['.github/copilot-instructions.md'],
21
+ cline: ['.clinerules'],
22
+ antigravity: ['.agent/']
23
+ };
24
+
25
+ /**
26
+ * Conflict resolution strategies
27
+ */
28
+ const CONFLICT_STRATEGY = {
29
+ SOURCE_WINS: 'source_wins', // Changed file always wins
30
+ REGENERATE_ALL: 'regenerate_all', // Regenerate all from codebase
31
+ MANUAL: 'manual', // Require manual resolution
32
+ NEWEST: 'newest' // File with newest modification time wins
33
+ };
34
+
35
+ /**
36
+ * Sync state storage path
37
+ */
38
+ function getSyncStatePath(projectRoot) {
39
+ return path.join(projectRoot, '.ai-context', 'sync-state.json');
40
+ }
41
+
42
+ /**
43
+ * Initialize sync state (creates new if doesn't exist, loads existing if present)
44
+ */
45
+ function initSyncState(projectRoot) {
46
+ const statePath = getSyncStatePath(projectRoot);
47
+ const stateDir = path.dirname(statePath);
48
+
49
+ if (!fs.existsSync(stateDir)) {
50
+ fs.mkdirSync(stateDir, { recursive: true });
51
+ }
52
+
53
+ if (!fs.existsSync(statePath)) {
54
+ const initialState = {
55
+ version: '1.0.0',
56
+ lastSync: null,
57
+ toolHashes: {},
58
+ conflicts: [],
59
+ syncHistory: []
60
+ };
61
+ fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2));
62
+ return initialState;
63
+ }
64
+
65
+ try {
66
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
67
+ } catch (error) {
68
+ // File exists but is corrupted, create new state
69
+ const initialState = {
70
+ version: '1.0.0',
71
+ lastSync: null,
72
+ toolHashes: {},
73
+ conflicts: [],
74
+ syncHistory: []
75
+ };
76
+ fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2));
77
+ return initialState;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Load sync state (always loads from disk, doesn't create new)
83
+ */
84
+ function loadSyncState(projectRoot) {
85
+ const statePath = getSyncStatePath(projectRoot);
86
+
87
+ if (!fs.existsSync(statePath)) {
88
+ return null;
89
+ }
90
+
91
+ try {
92
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
93
+ } catch (error) {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Save sync state
100
+ */
101
+ function saveSyncState(projectRoot, state) {
102
+ const statePath = getSyncStatePath(projectRoot);
103
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
104
+ }
105
+
106
+ /**
107
+ * Calculate file hash for change detection
108
+ */
109
+ function calculateFileHash(filePath) {
110
+ if (!fs.existsSync(filePath)) {
111
+ return null;
112
+ }
113
+
114
+ const content = fs.readFileSync(filePath, 'utf-8');
115
+ return crypto.createHash('sha256').update(content).digest('hex');
116
+ }
117
+
118
+ /**
119
+ * Get all context files for a tool
120
+ */
121
+ function getToolContextFiles(toolName, projectRoot) {
122
+ const files = TOOL_CONTEXT_FILES[toolName] || [];
123
+ const results = [];
124
+
125
+ for (const file of files) {
126
+ const fullPath = path.join(projectRoot, file);
127
+
128
+ if (file.endsWith('/')) {
129
+ // Directory - calculate combined hash of all files
130
+ if (fs.existsSync(fullPath)) {
131
+ const dirHash = hashDirectory(fullPath);
132
+ results.push({ path: file, hash: dirHash, isDirectory: true });
133
+ }
134
+ } else {
135
+ // Single file
136
+ if (fs.existsSync(fullPath)) {
137
+ results.push({ path: file, hash: calculateFileHash(fullPath), isDirectory: false });
138
+ }
139
+ }
140
+ }
141
+
142
+ return results;
143
+ }
144
+
145
+ /**
146
+ * Calculate hash for a directory
147
+ */
148
+ function hashDirectory(dirPath) {
149
+ const hash = crypto.createHash('sha256');
150
+ const files = getAllFiles(dirPath);
151
+
152
+ for (const file of files.sort()) {
153
+ const content = fs.readFileSync(file, 'utf-8');
154
+ hash.update(content);
155
+ }
156
+
157
+ return hash.digest('hex');
158
+ }
159
+
160
+ /**
161
+ * Get all files in directory recursively
162
+ */
163
+ function getAllFiles(dirPath) {
164
+ const files = [];
165
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
166
+
167
+ for (const entry of entries) {
168
+ const fullPath = path.join(dirPath, entry.name);
169
+ if (entry.isDirectory()) {
170
+ files.push(...getAllFiles(fullPath));
171
+ } else {
172
+ files.push(fullPath);
173
+ }
174
+ }
175
+
176
+ return files;
177
+ }
178
+
179
+ /**
180
+ * Detect which tool's context has changed
181
+ */
182
+ function detectChangedTool(projectRoot, state) {
183
+ const currentHashes = {};
184
+ const changedTools = [];
185
+
186
+ for (const toolName of getAdapterNames()) {
187
+ const files = getToolContextFiles(toolName, projectRoot);
188
+ const toolHash = files.map(f => f.hash).filter(Boolean).join('|');
189
+
190
+ currentHashes[toolName] = toolHash;
191
+
192
+ // Use hasOwnProperty check to handle empty string case
193
+ const hasStoredHash = Object.prototype.hasOwnProperty.call(state.toolHashes, toolName);
194
+ const storedHash = state.toolHashes[toolName];
195
+
196
+ if (hasStoredHash && storedHash !== toolHash) {
197
+ changedTools.push({
198
+ tool: toolName,
199
+ previousHash: storedHash,
200
+ currentHash: toolHash
201
+ });
202
+ }
203
+ }
204
+
205
+ return { changedTools, currentHashes };
206
+ }
207
+
208
+ /**
209
+ * Propagate context change from source tool to all other tools
210
+ */
211
+ async function propagateContextChange(sourceTool, projectRoot, config, strategy = CONFLICT_STRATEGY.SOURCE_WINS) {
212
+ const results = {
213
+ sourceTool,
214
+ strategy,
215
+ timestamp: new Date().toISOString(),
216
+ propagated: [],
217
+ skipped: [],
218
+ errors: []
219
+ };
220
+
221
+ // 1. Re-analyze codebase to get fresh analysis
222
+ let analysis;
223
+ try {
224
+ analysis = await analyzeCodebase(projectRoot, config);
225
+ } catch (error) {
226
+ results.errors.push({
227
+ message: `Failed to analyze project: ${error.message}`
228
+ });
229
+ return results;
230
+ }
231
+
232
+ // 2. Get all adapters except source
233
+ const allAdapters = getAllAdapters();
234
+ const targetAdapters = allAdapters.filter(a => a.name !== sourceTool);
235
+
236
+ // 3. Generate contexts for all target tools
237
+ initGenerator();
238
+
239
+ for (const adapter of targetAdapters) {
240
+ try {
241
+ const result = await adapter.generate(analysis, config, projectRoot);
242
+
243
+ if (result.success) {
244
+ results.propagated.push({
245
+ tool: adapter.name,
246
+ displayName: adapter.displayName,
247
+ files: result.files
248
+ });
249
+ } else {
250
+ results.errors.push({
251
+ tool: adapter.name,
252
+ errors: result.errors
253
+ });
254
+ }
255
+ } catch (error) {
256
+ results.errors.push({
257
+ tool: adapter.name,
258
+ message: error.message
259
+ });
260
+ }
261
+ }
262
+
263
+ // 4. Update sync state
264
+ const state = initSyncState(projectRoot);
265
+ const { currentHashes } = detectChangedTool(projectRoot, state);
266
+ state.toolHashes = currentHashes;
267
+ state.lastSync = new Date().toISOString();
268
+ state.syncHistory.push({
269
+ timestamp: new Date().toISOString(),
270
+ sourceTool,
271
+ strategy,
272
+ propagatedCount: results.propagated.length,
273
+ errorCount: results.errors.length
274
+ });
275
+ saveSyncState(projectRoot, state);
276
+
277
+ return results;
278
+ }
279
+
280
+ /**
281
+ * Check if contexts are out of sync
282
+ */
283
+ function checkSyncStatus(projectRoot) {
284
+ const state = loadSyncState(projectRoot) || initSyncState(projectRoot);
285
+ const status = {
286
+ inSync: true,
287
+ tools: {},
288
+ lastSync: state.lastSync,
289
+ conflicts: []
290
+ };
291
+
292
+ const { changedTools, currentHashes } = detectChangedTool(projectRoot, state);
293
+
294
+ for (const toolName of getAdapterNames()) {
295
+ const files = getToolContextFiles(toolName, projectRoot);
296
+ const exists = files.length > 0;
297
+ const hasChanges = changedTools.some(c => c.tool === toolName);
298
+
299
+ status.tools[toolName] = {
300
+ exists,
301
+ hasChanges,
302
+ hash: currentHashes[toolName],
303
+ previousHash: state.toolHashes[toolName]
304
+ };
305
+
306
+ if (hasChanges && !state.lastSync) {
307
+ status.inSync = false;
308
+ }
309
+ }
310
+
311
+ if (changedTools.length > 0 && state.lastSync) {
312
+ status.inSync = false;
313
+ status.changedTools = changedTools;
314
+ }
315
+
316
+ return status;
317
+ }
318
+
319
+ /**
320
+ * Update sync state only (no file regeneration)
321
+ * Used by post-commit hooks to avoid creating new uncommitted changes
322
+ */
323
+ function updateSyncStateOnly(projectRoot) {
324
+ const state = initSyncState(projectRoot);
325
+ const { currentHashes } = detectChangedTool(projectRoot, state);
326
+
327
+ // Only update hashes, don't regenerate files
328
+ state.toolHashes = currentHashes;
329
+ state.lastSync = new Date().toISOString();
330
+ state.syncHistory.push({
331
+ timestamp: new Date().toISOString(),
332
+ source: 'post-commit',
333
+ strategy: 'state_only',
334
+ propagatedCount: 0,
335
+ errorCount: 0
336
+ });
337
+ saveSyncState(projectRoot, state);
338
+
339
+ return {
340
+ updated: true,
341
+ timestamp: state.lastSync,
342
+ hashes: currentHashes
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Sync all tools from codebase (fresh regeneration)
348
+ */
349
+ async function syncAllFromCodebase(projectRoot, config) {
350
+ const results = {
351
+ timestamp: new Date().toISOString(),
352
+ tools: [],
353
+ errors: []
354
+ };
355
+
356
+ try {
357
+ // Analyze project
358
+ const analysis = await analyzeCodebase(projectRoot, config);
359
+
360
+ // Generate for all tools
361
+ const generateResults = await generateAll(analysis, config, projectRoot, {
362
+ aiTools: getAdapterNames()
363
+ });
364
+
365
+ results.tools = generateResults.generated.map(g => ({
366
+ tool: g.adapter,
367
+ displayName: g.displayName,
368
+ fileCount: g.files.length
369
+ }));
370
+
371
+ results.errors = generateResults.errors;
372
+
373
+ // Update sync state
374
+ const state = initSyncState(projectRoot);
375
+ const { currentHashes } = detectChangedTool(projectRoot, state);
376
+ state.toolHashes = currentHashes;
377
+ state.lastSync = new Date().toISOString();
378
+ state.syncHistory.push({
379
+ timestamp: new Date().toISOString(),
380
+ source: 'codebase',
381
+ strategy: 'regenerate_all',
382
+ propagatedCount: results.tools.length,
383
+ errorCount: results.errors.length
384
+ });
385
+ saveSyncState(projectRoot, state);
386
+
387
+ } catch (error) {
388
+ results.errors.push({
389
+ message: `Sync failed: ${error.message}`,
390
+ stack: error.stack
391
+ });
392
+ }
393
+
394
+ return results;
395
+ }
396
+
397
+ /**
398
+ * Resolve conflict between tools
399
+ */
400
+ async function resolveConflict(projectRoot, config, strategy, preferredTool = null) {
401
+ const status = checkSyncStatus(projectRoot);
402
+
403
+ if (status.inSync) {
404
+ return {
405
+ resolved: true,
406
+ message: 'No conflicts to resolve'
407
+ };
408
+ }
409
+
410
+ switch (strategy) {
411
+ case CONFLICT_STRATEGY.SOURCE_WINS:
412
+ if (!preferredTool) {
413
+ return {
414
+ resolved: false,
415
+ message: 'Source strategy requires a preferred tool'
416
+ };
417
+ }
418
+ return await propagateContextChange(preferredTool, projectRoot, config, strategy);
419
+
420
+ case CONFLICT_STRATEGY.REGENERATE_ALL:
421
+ return await syncAllFromCodebase(projectRoot, config);
422
+
423
+ case CONFLICT_STRATEGY.NEWEST:
424
+ // Find tool with most recent change
425
+ const newestTool = findNewestTool(projectRoot, status);
426
+ return await propagateContextChange(newestTool, projectRoot, config, strategy);
427
+
428
+ case CONFLICT_STRATEGY.MANUAL:
429
+ return {
430
+ resolved: false,
431
+ message: 'Manual resolution required',
432
+ status
433
+ };
434
+
435
+ default:
436
+ return {
437
+ resolved: false,
438
+ message: `Unknown strategy: ${strategy}`
439
+ };
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Find tool with most recently modified context
445
+ */
446
+ function findNewestTool(projectRoot, status) {
447
+ let newestTool = null;
448
+ let newestTime = 0;
449
+
450
+ for (const [toolName, toolStatus] of Object.entries(status.tools)) {
451
+ const files = getToolContextFiles(toolName, projectRoot);
452
+
453
+ for (const file of files) {
454
+ const fullPath = path.join(projectRoot, file.path);
455
+
456
+ if (file.isDirectory) {
457
+ continue; // Skip directories for mtime check
458
+ }
459
+
460
+ const stats = fs.statSync(fullPath);
461
+ if (stats.mtimeMs > newestTime) {
462
+ newestTime = stats.mtimeMs;
463
+ newestTool = toolName;
464
+ }
465
+ }
466
+ }
467
+
468
+ return newestTool;
469
+ }
470
+
471
+ /**
472
+ * Get sync history
473
+ */
474
+ function getSyncHistory(projectRoot, limit = 10) {
475
+ const state = initSyncState(projectRoot);
476
+ return state.syncHistory.slice(-limit);
477
+ }
478
+
479
+ /**
480
+ * Format sync status for display
481
+ */
482
+ function formatSyncStatus(status) {
483
+ const lines = [];
484
+
485
+ lines.push('');
486
+ lines.push('Cross-Tool Sync Status');
487
+ lines.push('='.repeat(50));
488
+ lines.push('');
489
+
490
+ const statusEmoji = status.inSync ? '✓' : '⚠';
491
+ lines.push(`Overall: ${status.inSync ? 'In Sync' : 'Out of Sync'} ${statusEmoji}`);
492
+ lines.push('');
493
+
494
+ if (status.lastSync) {
495
+ lines.push(`Last Sync: ${new Date(status.lastSync).toLocaleString()}`);
496
+ lines.push('');
497
+ }
498
+
499
+ lines.push('Tools:');
500
+ for (const [toolName, toolStatus] of Object.entries(status.tools)) {
501
+ const existsEmoji = toolStatus.exists ? '✓' : '✗';
502
+ const changesEmoji = toolStatus.hasChanges ? '⚠' : '';
503
+ lines.push(` ${toolName}: ${existsEmoji} ${changesEmoji}`);
504
+ }
505
+
506
+ if (status.changedTools && status.changedTools.length > 0) {
507
+ lines.push('');
508
+ lines.push('Changed Tools:');
509
+ for (const change of status.changedTools) {
510
+ lines.push(` - ${change.tool}`);
511
+ }
512
+ }
513
+
514
+ lines.push('');
515
+
516
+ return lines.join('\n');
517
+ }
518
+
519
+ module.exports = {
520
+ // Core functions
521
+ detectChangedTool,
522
+ propagateContextChange,
523
+ checkSyncStatus,
524
+ syncAllFromCodebase,
525
+ updateSyncStateOnly,
526
+ resolveConflict,
527
+ getSyncHistory,
528
+
529
+ // Utilities
530
+ initSyncState,
531
+ loadSyncState,
532
+ saveSyncState,
533
+ calculateFileHash,
534
+ getToolContextFiles,
535
+ formatSyncStatus,
536
+
537
+ // Constants
538
+ CONFLICT_STRATEGY,
539
+ TOOL_CONTEXT_FILES
540
+ };