Skip to content

Commit e27c5f3

Browse files
committed
feat: complete Week 2 core modules
- Implement GitManager with full Git operations support - Clone, fetch, checkout operations - Tag and branch listing - Commit history and status checks - Comprehensive error handling for Git operations - Implement FileSystemManager with complete file operations - File read/write/copy/remove operations - Directory management and walking - File hashing (SHA-256) for integrity checks - Cross-platform path handling - Add path utilities module - Config directory resolution - Path normalization and sanitization - Config name resolution from paths - Destination path calculation - Implement YAML utilities for frontmatter parsing - Define Zod schemas for all configuration types - Agent, Skill, MCP server schemas - Manifest and Lockfile schemas - Tag validation with proper regex patterns - Implement comprehensive validators - AgentValidator with frontmatter validation - SkillValidator with name format checks - McpValidator with secret detection - ManifestValidator and LockfileValidator - Add extensive unit tests (58 tests total) - Path utilities tests - FileSystemManager tests with temp directory - Validator tests for all config types - All tests passing All acceptance criteria for Week 2 (Task 2.1, 2.2, 2.3) completed: ✓ GitManager fully implemented with 80%+ coverage ✓ FileSystemManager working with proper error handling ✓ Path utilities functional on Unix and Windows ✓ All Zod schemas complete and validated ✓ All validator classes implemented ✓ 58 tests passing ✓ Type checking passes ✓ Linting passes
1 parent 9001e88 commit e27c5f3

File tree

9 files changed

+1669
-0
lines changed

9 files changed

+1669
-0
lines changed

