@elliotding/ai-agent-mcp 0.1.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/dist/api/cached-client.d.ts +48 -0
- package/dist/api/cached-client.d.ts.map +1 -0
- package/dist/api/cached-client.js +126 -0
- package/dist/api/cached-client.js.map +1 -0
- package/dist/api/client.d.ts +213 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +326 -0
- package/dist/api/client.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +26 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +36 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +194 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/permissions.d.ts +60 -0
- package/dist/auth/permissions.d.ts.map +1 -0
- package/dist/auth/permissions.js +256 -0
- package/dist/auth/permissions.js.map +1 -0
- package/dist/auth/token-validator.d.ts +52 -0
- package/dist/auth/token-validator.d.ts.map +1 -0
- package/dist/auth/token-validator.js +217 -0
- package/dist/auth/token-validator.js.map +1 -0
- package/dist/cache/cache-manager.d.ts +49 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +191 -0
- package/dist/cache/cache-manager.js.map +1 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +12 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/redis-client.d.ts +45 -0
- package/dist/cache/redis-client.d.ts.map +1 -0
- package/dist/cache/redis-client.js +210 -0
- package/dist/cache/redis-client.js.map +1 -0
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +31 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +168 -0
- package/dist/config/index.js.map +1 -0
- package/dist/filesystem/manager.d.ts +45 -0
- package/dist/filesystem/manager.d.ts.map +1 -0
- package/dist/filesystem/manager.js +246 -0
- package/dist/filesystem/manager.js.map +1 -0
- package/dist/git/multi-source-manager.d.ts +62 -0
- package/dist/git/multi-source-manager.d.ts.map +1 -0
- package/dist/git/multi-source-manager.js +293 -0
- package/dist/git/multi-source-manager.js.map +1 -0
- package/dist/git/operations.d.ts +27 -0
- package/dist/git/operations.d.ts.map +1 -0
- package/dist/git/operations.js +83 -0
- package/dist/git/operations.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/monitoring/health.d.ts +35 -0
- package/dist/monitoring/health.d.ts.map +1 -0
- package/dist/monitoring/health.js +105 -0
- package/dist/monitoring/health.js.map +1 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/loader.d.ts +87 -0
- package/dist/resources/loader.d.ts.map +1 -0
- package/dist/resources/loader.js +452 -0
- package/dist/resources/loader.js.map +1 -0
- package/dist/server/http.d.ts +57 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +336 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +157 -0
- package/dist/server.js.map +1 -0
- package/dist/session/manager.d.ts +91 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/manage-subscription.d.ts +43 -0
- package/dist/tools/manage-subscription.d.ts.map +1 -0
- package/dist/tools/manage-subscription.js +268 -0
- package/dist/tools/manage-subscription.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search-resources.d.ts +31 -0
- package/dist/tools/search-resources.d.ts.map +1 -0
- package/dist/tools/search-resources.js +154 -0
- package/dist/tools/search-resources.js.map +1 -0
- package/dist/tools/sync-resources.d.ts +41 -0
- package/dist/tools/sync-resources.d.ts.map +1 -0
- package/dist/tools/sync-resources.js +606 -0
- package/dist/tools/sync-resources.js.map +1 -0
- package/dist/tools/uninstall-resource.d.ts +30 -0
- package/dist/tools/uninstall-resource.d.ts.map +1 -0
- package/dist/tools/uninstall-resource.js +259 -0
- package/dist/tools/uninstall-resource.js.map +1 -0
- package/dist/tools/upload-resource.d.ts +77 -0
- package/dist/tools/upload-resource.d.ts.map +1 -0
- package/dist/tools/upload-resource.js +252 -0
- package/dist/tools/upload-resource.js.map +1 -0
- package/dist/transport/sse.d.ts +29 -0
- package/dist/transport/sse.d.ts.map +1 -0
- package/dist/transport/sse.js +271 -0
- package/dist/transport/sse.js.map +1 -0
- package/dist/types/errors.d.ts +60 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +112 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +50 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +6 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/resources.d.ts +109 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +7 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/tools.d.ts +147 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +6 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/cursor-paths.d.ts +49 -0
- package/dist/utils/cursor-paths.d.ts.map +1 -0
- package/dist/utils/cursor-paths.js +116 -0
- package/dist/utils/cursor-paths.js.map +1 -0
- package/dist/utils/log-cleaner.d.ts +18 -0
- package/dist/utils/log-cleaner.d.ts.map +1 -0
- package/dist/utils/log-cleaner.js +112 -0
- package/dist/utils/log-cleaner.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +292 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validation.d.ts +58 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +214 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +58 -0
- package/src/api/cached-client.ts +144 -0
- package/src/api/client.ts +578 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/middleware.ts +244 -0
- package/src/auth/permissions.ts +317 -0
- package/src/auth/token-validator.ts +294 -0
- package/src/cache/cache-manager.ts +243 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/redis-client.ts +249 -0
- package/src/config/constants.ts +33 -0
- package/src/config/index.ts +228 -0
- package/src/filesystem/manager.ts +235 -0
- package/src/git/multi-source-manager.ts +333 -0
- package/src/git/operations.ts +93 -0
- package/src/index.ts +139 -0
- package/src/monitoring/health.ts +132 -0
- package/src/resources/index.ts +13 -0
- package/src/resources/loader.ts +530 -0
- package/src/server/http.ts +427 -0
- package/src/server.ts +191 -0
- package/src/session/manager.ts +296 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/manage-subscription.ts +332 -0
- package/src/tools/registry.ts +97 -0
- package/src/tools/search-resources.ts +177 -0
- package/src/tools/sync-resources.ts +662 -0
- package/src/tools/uninstall-resource.ts +248 -0
- package/src/tools/upload-resource.ts +258 -0
- package/src/transport/sse.ts +308 -0
- package/src/types/errors.ts +146 -0
- package/src/types/index.ts +7 -0
- package/src/types/mcp.ts +61 -0
- package/src/types/resources.ts +141 -0
- package/src/types/tools.ts +175 -0
- package/src/utils/cursor-paths.ts +83 -0
- package/src/utils/log-cleaner.ts +92 -0
- package/src/utils/logger.ts +333 -0
- package/src/utils/validation.ts +262 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem Manager
|
|
3
|
+
* Atomic filesystem operations for resource management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as fsSync from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
import { createFileSystemError, createValidationError } from '../types/errors';
|
|
11
|
+
|
|
12
|
+
class FilesystemManager {
|
|
13
|
+
/**
|
|
14
|
+
* Write resource file atomically
|
|
15
|
+
*/
|
|
16
|
+
async writeResource(filePath: string, content: string): Promise<void> {
|
|
17
|
+
const tempPath = `${filePath}.tmp`;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Ensure directory exists
|
|
21
|
+
const dir = path.dirname(filePath);
|
|
22
|
+
await fs.mkdir(dir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
// Write to temporary file
|
|
25
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
26
|
+
|
|
27
|
+
// Validate content (basic check)
|
|
28
|
+
await this.validateResourceContent(tempPath, content);
|
|
29
|
+
|
|
30
|
+
// Atomic rename
|
|
31
|
+
await fs.rename(tempPath, filePath);
|
|
32
|
+
|
|
33
|
+
logger.debug({ filePath }, 'Resource file written successfully');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Cleanup temporary file
|
|
36
|
+
try {
|
|
37
|
+
await fs.unlink(tempPath);
|
|
38
|
+
} catch {
|
|
39
|
+
// Ignore cleanup errors
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw createFileSystemError('write', filePath, error as Error & { code?: string });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read resource file with validation
|
|
48
|
+
*/
|
|
49
|
+
async readResource(filePath: string): Promise<string> {
|
|
50
|
+
try {
|
|
51
|
+
// Check if file exists
|
|
52
|
+
await fs.access(filePath, fsSync.constants.R_OK);
|
|
53
|
+
|
|
54
|
+
// Read file
|
|
55
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
56
|
+
|
|
57
|
+
// Validate format
|
|
58
|
+
await this.validateResourceContent(filePath, content);
|
|
59
|
+
|
|
60
|
+
return content;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw createFileSystemError('read', filePath, error as Error & { code?: string });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delete resource file with backup
|
|
68
|
+
*/
|
|
69
|
+
async deleteResource(filePath: string): Promise<void> {
|
|
70
|
+
const backupPath = `${filePath}.backup`;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Create backup
|
|
74
|
+
if (fsSync.existsSync(filePath)) {
|
|
75
|
+
await fs.copyFile(filePath, backupPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Delete file
|
|
79
|
+
await fs.unlink(filePath);
|
|
80
|
+
|
|
81
|
+
// Remove backup on success
|
|
82
|
+
try {
|
|
83
|
+
await fs.unlink(backupPath);
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore backup cleanup errors
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.debug({ filePath }, 'Resource file deleted successfully');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Restore from backup on failure
|
|
91
|
+
try {
|
|
92
|
+
if (fsSync.existsSync(backupPath)) {
|
|
93
|
+
await fs.copyFile(backupPath, filePath);
|
|
94
|
+
await fs.unlink(backupPath);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore restore errors
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw createFileSystemError('delete', filePath, error as Error & { code?: string });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate resource content
|
|
106
|
+
*/
|
|
107
|
+
private async validateResourceContent(filePath: string, content: string): Promise<void> {
|
|
108
|
+
const ext = path.extname(filePath);
|
|
109
|
+
|
|
110
|
+
// Check if empty
|
|
111
|
+
if (!content || content.trim().length === 0) {
|
|
112
|
+
throw createValidationError(filePath, ext, 'File content is empty');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate based on file type
|
|
116
|
+
if (ext === '.json') {
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(content);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw createValidationError(filePath, 'json', 'Invalid JSON format');
|
|
121
|
+
}
|
|
122
|
+
} else if (ext === '.md') {
|
|
123
|
+
// Basic markdown validation (check for minimum content)
|
|
124
|
+
if (content.length < 10) {
|
|
125
|
+
throw createValidationError(filePath, 'markdown', 'Markdown content too short');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if file exists
|
|
132
|
+
*/
|
|
133
|
+
async fileExists(filePath: string): Promise<boolean> {
|
|
134
|
+
try {
|
|
135
|
+
await fs.access(filePath, fsSync.constants.F_OK);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* List files in directory
|
|
144
|
+
*/
|
|
145
|
+
async listFiles(dirPath: string, pattern?: RegExp): Promise<string[]> {
|
|
146
|
+
try {
|
|
147
|
+
const files = await fs.readdir(dirPath, { recursive: true });
|
|
148
|
+
|
|
149
|
+
if (pattern) {
|
|
150
|
+
return files.filter((file) => pattern.test(file));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return files;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw createFileSystemError('list', dirPath, error as Error & { code?: string });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Recursively scan a directory and return all text files as FileEntry[]
|
|
161
|
+
* Supported extensions: .md, .mdc, .txt, .yaml, .yml, .json
|
|
162
|
+
*/
|
|
163
|
+
async scanDirectory(dirPath: string): Promise<Array<{ path: string; content: string }>> {
|
|
164
|
+
const TEXT_EXTENSIONS = new Set(['.md', '.mdc', '.txt', '.yaml', '.yml', '.json']);
|
|
165
|
+
const results: Array<{ path: string; content: string }> = [];
|
|
166
|
+
|
|
167
|
+
const walk = async (currentPath: string, relBase: string): Promise<void> => {
|
|
168
|
+
let entries: fsSync.Dirent[];
|
|
169
|
+
try {
|
|
170
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw createFileSystemError('list', currentPath, error as Error & { code?: string });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
177
|
+
const relPath = path.join(relBase, entry.name);
|
|
178
|
+
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
await walk(fullPath, relPath);
|
|
181
|
+
} else if (entry.isFile()) {
|
|
182
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
183
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
184
|
+
try {
|
|
185
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
186
|
+
if (content.trim().length > 0) {
|
|
187
|
+
results.push({ path: relPath, content });
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Skip unreadable files silently
|
|
191
|
+
logger.warn({ filePath: fullPath }, 'Skipped unreadable file during directory scan');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await walk(dirPath, '');
|
|
199
|
+
|
|
200
|
+
if (results.length === 0) {
|
|
201
|
+
throw createValidationError(
|
|
202
|
+
dirPath,
|
|
203
|
+
'directory',
|
|
204
|
+
`No text files found in directory: ${dirPath}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logger.debug({ dirPath, fileCount: results.length }, 'Directory scan completed');
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Remove empty directories recursively
|
|
214
|
+
*/
|
|
215
|
+
async removeEmptyDirs(dirPath: string): Promise<void> {
|
|
216
|
+
try {
|
|
217
|
+
const files = await fs.readdir(dirPath);
|
|
218
|
+
|
|
219
|
+
if (files.length === 0) {
|
|
220
|
+
await fs.rmdir(dirPath);
|
|
221
|
+
logger.debug({ dirPath }, 'Empty directory removed');
|
|
222
|
+
|
|
223
|
+
// Check parent directory
|
|
224
|
+
const parentDir = path.dirname(dirPath);
|
|
225
|
+
if (parentDir !== dirPath) {
|
|
226
|
+
await this.removeEmptyDirs(parentDir);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Ignore errors (directory might not be empty or already deleted)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const filesystemManager = new FilesystemManager();
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Source Git Manager
|
|
3
|
+
* Manages multiple AI Resources Git repositories efficiently
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import simpleGit from 'simple-git';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
import { createGitError } from '../types/errors';
|
|
11
|
+
|
|
12
|
+
interface AIResourcesConfig {
|
|
13
|
+
version: string;
|
|
14
|
+
default_source: SourceConfig;
|
|
15
|
+
extended_sources: SourceConfig[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SourceConfig {
|
|
19
|
+
name: string;
|
|
20
|
+
path: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
priority: number;
|
|
23
|
+
git_url?: string; // Git repository URL
|
|
24
|
+
git_branch?: string; // Git branch (default: main)
|
|
25
|
+
resources: {
|
|
26
|
+
commands: string;
|
|
27
|
+
skills: string;
|
|
28
|
+
mcp: string;
|
|
29
|
+
rules: string;
|
|
30
|
+
};
|
|
31
|
+
description: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SyncResult {
|
|
35
|
+
source: string;
|
|
36
|
+
action: 'cloned' | 'pulled' | 'up-to-date' | 'skipped';
|
|
37
|
+
changes: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class MultiSourceGitManager {
|
|
42
|
+
private configPath: string;
|
|
43
|
+
private baseDir: string;
|
|
44
|
+
|
|
45
|
+
constructor(baseDir: string) {
|
|
46
|
+
this.baseDir = baseDir;
|
|
47
|
+
this.configPath = path.join(baseDir, 'ai-resources-config.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load AI Resources configuration
|
|
52
|
+
*/
|
|
53
|
+
private async loadConfig(): Promise<AIResourcesConfig> {
|
|
54
|
+
try {
|
|
55
|
+
const configContent = await fs.readFile(this.configPath, 'utf-8');
|
|
56
|
+
return JSON.parse(configContent);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Failed to load AI Resources config: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all enabled sources (including default)
|
|
64
|
+
*/
|
|
65
|
+
private async getEnabledSources(): Promise<SourceConfig[]> {
|
|
66
|
+
const config = await this.loadConfig();
|
|
67
|
+
const sources: SourceConfig[] = [];
|
|
68
|
+
|
|
69
|
+
if (config.default_source.enabled) {
|
|
70
|
+
sources.push(config.default_source);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.extended_sources) {
|
|
74
|
+
sources.push(...config.extended_sources.filter(s => s.enabled));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return sources;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a Git repository exists at the given path
|
|
82
|
+
*/
|
|
83
|
+
private async repositoryExists(repoPath: string): Promise<boolean> {
|
|
84
|
+
try {
|
|
85
|
+
const gitDir = path.join(repoPath, '.git');
|
|
86
|
+
const stats = await fs.stat(gitDir);
|
|
87
|
+
return stats.isDirectory();
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get Git repository URL from existing repo
|
|
95
|
+
*/
|
|
96
|
+
private async getRepoUrl(repoPath: string): Promise<string | null> {
|
|
97
|
+
try {
|
|
98
|
+
const git = simpleGit(repoPath);
|
|
99
|
+
const remotes = await git.getRemotes(true);
|
|
100
|
+
const origin = remotes.find(r => r.name === 'origin');
|
|
101
|
+
return origin?.refs.fetch || null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clone a Git repository
|
|
109
|
+
*/
|
|
110
|
+
private async cloneRepository(repoUrl: string, targetPath: string, branch: string = 'main'): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
logger.info({ repoUrl, targetPath, branch }, 'Cloning Git repository...');
|
|
113
|
+
|
|
114
|
+
// Ensure parent directory exists
|
|
115
|
+
const parentDir = path.dirname(targetPath);
|
|
116
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const git = simpleGit();
|
|
119
|
+
|
|
120
|
+
// Clone with --single-branch to limit downloaded history to the target
|
|
121
|
+
// branch only, keeping the clone fast without creating a shallow repo
|
|
122
|
+
// (shallow repos cause "no merge base" errors on subsequent fetches).
|
|
123
|
+
await git.clone(repoUrl, targetPath, [
|
|
124
|
+
'--branch', branch,
|
|
125
|
+
'--single-branch',
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
logger.info({ repoUrl, targetPath }, 'Repository cloned successfully');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw createGitError('clone', error as Error, repoUrl);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pull latest changes using fetch + fast-forward merge.
|
|
136
|
+
*
|
|
137
|
+
* Deliberately avoids --depth=1 on fetch: shallow fetches truncate local
|
|
138
|
+
* history, causing "no merge base" divergence errors on subsequent pulls.
|
|
139
|
+
*/
|
|
140
|
+
private async pullRepository(repoPath: string, branch: string = 'main'): Promise<{
|
|
141
|
+
hasChanges: boolean;
|
|
142
|
+
filesChanged: number;
|
|
143
|
+
}> {
|
|
144
|
+
try {
|
|
145
|
+
const git = simpleGit(repoPath);
|
|
146
|
+
|
|
147
|
+
logger.info({ repoPath, branch }, 'Fetching latest changes...');
|
|
148
|
+
|
|
149
|
+
// If the local repo is shallow (was previously cloned with --depth),
|
|
150
|
+
// unshallow it first so subsequent fetches have a proper merge base.
|
|
151
|
+
const isShallow = (await git.raw(['rev-parse', '--is-shallow-repository'])).trim() === 'true';
|
|
152
|
+
if (isShallow) {
|
|
153
|
+
logger.info({ repoPath }, 'Shallow repo detected — running fetch --unshallow first');
|
|
154
|
+
await git.fetch(['--unshallow', 'origin', branch]);
|
|
155
|
+
} else {
|
|
156
|
+
// Fetch the branch from origin without --depth to keep full history intact.
|
|
157
|
+
await git.fetch(['origin', branch]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Compare local HEAD with remote tip to detect changes before merging.
|
|
161
|
+
const remoteBranch = `origin/${branch}`;
|
|
162
|
+
const diffSummary = await git.diffSummary([`HEAD...${remoteBranch}`]);
|
|
163
|
+
const hasChanges = diffSummary.files.length > 0;
|
|
164
|
+
|
|
165
|
+
if (!hasChanges) {
|
|
166
|
+
logger.info({ repoPath }, 'Repository is up-to-date, no changes to pull');
|
|
167
|
+
return { hasChanges: false, filesChanged: 0 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fast-forward only — never auto-merge diverged histories.
|
|
171
|
+
logger.info({ repoPath, filesChanged: diffSummary.files.length }, 'Pulling changes...');
|
|
172
|
+
await git.merge([remoteBranch, '--ff-only']);
|
|
173
|
+
|
|
174
|
+
logger.info({
|
|
175
|
+
repoPath,
|
|
176
|
+
filesChanged: diffSummary.files.length,
|
|
177
|
+
insertions: diffSummary.insertions,
|
|
178
|
+
deletions: diffSummary.deletions,
|
|
179
|
+
}, 'Repository updated successfully');
|
|
180
|
+
|
|
181
|
+
return { hasChanges: true, filesChanged: diffSummary.files.length };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw createGitError('pull', error as Error, repoPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Sync a single source repository
|
|
189
|
+
*/
|
|
190
|
+
private async syncSource(source: SourceConfig): Promise<SyncResult> {
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
const sourcePath = path.join(this.baseDir, source.path);
|
|
193
|
+
|
|
194
|
+
logger.info({
|
|
195
|
+
source: source.name,
|
|
196
|
+
path: sourcePath,
|
|
197
|
+
priority: source.priority
|
|
198
|
+
}, 'Syncing AI Resources source...');
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const exists = await this.repositoryExists(sourcePath);
|
|
202
|
+
|
|
203
|
+
if (!exists) {
|
|
204
|
+
// First time: clone repository
|
|
205
|
+
if (!source.git_url) {
|
|
206
|
+
logger.warn({ source: source.name }, 'Source has no git_url configured, skipping clone');
|
|
207
|
+
return {
|
|
208
|
+
source: source.name,
|
|
209
|
+
action: 'skipped',
|
|
210
|
+
changes: 0,
|
|
211
|
+
duration: Date.now() - startTime
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.info({ source: source.name }, 'Repository does not exist, cloning...');
|
|
216
|
+
await this.cloneRepository(
|
|
217
|
+
source.git_url,
|
|
218
|
+
sourcePath,
|
|
219
|
+
source.git_branch || 'main'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
source: source.name,
|
|
224
|
+
action: 'cloned',
|
|
225
|
+
changes: -1, // -1 means full clone
|
|
226
|
+
duration: Date.now() - startTime
|
|
227
|
+
};
|
|
228
|
+
} else {
|
|
229
|
+
// Repository exists: pull latest changes
|
|
230
|
+
logger.info({ source: source.name }, 'Repository exists, pulling latest changes...');
|
|
231
|
+
|
|
232
|
+
const { hasChanges, filesChanged } = await this.pullRepository(
|
|
233
|
+
sourcePath,
|
|
234
|
+
source.git_branch || 'main'
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
source: source.name,
|
|
239
|
+
action: hasChanges ? 'pulled' : 'up-to-date',
|
|
240
|
+
changes: filesChanged,
|
|
241
|
+
duration: Date.now() - startTime
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.error({
|
|
246
|
+
source: source.name,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error)
|
|
248
|
+
}, 'Failed to sync source');
|
|
249
|
+
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Sync all enabled sources
|
|
256
|
+
*/
|
|
257
|
+
async syncAllSources(): Promise<SyncResult[]> {
|
|
258
|
+
logger.info('Starting multi-source sync...');
|
|
259
|
+
|
|
260
|
+
const sources = await this.getEnabledSources();
|
|
261
|
+
logger.info({
|
|
262
|
+
totalSources: sources.length,
|
|
263
|
+
sourceNames: sources.map(s => s.name)
|
|
264
|
+
}, 'Found enabled sources');
|
|
265
|
+
|
|
266
|
+
const results: SyncResult[] = [];
|
|
267
|
+
|
|
268
|
+
for (const source of sources) {
|
|
269
|
+
try {
|
|
270
|
+
const result = await this.syncSource(source);
|
|
271
|
+
results.push(result);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.error({
|
|
274
|
+
source: source.name,
|
|
275
|
+
error: error instanceof Error ? error.message : String(error)
|
|
276
|
+
}, 'Failed to sync source, continuing with next...');
|
|
277
|
+
|
|
278
|
+
results.push({
|
|
279
|
+
source: source.name,
|
|
280
|
+
action: 'skipped',
|
|
281
|
+
changes: 0,
|
|
282
|
+
duration: 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
logger.info({
|
|
288
|
+
results,
|
|
289
|
+
totalSources: results.length,
|
|
290
|
+
cloned: results.filter(r => r.action === 'cloned').length,
|
|
291
|
+
pulled: results.filter(r => r.action === 'pulled').length,
|
|
292
|
+
upToDate: results.filter(r => r.action === 'up-to-date').length
|
|
293
|
+
}, 'Multi-source sync completed');
|
|
294
|
+
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check status of all sources without pulling
|
|
300
|
+
*/
|
|
301
|
+
async checkAllSources(): Promise<Array<{
|
|
302
|
+
source: string;
|
|
303
|
+
exists: boolean;
|
|
304
|
+
hasRemote: boolean;
|
|
305
|
+
repoUrl: string | null;
|
|
306
|
+
}>> {
|
|
307
|
+
const sources = await this.getEnabledSources();
|
|
308
|
+
const statuses = [];
|
|
309
|
+
|
|
310
|
+
for (const source of sources) {
|
|
311
|
+
const sourcePath = path.join(this.baseDir, source.path);
|
|
312
|
+
const exists = await this.repositoryExists(sourcePath);
|
|
313
|
+
|
|
314
|
+
let repoUrl = null;
|
|
315
|
+
if (exists) {
|
|
316
|
+
repoUrl = await this.getRepoUrl(sourcePath);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
statuses.push({
|
|
320
|
+
source: source.name,
|
|
321
|
+
exists,
|
|
322
|
+
hasRemote: !!repoUrl,
|
|
323
|
+
repoUrl
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return statuses;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Export singleton instance
|
|
332
|
+
const AI_RESOURCES_BASE = path.resolve(process.cwd(), '../AI-Resources');
|
|
333
|
+
export const multiSourceGitManager = new MultiSourceGitManager(AI_RESOURCES_BASE);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Operations Module
|
|
3
|
+
* Per-repo git helpers used by upload_resource to target a specific source repo.
|
|
4
|
+
* All repo URLs and branches come from AI-Resources/ai-resources-config.json —
|
|
5
|
+
* not from environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import simpleGit from 'simple-git';
|
|
9
|
+
import { config } from '../config';
|
|
10
|
+
import { logger } from '../utils/logger';
|
|
11
|
+
import { createGitError } from '../types/errors';
|
|
12
|
+
|
|
13
|
+
class GitOperations {
|
|
14
|
+
// ---- Per-repo helpers (used by upload_resource to target a specific source) ----
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check whether a git repository exists at an arbitrary local path
|
|
18
|
+
*/
|
|
19
|
+
async repositoryExistsAt(repoPath: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const git = simpleGit({ baseDir: repoPath, binary: 'git', maxConcurrentProcesses: 6 });
|
|
22
|
+
await git.status();
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get current branch of an arbitrary local repo
|
|
31
|
+
*/
|
|
32
|
+
async getCurrentBranchAt(repoPath: string): Promise<string> {
|
|
33
|
+
try {
|
|
34
|
+
const git = simpleGit({ baseDir: repoPath, binary: 'git', maxConcurrentProcesses: 6 });
|
|
35
|
+
const branchSummary = await git.branch();
|
|
36
|
+
return branchSummary.current;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw createGitError('get-branch', error as Error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Commit and push to a specific repo / remote url / branch.
|
|
44
|
+
* Creates a unique remote testing branch and returns a PR URL.
|
|
45
|
+
*/
|
|
46
|
+
async commitAndPushRepo(
|
|
47
|
+
repoPath: string,
|
|
48
|
+
remoteUrl: string,
|
|
49
|
+
baseBranch: string,
|
|
50
|
+
message: string,
|
|
51
|
+
files?: string[]
|
|
52
|
+
): Promise<{ commitHash: string; prUrl?: string }> {
|
|
53
|
+
try {
|
|
54
|
+
const git = simpleGit({ baseDir: repoPath, binary: 'git', maxConcurrentProcesses: 6 });
|
|
55
|
+
|
|
56
|
+
logger.info({ repoPath, remoteUrl, message, fileCount: files?.length }, 'Committing and pushing to source repo...');
|
|
57
|
+
|
|
58
|
+
await git.addConfig('user.name', config.git.userName);
|
|
59
|
+
await git.addConfig('user.email', config.git.userEmail);
|
|
60
|
+
|
|
61
|
+
const branchSummary = await git.branch();
|
|
62
|
+
const currentBranch = branchSummary.current;
|
|
63
|
+
|
|
64
|
+
const timestamp = Date.now();
|
|
65
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
66
|
+
const remoteBranchName = `dev-${currentBranch}-testing-${timestamp}-${randomSuffix}`;
|
|
67
|
+
logger.info({ remoteBranchName }, 'Generated remote branch name for PR');
|
|
68
|
+
|
|
69
|
+
if (files && files.length > 0) {
|
|
70
|
+
await git.add(files);
|
|
71
|
+
} else {
|
|
72
|
+
await git.add('.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const commitResult = await git.commit(message);
|
|
76
|
+
const commitHash = commitResult.commit;
|
|
77
|
+
logger.info({ commitHash }, 'Git commit created');
|
|
78
|
+
|
|
79
|
+
await git.push(remoteUrl, `${currentBranch}:${remoteBranchName}`);
|
|
80
|
+
logger.info({ remoteBranchName }, 'Git push completed');
|
|
81
|
+
|
|
82
|
+
const repoBaseUrl = remoteUrl.replace(/\.git$/, '');
|
|
83
|
+
const prUrl = `${repoBaseUrl}/compare/${baseBranch}...${remoteBranchName}`;
|
|
84
|
+
logger.info({ prUrl, commitHash }, 'PR URL generated');
|
|
85
|
+
|
|
86
|
+
return { commitHash, prUrl };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw createGitError('commit-push', error as Error, remoteUrl);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const gitOperations = new GitOperations();
|