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.
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Tool Sync Service
|
|
3
|
+
*
|
|
4
|
+
* Background service that monitors context files and auto-syncs changes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { createToolContextWatcher } = require('./file-watcher');
|
|
9
|
+
const { propagateContextChange, checkSyncStatus, formatSyncStatus, CONFLICT_STRATEGY } = require('./sync-manager');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sync service configuration
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
// Polling interval for file changes (ms)
|
|
16
|
+
pollInterval: 1000,
|
|
17
|
+
|
|
18
|
+
// Debounce delay before triggering sync (ms)
|
|
19
|
+
debounceDelay: 2000,
|
|
20
|
+
|
|
21
|
+
// Auto-sync strategy
|
|
22
|
+
strategy: CONFLICT_STRATEGY.SOURCE_WINS,
|
|
23
|
+
|
|
24
|
+
// Enable/disable auto-sync
|
|
25
|
+
enabled: true,
|
|
26
|
+
|
|
27
|
+
// Log verbosity
|
|
28
|
+
verbose: false,
|
|
29
|
+
|
|
30
|
+
// Callbacks
|
|
31
|
+
onSyncStart: null,
|
|
32
|
+
onSyncComplete: null,
|
|
33
|
+
onError: null
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cross-tool sync service
|
|
38
|
+
*/
|
|
39
|
+
class SyncService {
|
|
40
|
+
constructor(projectRoot, config = {}) {
|
|
41
|
+
this.projectRoot = projectRoot;
|
|
42
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
43
|
+
|
|
44
|
+
this.watcher = null;
|
|
45
|
+
this.syncTimeouts = new Map();
|
|
46
|
+
this.isRunning = false;
|
|
47
|
+
this.syncInProgress = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start the sync service
|
|
52
|
+
*/
|
|
53
|
+
start() {
|
|
54
|
+
if (this.isRunning) {
|
|
55
|
+
this.log('Sync service already running');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.log('Starting cross-tool sync service...');
|
|
60
|
+
|
|
61
|
+
// Create file watcher
|
|
62
|
+
this.watcher = createToolContextWatcher(this.projectRoot, {
|
|
63
|
+
pollInterval: this.config.pollInterval
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Set up event listeners
|
|
67
|
+
this.watcher.on('changed', (event) => this.handleFileChanged(event));
|
|
68
|
+
this.watcher.on('created', (event) => this.handleFileCreated(event));
|
|
69
|
+
this.watcher.on('deleted', (event) => this.handleFileDeleted(event));
|
|
70
|
+
|
|
71
|
+
// Start watching
|
|
72
|
+
this.watcher.start();
|
|
73
|
+
this.isRunning = true;
|
|
74
|
+
|
|
75
|
+
this.log('Sync service started');
|
|
76
|
+
this.log(`Watching: ${this.watcher.getWatchedPaths().join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stop the sync service
|
|
81
|
+
*/
|
|
82
|
+
stop() {
|
|
83
|
+
if (!this.isRunning) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.log('Stopping sync service...');
|
|
88
|
+
|
|
89
|
+
if (this.watcher) {
|
|
90
|
+
this.watcher.stop();
|
|
91
|
+
this.watcher.unwatchAll();
|
|
92
|
+
this.watcher = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Clear any pending syncs
|
|
96
|
+
for (const timeout of this.syncTimeouts.values()) {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
this.syncTimeouts.clear();
|
|
100
|
+
|
|
101
|
+
this.isRunning = false;
|
|
102
|
+
this.log('Sync service stopped');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Handle file changed event
|
|
107
|
+
*/
|
|
108
|
+
handleFileChanged(event) {
|
|
109
|
+
const toolName = this.detectToolFromPath(event.path);
|
|
110
|
+
|
|
111
|
+
if (!toolName) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.log(`Detected change in ${toolName}: ${event.path}`);
|
|
116
|
+
|
|
117
|
+
// Debounce sync
|
|
118
|
+
this.scheduleSync(toolName, event.path);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle file created event
|
|
123
|
+
*/
|
|
124
|
+
handleFileCreated(event) {
|
|
125
|
+
const toolName = this.detectToolFromPath(event.path);
|
|
126
|
+
|
|
127
|
+
if (!toolName) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.log(`Detected creation in ${toolName}: ${event.path}`);
|
|
132
|
+
this.scheduleSync(toolName, event.path);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle file deleted event
|
|
137
|
+
*/
|
|
138
|
+
handleFileDeleted(event) {
|
|
139
|
+
const toolName = this.detectToolFromPath(event.path);
|
|
140
|
+
|
|
141
|
+
if (!toolName) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.log(`Detected deletion in ${toolName}: ${event.path}`);
|
|
146
|
+
// Don't auto-sync on delete - let user handle it
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Detect which tool a path belongs to
|
|
151
|
+
*/
|
|
152
|
+
detectToolFromPath(filePath) {
|
|
153
|
+
const { TOOL_CONTEXT_FILES } = require('./sync-manager');
|
|
154
|
+
|
|
155
|
+
for (const [toolName, files] of Object.entries(TOOL_CONTEXT_FILES)) {
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const fullPath = path.join(this.projectRoot, file);
|
|
158
|
+
|
|
159
|
+
if (filePath.startsWith(fullPath) || filePath === fullPath) {
|
|
160
|
+
return toolName;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Schedule a debounced sync
|
|
170
|
+
*/
|
|
171
|
+
scheduleSync(toolName, changedPath) {
|
|
172
|
+
// Clear existing timeout for this tool
|
|
173
|
+
const existingTimeout = this.syncTimeouts.get(toolName);
|
|
174
|
+
if (existingTimeout) {
|
|
175
|
+
clearTimeout(existingTimeout);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Schedule new sync
|
|
179
|
+
const timeout = setTimeout(async () => {
|
|
180
|
+
await this.performSync(toolName, changedPath);
|
|
181
|
+
this.syncTimeouts.delete(toolName);
|
|
182
|
+
}, this.config.debounceDelay);
|
|
183
|
+
|
|
184
|
+
this.syncTimeouts.set(toolName, timeout);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Perform the actual sync
|
|
189
|
+
*/
|
|
190
|
+
async performSync(sourceTool, changedPath) {
|
|
191
|
+
if (this.syncInProgress) {
|
|
192
|
+
this.log('Sync already in progress, skipping');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.syncInProgress = true;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
this.log(`Starting sync from ${sourceTool}...`);
|
|
200
|
+
|
|
201
|
+
if (this.config.onSyncStart) {
|
|
202
|
+
this.config.onSyncStart(sourceTool, changedPath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get config for project
|
|
206
|
+
const config = this.getProjectConfig();
|
|
207
|
+
|
|
208
|
+
// Propagate changes
|
|
209
|
+
const results = await propagateContextChange(
|
|
210
|
+
sourceTool,
|
|
211
|
+
this.projectRoot,
|
|
212
|
+
config,
|
|
213
|
+
this.config.strategy
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (results.errors.length > 0) {
|
|
217
|
+
this.log(`Sync completed with ${results.errors.length} errors`);
|
|
218
|
+
|
|
219
|
+
if (this.config.onError) {
|
|
220
|
+
this.config.onError(results.errors);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
this.log(`Sync completed successfully`);
|
|
224
|
+
this.log(`Propagated to: ${results.propagated.map(p => p.displayName).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.config.onSyncComplete) {
|
|
228
|
+
this.config.onSyncComplete(results);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.log(`Sync failed: ${error.message}`);
|
|
233
|
+
|
|
234
|
+
if (this.config.onError) {
|
|
235
|
+
this.config.onError([error]);
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
this.syncInProgress = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get project configuration
|
|
244
|
+
*/
|
|
245
|
+
getProjectConfig() {
|
|
246
|
+
const configPath = path.join(this.projectRoot, 'ai-context.config.json');
|
|
247
|
+
|
|
248
|
+
if (require('fs').existsSync(configPath)) {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
|
|
251
|
+
} catch {
|
|
252
|
+
// Fall through to defaults
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
// Default config
|
|
258
|
+
includePatterns: ['**/*.{js,ts,jsx,tsx,py,go,rs,java}'],
|
|
259
|
+
excludePatterns: ['node_modules/**', '**/node_modules/**']
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get current sync status
|
|
265
|
+
*/
|
|
266
|
+
getStatus() {
|
|
267
|
+
return checkSyncStatus(this.projectRoot);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Log message if verbose
|
|
272
|
+
*/
|
|
273
|
+
log(message) {
|
|
274
|
+
if (this.config.verbose) {
|
|
275
|
+
console.log(`[SyncService] ${message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create and start a sync service
|
|
282
|
+
*/
|
|
283
|
+
function createSyncService(projectRoot, config = {}) {
|
|
284
|
+
const service = new SyncService(projectRoot, config);
|
|
285
|
+
|
|
286
|
+
if (config.autoStart !== false) {
|
|
287
|
+
service.start();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return service;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
SyncService,
|
|
295
|
+
createSyncService,
|
|
296
|
+
DEFAULT_CONFIG
|
|
297
|
+
};
|
package/package.json
CHANGED