create-universal-ai-context 2.1.2 → 2.2.0

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,36 @@ 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
+
70
100
  ## Existing Documentation Detection
71
101
 
72
102
  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,263 @@ 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
+ const installScript = path.join(__dirname, '..', '.claude', 'automation', 'hooks', 'install.js');
749
+
750
+ if (!fs.existsSync(installScript)) {
751
+ console.error(chalk.red('\n✖ Error: Install script not found'));
752
+ process.exit(1);
753
+ }
754
+
755
+ try {
756
+ require(installScript);
757
+ } catch (error) {
758
+ console.error(chalk.red('\n✖ Error:'), error.message);
759
+ process.exit(1);
760
+ }
761
+ });
762
+
495
763
  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;