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:
|
package/bin/create-ai-context.js
CHANGED
|
@@ -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;
|