src/core/fs/fs-manager.ts

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
import crypto from 'crypto';
4+
import {
5+
FileNotFoundError,
6+
FileReadError,
7+
FileWriteError,
8+
PermissionDeniedError,
9+
FileSystemError,
10+
ErrorCode,
11+
} from '../../utils/errors.js';
12+
import { getLogger } from '../../utils/logger.js';
13+
14+
/**
15+
* File system operations manager
16+
* Handles all file system operations with proper error handling
17+
*/
18+
export class FileSystemManager {
19+
private logger = getLogger();
20+
21+
/**
22+
* Ensure a directory exists, creating it if necessary
23+
*/
24+
async ensureDir(dirPath: string): Promise<void> {
25+
this.logger.debug(`Ensuring directory exists: ${dirPath}`);
26+
try {
27+
await fs.mkdir(dirPath, { recursive: true });
28+
} catch (error) {
29+
if (error instanceof Error) {
30+
throw new FileSystemError(
31+
`Failed to create directory: ${dirPath}`,
32+
ErrorCode.FS_WRITE_ERROR,
33+
{ path: dirPath },
34+
error
35+
);
36+
}
37+
throw error;
38+
}
39+
}
40+
41+
/**
42+
* Copy a file from source to destination
43+
*/
44+
async copyFile(src: string, dest: string): Promise<void> {
45+
this.logger.debug(`Copying file: ${src} -> ${dest}`);
46+
try {
47+
// Ensure destination directory exists
48+
const destDir = path.dirname(dest);
49+
await this.ensureDir(destDir);
50+
51+
// Copy the file
52+
await fs.copyFile(src, dest);
53+
this.logger.debug('File copied successfully');
54+
} catch (error) {
55+
if (error instanceof Error) {
56+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
57+
throw new FileNotFoundError(src);
58+
}
59+
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
60+
throw new PermissionDeniedError(dest, 'copy');
61+
}
62+
throw new FileSystemError(
63+
`Failed to copy file: ${src} -> ${dest}`,
64+
ErrorCode.FS_COPY_ERROR,
65+
{ src, dest },
66+
error
67+
);
68+
}
69+
throw error;
70+
}
71+
}
72+
73+
/**
74+
* Remove a file
75+
*/
76+
async removeFile(filePath: string): Promise<void> {
77+
this.logger.debug(`Removing file: ${filePath}`);
78+
try {
79+
await fs.unlink(filePath);
80+
this.logger.debug('File removed successfully');
81+
} catch (error) {
82+
if (error instanceof Error) {
83+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
84+
// File doesn't exist - this is okay
85+
return;
86+
}
87+
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
88+
throw new PermissionDeniedError(filePath, 'delete');
89+
}
90+
throw new FileSystemError(
91+
`Failed to remove file: ${filePath}`,
92+
ErrorCode.FS_DELETE_ERROR,
93+
{ path: filePath },
94+
error
95+
);
96+
}
97+
throw error;
98+
}
99+
}
100+
101+
/**
102+
* Remove a directory recursively
103+
*/
104+
async removeDir(dirPath: string): Promise<void> {
105+
this.logger.debug(`Removing directory: ${dirPath}`);
106+
try {
107+
await fs.rm(dirPath, { recursive: true, force: true });
108+
this.logger.debug('Directory removed successfully');
109+
} catch (error) {
110+
if (error instanceof Error) {
111+
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
112+
throw new PermissionDeniedError(dirPath, 'delete');
113+
}
114+
throw new FileSystemError(
115+
`Failed to remove directory: ${dirPath}`,
116+
ErrorCode.FS_DELETE_ERROR,
117+
{ path: dirPath },
118+
error
119+
);
120+
}
121+
throw error;
122+
}
123+
}
124+
125+
/**
126+
* Read a file as string
127+
*/
128+
async readFile(filePath: string): Promise<string> {
129+
this.logger.debug(`Reading file: ${filePath}`);
130+
try {
131+
const content = await fs.readFile(filePath, 'utf-8');
132+
return content;
133+
} catch (error) {
134+
if (error instanceof Error) {
135+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
136+
throw new FileNotFoundError(filePath);
137+
}
138+
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
139+
throw new PermissionDeniedError(filePath, 'read');
140+
}
141+
throw new FileReadError(filePath, error);
142+
}
143+
throw error;
144+
}
145+
}
146+
147+
/**
148+
* Write content to a file
149+
*/
150+
async writeFile(filePath: string, content: string): Promise<void> {
151+
this.logger.debug(`Writing file: ${filePath}`);
152+
try {
153+
// Ensure directory exists
154+
const dir = path.dirname(filePath);
155+
await this.ensureDir(dir);
156+
157+
// Write file
158+
await fs.writeFile(filePath, content, 'utf-8');
159+
this.logger.debug('File written successfully');
160+
} catch (error) {
161+
if (error instanceof Error) {
162+
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
163+
throw new PermissionDeniedError(filePath, 'write');
164+
}
165+
throw new FileWriteError(filePath, error);
166+
}
167+
throw error;
168+
}
169+
}
170+
171+
/**
172+
* List files in a directory (non-recursive)
173+
*/
174+
async listFiles(dirPath: string): Promise<string[]> {
175+
this.logger.debug(`Listing files in: ${dirPath}`);
176+
try {
177+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
178+
const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
179+
this.logger.debug(`Found ${files.length} files`);
180+
return files;
181+
} catch (error) {
182+
if (error instanceof Error) {
183+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
184+
throw new FileNotFoundError(dirPath);
185+
}
186+
throw new FileSystemError(
187+
`Failed to list files in: ${dirPath}`,
188+
ErrorCode.FS_READ_ERROR,
189+
{ path: dirPath },
190+
error
191+
);
192+
}
193+
throw error;
194+
}
195+
}
196+
197+
/**
198+
* Check if a file exists
199+
*/
200+
async fileExists(filePath: string): Promise<boolean> {
201+
try {
202+
await fs.access(filePath);
203+
return true;
204+
} catch {
205+
return false;
206+
}
207+
}
208+
209+
/**
210+
* Check if a path is a directory
211+
*/
212+
async isDirectory(dirPath: string): Promise<boolean> {
213+
try {
214+
const stats = await fs.stat(dirPath);
215+
return stats.isDirectory();
216+
} catch {
217+
return false;
218+
}
219+
}
220+
221+
/**
222+
* Get SHA-256 hash of a file
223+
*/
224+
async getFileHash(filePath: string): Promise<string> {
225+
this.logger.debug(`Computing hash for: ${filePath}`);
226+
try {
227+
const content = await this.readFile(filePath);
228+
const hash = crypto.createHash('sha256').update(content).digest('hex');
229+
this.logger.debug(`Hash: ${hash}`);
230+
return hash;
231+
} catch (error) {
232+
if (error instanceof FileNotFoundError) {
233+
throw error;
234+
}
235+
if (error instanceof Error) {
236+
throw new FileSystemError(
237+
`Failed to compute hash: ${filePath}`,
238+
ErrorCode.FS_READ_ERROR,
239+
{ path: filePath },
240+
error
241+
);
242+
}
243+
throw error;
244+
}
245+
}
246+
247+
/**
248+
* Get file stats
249+
*/
250+
async getStats(filePath: string): Promise<{ size: number; modified: Date }> {
251+
try {
252+
const stats = await fs.stat(filePath);
253+
return {
254+
size: stats.size,
255+
modified: stats.mtime,
256+
};
257+
} catch (error) {
258+
if (error instanceof Error) {
259+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
260+
throw new FileNotFoundError(filePath);
261+
}
262+
throw new FileSystemError(
263+
`Failed to get stats: ${filePath}`,
264+
ErrorCode.FS_READ_ERROR,
265+
{ path: filePath },
266+
error
267+
);
268+
}
269+
throw error;
270+
}
271+
}
272+
273+
/**
274+
* Walk directory recursively and return all file paths
275+
*/
276+
async walkDir(dirPath: string, pattern?: RegExp): Promise<string[]> {
277+
this.logger.debug(`Walking directory: ${dirPath}`);
278+
const results: string[] = [];
279+
280+
try {
281+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
282+
283+
for (const entry of entries) {
284+
const fullPath = path.join(dirPath, entry.name);
285+
286+
if (entry.isDirectory()) {
287+
const subResults = await this.walkDir(fullPath, pattern);
288+
results.push(...subResults);
289+
} else if (entry.isFile()) {
290+
if (!pattern || pattern.test(fullPath)) {
291+
results.push(fullPath);
292+
}
293+
}
294+
}
295+
296+
return results;
297+
} catch (error) {
298+
if (error instanceof Error) {
299+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
300+
return [];
301+
}
302+
throw new FileSystemError(
303+
`Failed to walk directory: ${dirPath}`,
304+
ErrorCode.FS_READ_ERROR,
305+
{ path: dirPath },
306+
error
307+
);
308+
}
309+
throw error;
310+
}
311+
}
312+
}

0 commit comments

Comments
 (0)