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 +91 -0
- package/bin/create-ai-context.js +262 -0
- package/lib/cross-tool-sync/file-watcher.js +274 -0
- package/lib/cross-tool-sync/index.js +40 -0
- package/lib/cross-tool-sync/sync-manager.js +512 -0
- package/lib/cross-tool-sync/sync-service.js +297 -0
- package/lib/install-hooks.js +82 -0
- package/package.json +1 -1
- package/templates/hooks/post-commit.hbs +29 -0
- package/templates/hooks/pre-commit.hbs +46 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install Git Hooks for AI Context Sync
|
|
4
|
+
*
|
|
5
|
+
* Usage: Called via `npx create-ai-context hooks:install`
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
function installHooks(projectRoot = process.cwd()) {
|
|
12
|
+
const hooksDir = path.join(__dirname, '..', 'templates', 'hooks');
|
|
13
|
+
const gitHooksDir = path.join(projectRoot, '.git', 'hooks');
|
|
14
|
+
|
|
15
|
+
// Hooks to install
|
|
16
|
+
const hooks = [
|
|
17
|
+
{ source: 'pre-commit.hbs', target: 'pre-commit' },
|
|
18
|
+
{ source: 'post-commit.hbs', target: 'post-commit' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function ensureDirectory(dir) {
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyHook(sourceFile, targetFile) {
|
|
28
|
+
const sourcePath = path.join(hooksDir, sourceFile);
|
|
29
|
+
const targetPath = path.join(gitHooksDir, targetFile);
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(sourcePath)) {
|
|
32
|
+
console.warn(`Warning: ${sourceFile} not found, skipping`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
37
|
+
|
|
38
|
+
fs.writeFileSync(targetPath, content, { mode: 0o755 });
|
|
39
|
+
console.log(`✓ Installed ${targetFile}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('\nInstalling AI context sync git hooks...\n');
|
|
44
|
+
|
|
45
|
+
// Check if we're in a git repository
|
|
46
|
+
if (!fs.existsSync(gitHooksDir)) {
|
|
47
|
+
console.error('Error: .git/hooks directory not found');
|
|
48
|
+
console.error('Please run this command from the root of a git repository\n');
|
|
49
|
+
throw new Error('Not a git repository');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Ensure hooks directory exists
|
|
53
|
+
ensureDirectory(gitHooksDir);
|
|
54
|
+
|
|
55
|
+
// Copy hooks
|
|
56
|
+
let installed = 0;
|
|
57
|
+
for (const hook of hooks) {
|
|
58
|
+
if (copyHook(hook.source, hook.target)) {
|
|
59
|
+
installed++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\nSuccessfully installed ${installed} git hooks\n`);
|
|
64
|
+
console.log('The hooks will:');
|
|
65
|
+
console.log(' • Check sync status before commits (pre-commit)');
|
|
66
|
+
console.log(' • Auto-sync after successful commits (post-commit)\n');
|
|
67
|
+
console.log('To skip sync checks, use: git commit --no-verify\n');
|
|
68
|
+
|
|
69
|
+
return { installed, total: hooks.length };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { installHooks };
|
|
73
|
+
|
|
74
|
+
// If run directly
|
|
75
|
+
if (require.main === module) {
|
|
76
|
+
try {
|
|
77
|
+
installHooks();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('\n✖ Installation failed:', error.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Post-commit hook for AI context sync
|
|
3
|
+
# This hook triggers sync after successful commits
|
|
4
|
+
|
|
5
|
+
# Don't block commit if this fails
|
|
6
|
+
set +e
|
|
7
|
+
|
|
8
|
+
# Check if ai-context binary is available
|
|
9
|
+
if ! command -v create-ai-context &> /dev/null; then
|
|
10
|
+
# Try npx
|
|
11
|
+
if command -v npx &> /dev/null; then
|
|
12
|
+
AI_CONTEXT_CMD="npx create-universal-ai-context"
|
|
13
|
+
else
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
else
|
|
17
|
+
AI_CONTEXT_CMD="create-ai-context"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Run background sync (async)
|
|
21
|
+
echo "Syncing AI contexts in background..."
|
|
22
|
+
|
|
23
|
+
# Use subshell to run in background
|
|
24
|
+
(
|
|
25
|
+
sleep 2 # Wait a bit to not interfere with commit
|
|
26
|
+
$AI_CONTEXT_CMD sync:all --quiet > /dev/null 2>&1
|
|
27
|
+
) &
|
|
28
|
+
|
|
29
|
+
exit 0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-commit hook for AI context sync
|
|
3
|
+
# This hook checks if AI tool contexts are out of sync
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Colors for output
|
|
8
|
+
RED='\033[0;31m'
|
|
9
|
+
YELLOW='\033[1;33m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
NC='\033[0m' # No Color
|
|
12
|
+
|
|
13
|
+
# Check if ai-context binary is available
|
|
14
|
+
if ! command -v create-ai-context &> /dev/null; then
|
|
15
|
+
# Try npx
|
|
16
|
+
if command -v npx &> /dev/null; then
|
|
17
|
+
AI_CONTEXT_CMD="npx create-universal-ai-context"
|
|
18
|
+
else
|
|
19
|
+
echo -e "${YELLOW}Warning: create-ai-context not found. Skipping sync check.${NC}"
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
else
|
|
23
|
+
AI_CONTEXT_CMD="create-ai-context"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Run sync check
|
|
27
|
+
echo -e "${GREEN}Checking AI context sync status...${NC}"
|
|
28
|
+
|
|
29
|
+
# Check for out-of-sync contexts
|
|
30
|
+
CHECK_OUTPUT=$($AI_CONTEXT_CMD sync:check 2>&1)
|
|
31
|
+
CHECK_EXIT_CODE=$?
|
|
32
|
+
|
|
33
|
+
if [ $CHECK_EXIT_CODE -ne 0 ]; then
|
|
34
|
+
echo -e "${RED}AI contexts are out of sync!${NC}"
|
|
35
|
+
echo ""
|
|
36
|
+
echo "$CHECK_OUTPUT"
|
|
37
|
+
echo ""
|
|
38
|
+
echo -e "${YELLOW}To sync all contexts, run:${NC}"
|
|
39
|
+
echo " $AI_CONTEXT_CMD sync:all"
|
|
40
|
+
echo ""
|
|
41
|
+
echo -e "${YELLOW}To skip this check, use: git commit --no-verify${NC}"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo -e "${GREEN}AI contexts are in sync${NC}"
|
|
46
|
+
exit 0
|