create-universal-ai-context 2.1.3 → 2.2.1

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 CHANGED
@@ -67,6 +67,97 @@ npx create-universal-ai-context drift --fix # Auto-fix issues
67
67
  npx create-universal-ai-context drift --strict # Exit 1 on issues (CI)
68
68
  ```
69
69
 
70
+ ## Cross-Tool Sync
71
+
72
+ Keep all AI tool contexts synchronized automatically:
73
+
74
+ ```bash
75
+ # Check sync status
76
+ npx create-universal-ai-context sync:check
77
+
78
+ # Sync all tools from codebase
79
+ npx create-universal-ai-context sync:all
80
+
81
+ # Propagate from specific tool
82
+ npx create-universal-ai-context sync:from claude --strategy source_wins
83
+
84
+ # Resolve conflicts
85
+ npx create-universal-ai-context sync:resolve --strategy regenerate_all
86
+
87
+ # View sync history
88
+ npx create-universal-ai-context sync:history
89
+
90
+ # Install git hooks for automatic sync
91
+ npx create-universal-ai-context hooks:install
92
+ ```
93
+
94
+ **Conflict Strategies:**
95
+ - `source_wins` - Changed tool's context wins
96
+ - `regenerate_all` - Regenerate all from codebase
97
+ - `newest` - Most recently modified wins
98
+ - `manual` - Require manual resolution
99
+
100
+ ## Automation Scripts
101
+
102
+ The generated `.ai-context/` directory includes automation that keeps documentation synchronized:
103
+
104
+ ### Generators
105
+
106
+ **Code Mapper** (`.ai-context/automation/generators/code-mapper.js`)
107
+ - Scans workflow files for file:line references
108
+ - Builds CODE_TO_WORKFLOW_MAP.md (reverse index of code → docs)
109
+ - Shows which workflows reference each file
110
+ - Helps identify what needs updating after code changes
111
+
112
+ **Index Builder** (`.ai-context/automation/generators/index-builder.js`)
113
+ - Regenerates category indexes automatically
114
+ - Updates navigation indexes with accurate counts
115
+ - Scans workflows, agents, and commands for metadata
116
+
117
+ ### Git Hooks
118
+
119
+ **Pre-Commit Hook**
120
+ - Validates before allowing commits
121
+ - Warns if code changes might affect documentation
122
+ - Optionally blocks commits if docs are stale
123
+
124
+ **Post-Commit Hook**
125
+ - Rebuilds CODE_TO_WORKFLOW_MAP.md in background
126
+ - Updates file hashes for change tracking
127
+ - Keeps indexes synchronized
128
+
129
+ ### Install Hooks
130
+
131
+ ```bash
132
+ # Copy hooks to .git/hooks/
133
+ cp .ai-context/automation/hooks/pre-commit.sh .git/hooks/pre-commit
134
+ cp .ai-context/automation/hooks/post-commit.sh .git/hooks/post-commit
135
+ chmod +x .git/hooks/pre-commit .git/hooks/post-commit
136
+ ```
137
+
138
+ ### Run Manually
139
+
140
+ ```bash
141
+ # Regenerate code-to-workflow map
142
+ node .ai-context/automation/generators/code-mapper.js
143
+
144
+ # Rebuild all indexes
145
+ node .ai-context/automation/generators/index-builder.js
146
+
147
+ # Dry-run to preview
148
+ node .ai-context/automation/generators/code-mapper.js --dry-run
149
+ ```
150
+
151
+ ### Configuration
152
+
153
+ Control automation via `.ai-context/automation/config.json`:
154
+ - Enable/disable generators
155
+ - Configure hook behavior
156
+ - Set drift check sensitivity
157
+ - Toggle blocking on stale documentation
158
+
159
+ ---
160
+
70
161
  ## Existing Documentation Detection
71
162
 
72
163
  The CLI automatically detects existing AI context files:
@@ -31,6 +31,15 @@ const {
31
31
  checkDocumentDrift,
32
32
  formatDriftReportConsole
33
33
  } = require('../lib/drift-checker');
34
+ const {
35
+ checkSyncStatus,
36
+ syncAllFromCodebase,
37
+ propagateContextChange,
38
+ resolveConflict,
39
+ formatSyncStatus,
40
+ getSyncHistory,
41
+ CONFLICT_STRATEGY
42
+ } = require('../lib/cross-tool-sync');
34
43
  const packageJson = require('../package.json');
35
44
 
36
45
  // ASCII Banner
@@ -492,4 +501,257 @@ function formatDriftReportMarkdown(report) {
492
501
  return lines.join('\n');
493
502
  }
494
503
 
504
+ // Sync subcommands
505
+ program
506
+ .command('sync:check')
507
+ .description('Check if AI tool contexts are synchronized')
508
+ .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
509
+ .option('--json', 'Output as JSON')
510
+ .action(async (options) => {
511
+ console.log(banner);
512
+
513
+ const projectRoot = path.resolve(options.path);
514
+
515
+ try {
516
+ const status = checkSyncStatus(projectRoot);
517
+
518
+ if (options.json) {
519
+ console.log(JSON.stringify(status, null, 2));
520
+ } else {
521
+ console.log(formatSyncStatus(status));
522
+
523
+ if (!status.inSync) {
524
+ console.log(chalk.yellow('\nTo sync all contexts, run:'));
525
+ console.log(chalk.gray(' npx create-ai-context sync:all'));
526
+ process.exit(1);
527
+ }
528
+ }
529
+ } catch (error) {
530
+ console.error(chalk.red('\n✖ Error:'), error.message);
531
+ process.exit(1);
532
+ }
533
+ });
534
+
535
+ program
536
+ .command('sync:all')
537
+ .description('Synchronize all AI tool contexts from codebase')
538
+ .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
539
+ .option('--quiet', 'Suppress output')
540
+ .action(async (options) => {
541
+ if (!options.quiet) {
542
+ console.log(banner);
543
+ }
544
+
545
+ const projectRoot = path.resolve(options.path);
546
+ const spinner = createSpinner();
547
+
548
+ try {
549
+ if (!options.quiet) {
550
+ spinner.start('Analyzing codebase...');
551
+ }
552
+
553
+ const config = {
554
+ projectName: path.basename(projectRoot),
555
+ aiTools: ['claude', 'copilot', 'cline', 'antigravity']
556
+ };
557
+
558
+ const results = await syncAllFromCodebase(projectRoot, config);
559
+
560
+ if (!options.quiet) {
561
+ if (results.errors.length > 0) {
562
+ spinner.warn('Sync completed with errors');
563
+
564
+ console.log(chalk.bold('\nSynced tools:'));
565
+ for (const tool of results.tools) {
566
+ console.log(chalk.green(` ✓ ${tool.tool} (${tool.fileCount} files)`));
567
+ }
568
+
569
+ console.log(chalk.red('\nErrors:'));
570
+ for (const error of results.errors) {
571
+ console.error(chalk.red(` ✖ ${error.message || error.tool}`));
572
+ }
573
+ process.exit(1);
574
+ } else {
575
+ spinner.succeed(`Synced ${results.tools.length} AI tools`);
576
+
577
+ console.log(chalk.bold('\nSynced tools:'));
578
+ for (const tool of results.tools) {
579
+ console.log(chalk.green(` ✓ ${tool.tool} (${tool.fileCount} files)`));
580
+ }
581
+ }
582
+ }
583
+ } catch (error) {
584
+ if (!options.quiet) {
585
+ spinner.fail('Sync failed');
586
+ console.error(chalk.red('\n✖ Error:'), error.message);
587
+ }
588
+ process.exit(1);
589
+ }
590
+ });
591
+
592
+ program
593
+ .command('sync:from <tool>')
594
+ .description('Propagate context from a specific tool to all others')
595
+ .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
596
+ .option('-s, --strategy <strategy>', 'Conflict resolution strategy', 'source_wins')
597
+ .action(async (sourceTool, options) => {
598
+ console.log(banner);
599
+
600
+ const projectRoot = path.resolve(options.path);
601
+ const spinner = createSpinner();
602
+
603
+ const validTools = ['claude', 'copilot', 'cline', 'antigravity'];
604
+ if (!validTools.includes(sourceTool)) {
605
+ console.error(chalk.red(`\n✖ Error: Invalid tool: ${sourceTool}`));
606
+ console.error(chalk.gray(` Valid options: ${validTools.join(', ')}`));
607
+ process.exit(1);
608
+ }
609
+
610
+ try {
611
+ spinner.start(`Propagating from ${sourceTool}...`);
612
+
613
+ const config = {
614
+ projectName: path.basename(projectRoot),
615
+ aiTools: validTools
616
+ };
617
+
618
+ const results = await propagateContextChange(
619
+ sourceTool,
620
+ projectRoot,
621
+ config,
622
+ options.strategy
623
+ );
624
+
625
+ if (results.errors.length > 0) {
626
+ spinner.warn('Propagation completed with errors');
627
+
628
+ console.log(chalk.bold('\nPropagated to:'));
629
+ for (const tool of results.propagated) {
630
+ console.log(chalk.green(` ✓ ${tool.displayName}`));
631
+ }
632
+
633
+ console.log(chalk.red('\nErrors:'));
634
+ for (const error of results.errors) {
635
+ console.error(chalk.red(` ✖ ${error.tool || error.message}`));
636
+ }
637
+ process.exit(1);
638
+ } else {
639
+ spinner.succeed(`Propagated to ${results.propagated.length} tools`);
640
+
641
+ console.log(chalk.bold('\nPropagated to:'));
642
+ for (const tool of results.propagated) {
643
+ console.log(chalk.green(` ✓ ${tool.displayName}`));
644
+ }
645
+ }
646
+ } catch (error) {
647
+ spinner.fail('Propagation failed');
648
+ console.error(chalk.red('\n✖ Error:'), error.message);
649
+ process.exit(1);
650
+ }
651
+ });
652
+
653
+ program
654
+ .command('sync:resolve')
655
+ .description('Resolve conflicts between AI tool contexts')
656
+ .option('-s, --strategy <strategy>', 'Strategy: source_wins, regenerate_all, newest, manual', 'regenerate_all')
657
+ .option('-t, --tool <tool>', 'Preferred tool (for source_wins strategy)')
658
+ .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
659
+ .action(async (options) => {
660
+ console.log(banner);
661
+
662
+ const projectRoot = path.resolve(options.path);
663
+ const spinner = createSpinner();
664
+
665
+ const validStrategies = Object.values(CONFLICT_STRATEGY);
666
+ if (!validStrategies.includes(options.strategy)) {
667
+ console.error(chalk.red(`\n✖ Error: Invalid strategy: ${options.strategy}`));
668
+ console.error(chalk.gray(` Valid options: ${validStrategies.join(', ')}`));
669
+ process.exit(1);
670
+ }
671
+
672
+ try {
673
+ spinner.start(`Resolving conflicts (${options.strategy})...`);
674
+
675
+ const config = {
676
+ projectName: path.basename(projectRoot),
677
+ aiTools: ['claude', 'copilot', 'cline', 'antigravity']
678
+ };
679
+
680
+ const result = await resolveConflict(
681
+ projectRoot,
682
+ config,
683
+ options.strategy,
684
+ options.tool
685
+ );
686
+
687
+ if (result.resolved) {
688
+ spinner.succeed(result.message);
689
+ } else {
690
+ spinner.warn('Unable to resolve');
691
+ console.log(chalk.yellow(`\n${result.message}`));
692
+
693
+ if (result.status) {
694
+ console.log(formatSyncStatus(result.status));
695
+ }
696
+ process.exit(1);
697
+ }
698
+ } catch (error) {
699
+ spinner.fail('Resolution failed');
700
+ console.error(chalk.red('\n✖ Error:'), error.message);
701
+ process.exit(1);
702
+ }
703
+ });
704
+
705
+ program
706
+ .command('sync:history')
707
+ .description('Show sync history')
708
+ .option('-n, --limit <number>', 'Number of entries to show', '10')
709
+ .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
710
+ .action(async (options) => {
711
+ console.log(banner);
712
+
713
+ const projectRoot = path.resolve(options.path);
714
+
715
+ try {
716
+ const history = getSyncHistory(projectRoot, parseInt(options.limit, 10));
717
+
718
+ console.log(chalk.bold('\nSync History:\n'));
719
+
720
+ if (history.length === 0) {
721
+ console.log(chalk.gray(' No sync history found\n'));
722
+ } else {
723
+ for (const entry of history.reverse()) {
724
+ const date = new Date(entry.timestamp).toLocaleString();
725
+ console.log(chalk.cyan(` ${date}`));
726
+ console.log(chalk.gray(` Source: ${entry.source || entry.sourceTool}`));
727
+ console.log(chalk.gray(` Strategy: ${entry.strategy}`));
728
+ console.log(chalk.gray(` Propagated: ${entry.propagatedCount} tools`));
729
+
730
+ if (entry.errorCount > 0) {
731
+ console.log(chalk.red(` Errors: ${entry.errorCount}`));
732
+ }
733
+ console.log('');
734
+ }
735
+ }
736
+ } catch (error) {
737
+ console.error(chalk.red('\n✖ Error:'), error.message);
738
+ process.exit(1);
739
+ }
740
+ });
741
+
742
+ program
743
+ .command('hooks:install')
744
+ .description('Install git hooks for automatic sync')
745
+ .action(async () => {
746
+ console.log(banner);
747
+
748
+ try {
749
+ const { installHooks } = require('../lib/install-hooks');
750
+ installHooks();
751
+ } catch (error) {
752
+ console.error(chalk.red('\n✖ Error:'), error.message);
753
+ process.exit(1);
754
+ }
755
+ });
756
+
495
757
  program.parse();
@@ -0,0 +1,274 @@
1
+ /**
2
+ * File Watcher for Cross-Tool Sync
3
+ *
4
+ * Monitors AI tool context files for changes and triggers synchronization.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { EventEmitter } = require('events');
10
+
11
+ /**
12
+ * Simple file watcher implementation (no chokidar dependency)
13
+ * Uses polling to detect file changes
14
+ */
15
+ class FileWatcher extends EventEmitter {
16
+ constructor(options = {}) {
17
+ super();
18
+ this.watchPaths = new Map();
19
+ this.fileStates = new Map();
20
+ this.pollInterval = options.pollInterval || 1000; // 1 second default
21
+ this.intervalId = null;
22
+ this.running = false;
23
+ }
24
+
25
+ /**
26
+ * Add a file or directory to watch
27
+ */
28
+ watch(watchPath, projectRoot) {
29
+ const fullPath = path.isAbsolute(watchPath)
30
+ ? watchPath
31
+ : path.join(projectRoot, watchPath);
32
+
33
+ const key = fullPath.toLowerCase();
34
+
35
+ if (this.watchPaths.has(key)) {
36
+ return;
37
+ }
38
+
39
+ // Store initial state
40
+ this.recordFileState(fullPath, key);
41
+
42
+ this.watchPaths.set(key, {
43
+ path: fullPath,
44
+ projectRoot,
45
+ isDirectory: this.isDirectory(fullPath)
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Record current state of a file/directory
51
+ */
52
+ recordFileState(filePath, key) {
53
+ try {
54
+ if (fs.existsSync(filePath)) {
55
+ const stats = fs.statSync(filePath);
56
+
57
+ if (stats.isDirectory()) {
58
+ // For directories, hash all files
59
+ this.fileStates.set(key, {
60
+ mtimeMs: stats.mtimeMs,
61
+ hash: this.hashDirectory(filePath),
62
+ exists: true
63
+ });
64
+ } else {
65
+ // For files, use content hash
66
+ const content = fs.readFileSync(filePath, 'utf-8');
67
+ const crypto = require('crypto');
68
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
69
+
70
+ this.fileStates.set(key, {
71
+ mtimeMs: stats.mtimeMs,
72
+ hash,
73
+ exists: true
74
+ });
75
+ }
76
+ } else {
77
+ this.fileStates.set(key, { exists: false });
78
+ }
79
+ } catch (error) {
80
+ this.fileStates.set(key, { exists: false, error: error.message });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if path is a directory
86
+ */
87
+ isDirectory(filePath) {
88
+ try {
89
+ return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Hash all files in a directory
97
+ */
98
+ hashDirectory(dirPath) {
99
+ const crypto = require('crypto');
100
+ const hash = crypto.createHash('sha256');
101
+ const files = this.getAllFiles(dirPath);
102
+
103
+ for (const file of files.sort()) {
104
+ try {
105
+ const content = fs.readFileSync(file, 'utf-8');
106
+ hash.update(content);
107
+ } catch {
108
+ // Skip files that can't be read
109
+ }
110
+ }
111
+
112
+ return hash.digest('hex');
113
+ }
114
+
115
+ /**
116
+ * Get all files in directory recursively
117
+ */
118
+ getAllFiles(dirPath) {
119
+ const files = [];
120
+
121
+ try {
122
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ const fullPath = path.join(dirPath, entry.name);
126
+
127
+ if (entry.isDirectory()) {
128
+ files.push(...this.getAllFiles(fullPath));
129
+ } else {
130
+ files.push(fullPath);
131
+ }
132
+ }
133
+ } catch {
134
+ // Skip directories that can't be read
135
+ }
136
+
137
+ return files;
138
+ }
139
+
140
+ /**
141
+ * Start watching
142
+ */
143
+ start() {
144
+ if (this.running) {
145
+ return;
146
+ }
147
+
148
+ this.running = true;
149
+ this.intervalId = setInterval(() => {
150
+ this.checkForChanges();
151
+ }, this.pollInterval);
152
+
153
+ this.emit('ready');
154
+ }
155
+
156
+ /**
157
+ * Stop watching
158
+ */
159
+ stop() {
160
+ if (!this.running) {
161
+ return;
162
+ }
163
+
164
+ this.running = false;
165
+
166
+ if (this.intervalId) {
167
+ clearInterval(this.intervalId);
168
+ this.intervalId = null;
169
+ }
170
+
171
+ this.emit('stopped');
172
+ }
173
+
174
+ /**
175
+ * Check all watched paths for changes
176
+ */
177
+ checkForChanges() {
178
+ for (const [key, watchInfo] of this.watchPaths.entries()) {
179
+ this.checkPath(key, watchInfo);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Check a single path for changes
185
+ */
186
+ checkPath(key, watchInfo) {
187
+ const { path: filePath } = watchInfo;
188
+ const oldState = this.fileStates.get(key);
189
+
190
+ if (!oldState) {
191
+ return;
192
+ }
193
+
194
+ // Record new state
195
+ this.recordFileState(filePath, key);
196
+ const newState = this.fileStates.get(key);
197
+
198
+ // Detect changes
199
+ if (!oldState.exists && newState.exists) {
200
+ // File/directory was created
201
+ this.emit('created', {
202
+ path: filePath,
203
+ ...watchInfo
204
+ });
205
+ } else if (oldState.exists && !newState.exists) {
206
+ // File/directory was deleted
207
+ this.emit('deleted', {
208
+ path: filePath,
209
+ ...watchInfo
210
+ });
211
+ } else if (oldState.exists && newState.exists) {
212
+ // Check for modifications
213
+ if (oldState.hash !== newState.hash) {
214
+ this.emit('changed', {
215
+ path: filePath,
216
+ ...watchInfo,
217
+ previousHash: oldState.hash,
218
+ currentHash: newState.hash
219
+ });
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get current watch list
226
+ */
227
+ getWatchedPaths() {
228
+ return Array.from(this.watchPaths.values()).map(w => w.path);
229
+ }
230
+
231
+ /**
232
+ * Unwatch a specific path
233
+ */
234
+ unwatch(watchPath) {
235
+ const fullPath = path.isAbsolute(watchPath)
236
+ ? watchPath
237
+ : path.join(process.cwd(), watchPath);
238
+
239
+ const key = fullPath.toLowerCase();
240
+ this.watchPaths.delete(key);
241
+ this.fileStates.delete(key);
242
+ }
243
+
244
+ /**
245
+ * Unwatch all paths
246
+ */
247
+ unwatchAll() {
248
+ this.watchPaths.clear();
249
+ this.fileStates.clear();
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a file watcher for AI tool contexts
255
+ */
256
+ function createToolContextWatcher(projectRoot, options = {}) {
257
+ const watcher = new FileWatcher(options);
258
+
259
+ // Add context files for all tools
260
+ const { TOOL_CONTEXT_FILES } = require('./sync-manager');
261
+
262
+ for (const [toolName, files] of Object.entries(TOOL_CONTEXT_FILES)) {
263
+ for (const file of files) {
264
+ watcher.watch(file, projectRoot);
265
+ }
266
+ }
267
+
268
+ return watcher;
269
+ }
270
+
271
+ module.exports = {
272
+ FileWatcher,
273
+ createToolContextWatcher
274
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Cross-Tool Sync Module
3
+ *
4
+ * Exports all synchronization functionality for automatic cross-tool
5
+ * context synchronization.
6
+ */
7
+
8
+ const SyncManager = require('./sync-manager');
9
+ const { FileWatcher, createToolContextWatcher } = require('./file-watcher');
10
+ const { SyncService, createSyncService, DEFAULT_CONFIG } = require('./sync-service');
11
+
12
+ module.exports = {
13
+ // Sync Manager - Core sync logic
14
+ ...SyncManager,
15
+
16
+ // File Watcher - Change detection
17
+ FileWatcher,
18
+ createToolContextWatcher,
19
+
20
+ // Sync Service - Background service
21
+ SyncService,
22
+ createSyncService,
23
+ DEFAULT_CONFIG
24
+ };
25
+
26
+ // Also export individual functions for named imports
27
+ module.exports.detectChangedTool = SyncManager.detectChangedTool;
28
+ module.exports.propagateContextChange = SyncManager.propagateContextChange;
29
+ module.exports.checkSyncStatus = SyncManager.checkSyncStatus;
30
+ module.exports.syncAllFromCodebase = SyncManager.syncAllFromCodebase;
31
+ module.exports.resolveConflict = SyncManager.resolveConflict;
32
+ module.exports.getSyncHistory = SyncManager.getSyncHistory;
33
+ module.exports.initSyncState = SyncManager.initSyncState;
34
+ module.exports.loadSyncState = SyncManager.loadSyncState;
35
+ module.exports.saveSyncState = SyncManager.saveSyncState;
36
+ module.exports.calculateFileHash = SyncManager.calculateFileHash;
37
+ module.exports.getToolContextFiles = SyncManager.getToolContextFiles;
38
+ module.exports.formatSyncStatus = SyncManager.formatSyncStatus;
39
+ module.exports.CONFLICT_STRATEGY = SyncManager.CONFLICT_STRATEGY;
40
+ module.exports.TOOL_CONTEXT_FILES = SyncManager.TOOL_CONTEXT_FILES;