btca-server 1.0.63 → 1.0.71
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 +4 -1
- package/package.json +4 -2
- package/src/agent/agent.test.ts +114 -16
- package/src/agent/loop.ts +14 -11
- package/src/agent/service.ts +117 -86
- package/src/collections/index.ts +0 -0
- package/src/collections/service.ts +187 -57
- package/src/collections/types.ts +1 -0
- package/src/collections/virtual-metadata.ts +32 -0
- package/src/config/config.test.ts +0 -0
- package/src/config/index.ts +195 -127
- package/src/config/remote.ts +132 -79
- package/src/context/index.ts +0 -0
- package/src/context/transaction.ts +20 -15
- package/src/errors.ts +0 -0
- package/src/index.ts +29 -15
- package/src/metrics/index.ts +18 -13
- package/src/providers/auth.ts +38 -11
- package/src/providers/model.ts +3 -1
- package/src/providers/openrouter.ts +39 -0
- package/src/providers/registry.ts +2 -0
- package/src/resources/helpers.ts +0 -0
- package/src/resources/impls/git.test.ts +0 -0
- package/src/resources/impls/git.ts +160 -117
- package/src/resources/index.ts +0 -0
- package/src/resources/schema.ts +24 -27
- package/src/resources/service.ts +0 -0
- package/src/resources/types.ts +0 -0
- package/src/stream/index.ts +0 -0
- package/src/stream/service.ts +23 -14
- package/src/tools/context.ts +4 -0
- package/src/tools/glob.ts +72 -45
- package/src/tools/grep.ts +136 -57
- package/src/tools/index.ts +0 -2
- package/src/tools/list.ts +34 -53
- package/src/tools/read.ts +46 -32
- package/src/tools/virtual-sandbox.ts +103 -0
- package/src/validation/index.ts +12 -12
- package/src/vfs/virtual-fs.test.ts +107 -0
- package/src/vfs/virtual-fs.ts +273 -0
- package/src/tools/ripgrep.ts +0 -348
- package/src/tools/sandbox.ts +0 -164
package/src/tools/ripgrep.ts
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ripgrep Binary Management
|
|
3
|
-
* Handles downloading and caching the ripgrep binary
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from 'node:fs/promises';
|
|
6
|
-
import * as os from 'node:os';
|
|
7
|
-
import * as path from 'node:path';
|
|
8
|
-
|
|
9
|
-
export namespace Ripgrep {
|
|
10
|
-
const VERSION = '14.1.1';
|
|
11
|
-
|
|
12
|
-
// Platform configurations
|
|
13
|
-
const PLATFORM_CONFIG: Record<
|
|
14
|
-
string,
|
|
15
|
-
{ platform: string; extension: 'tar.gz' | 'zip'; binaryName: string }
|
|
16
|
-
> = {
|
|
17
|
-
'darwin-arm64': {
|
|
18
|
-
platform: 'aarch64-apple-darwin',
|
|
19
|
-
extension: 'tar.gz',
|
|
20
|
-
binaryName: 'rg'
|
|
21
|
-
},
|
|
22
|
-
'darwin-x64': {
|
|
23
|
-
platform: 'x86_64-apple-darwin',
|
|
24
|
-
extension: 'tar.gz',
|
|
25
|
-
binaryName: 'rg'
|
|
26
|
-
},
|
|
27
|
-
'linux-arm64': {
|
|
28
|
-
platform: 'aarch64-unknown-linux-gnu',
|
|
29
|
-
extension: 'tar.gz',
|
|
30
|
-
binaryName: 'rg'
|
|
31
|
-
},
|
|
32
|
-
'linux-x64': {
|
|
33
|
-
platform: 'x86_64-unknown-linux-musl',
|
|
34
|
-
extension: 'tar.gz',
|
|
35
|
-
binaryName: 'rg'
|
|
36
|
-
},
|
|
37
|
-
'win32-x64': {
|
|
38
|
-
platform: 'x86_64-pc-windows-msvc',
|
|
39
|
-
extension: 'zip',
|
|
40
|
-
binaryName: 'rg.exe'
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get the btca data directory
|
|
46
|
-
*/
|
|
47
|
-
function getDataPath(): string {
|
|
48
|
-
const platform = os.platform();
|
|
49
|
-
|
|
50
|
-
if (platform === 'win32') {
|
|
51
|
-
const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
52
|
-
return path.join(appdata, 'btca');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Linux and macOS use XDG_DATA_HOME or ~/.local/share
|
|
56
|
-
const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
57
|
-
return path.join(xdgData, 'btca');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get the bin directory for storing binaries
|
|
62
|
-
*/
|
|
63
|
-
function getBinPath(): string {
|
|
64
|
-
return path.join(getDataPath(), 'bin');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get the expected ripgrep binary path
|
|
69
|
-
*/
|
|
70
|
-
function getRipgrepPath(): string {
|
|
71
|
-
const platform = os.platform();
|
|
72
|
-
const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
|
|
73
|
-
return path.join(getBinPath(), binaryName);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Check if ripgrep is already installed in PATH
|
|
78
|
-
*/
|
|
79
|
-
async function findInPath(): Promise<string | null> {
|
|
80
|
-
const rgPath = Bun.which('rg');
|
|
81
|
-
return rgPath || null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Check if our cached ripgrep binary exists
|
|
86
|
-
*/
|
|
87
|
-
async function findCached(): Promise<string | null> {
|
|
88
|
-
const rgPath = getRipgrepPath();
|
|
89
|
-
const file = Bun.file(rgPath);
|
|
90
|
-
if (await file.exists()) {
|
|
91
|
-
return rgPath;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get the platform configuration
|
|
98
|
-
*/
|
|
99
|
-
function getPlatformConfig(): (typeof PLATFORM_CONFIG)[string] | null {
|
|
100
|
-
const platform = os.platform();
|
|
101
|
-
const arch = os.arch();
|
|
102
|
-
const key = `${platform}-${arch}`;
|
|
103
|
-
return PLATFORM_CONFIG[key] || null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Download ripgrep from GitHub releases
|
|
108
|
-
*/
|
|
109
|
-
async function download(): Promise<string> {
|
|
110
|
-
const config = getPlatformConfig();
|
|
111
|
-
if (!config) {
|
|
112
|
-
throw new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const binDir = getBinPath();
|
|
116
|
-
const rgPath = getRipgrepPath();
|
|
117
|
-
|
|
118
|
-
// Ensure bin directory exists
|
|
119
|
-
await fs.mkdir(binDir, { recursive: true });
|
|
120
|
-
|
|
121
|
-
// Build download URL
|
|
122
|
-
const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`;
|
|
123
|
-
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`;
|
|
124
|
-
|
|
125
|
-
console.log(`Downloading ripgrep from ${url}...`);
|
|
126
|
-
|
|
127
|
-
// Download the archive
|
|
128
|
-
const response = await fetch(url);
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
throw new Error(`Failed to download ripgrep: ${response.status} ${response.statusText}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const buffer = await response.arrayBuffer();
|
|
134
|
-
const archivePath = path.join(binDir, filename);
|
|
135
|
-
|
|
136
|
-
// Write archive to disk
|
|
137
|
-
await Bun.write(archivePath, buffer);
|
|
138
|
-
|
|
139
|
-
// Extract based on file type
|
|
140
|
-
if (config.extension === 'tar.gz') {
|
|
141
|
-
// Extract tar.gz
|
|
142
|
-
const proc = Bun.spawn(['tar', '-xzf', archivePath, '--strip-components=1', '-C', binDir], {
|
|
143
|
-
cwd: binDir,
|
|
144
|
-
stdout: 'pipe',
|
|
145
|
-
stderr: 'pipe'
|
|
146
|
-
});
|
|
147
|
-
await proc.exited;
|
|
148
|
-
|
|
149
|
-
if (proc.exitCode !== 0) {
|
|
150
|
-
throw new Error(`Failed to extract ripgrep: exit code ${proc.exitCode}`);
|
|
151
|
-
}
|
|
152
|
-
} else {
|
|
153
|
-
// Extract zip (Windows)
|
|
154
|
-
// Use unzip if available, otherwise use Bun's built-in zip handling
|
|
155
|
-
const proc = Bun.spawn(['unzip', '-o', archivePath, '-d', binDir], {
|
|
156
|
-
cwd: binDir,
|
|
157
|
-
stdout: 'pipe',
|
|
158
|
-
stderr: 'pipe'
|
|
159
|
-
});
|
|
160
|
-
await proc.exited;
|
|
161
|
-
|
|
162
|
-
if (proc.exitCode !== 0) {
|
|
163
|
-
throw new Error(`Failed to extract ripgrep: exit code ${proc.exitCode}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Clean up archive
|
|
168
|
-
await fs.unlink(archivePath).catch(() => {});
|
|
169
|
-
|
|
170
|
-
// Make binary executable (Unix only)
|
|
171
|
-
if (os.platform() !== 'win32') {
|
|
172
|
-
await fs.chmod(rgPath, 0o755);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
console.log(`Ripgrep installed to ${rgPath}`);
|
|
176
|
-
|
|
177
|
-
return rgPath;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Get the path to the ripgrep binary
|
|
182
|
-
* Downloads it if not found in PATH or cache
|
|
183
|
-
*/
|
|
184
|
-
export async function filepath(): Promise<string> {
|
|
185
|
-
// First check PATH
|
|
186
|
-
const inPath = await findInPath();
|
|
187
|
-
if (inPath) {
|
|
188
|
-
return inPath;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Then check cache
|
|
192
|
-
const cached = await findCached();
|
|
193
|
-
if (cached) {
|
|
194
|
-
return cached;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Download if not found
|
|
198
|
-
return download();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Run ripgrep with the given arguments
|
|
203
|
-
*/
|
|
204
|
-
export async function run(
|
|
205
|
-
args: string[],
|
|
206
|
-
options: { cwd?: string } = {}
|
|
207
|
-
): Promise<{
|
|
208
|
-
stdout: string;
|
|
209
|
-
stderr: string;
|
|
210
|
-
exitCode: number;
|
|
211
|
-
}> {
|
|
212
|
-
const rgPath = await filepath();
|
|
213
|
-
|
|
214
|
-
const proc = Bun.spawn([rgPath, ...args], {
|
|
215
|
-
cwd: options.cwd || process.cwd(),
|
|
216
|
-
stdout: 'pipe',
|
|
217
|
-
stderr: 'pipe'
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
221
|
-
new Response(proc.stdout).text(),
|
|
222
|
-
new Response(proc.stderr).text(),
|
|
223
|
-
proc.exited
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
return { stdout, stderr, exitCode };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Generator that yields file paths matching a glob pattern
|
|
231
|
-
*/
|
|
232
|
-
export async function* files(options: {
|
|
233
|
-
cwd: string;
|
|
234
|
-
glob?: string[];
|
|
235
|
-
hidden?: boolean;
|
|
236
|
-
}): AsyncGenerator<string> {
|
|
237
|
-
const rgPath = await filepath();
|
|
238
|
-
|
|
239
|
-
const args = ['--files', '--follow', '--no-messages'];
|
|
240
|
-
|
|
241
|
-
if (options.hidden) {
|
|
242
|
-
args.push('--hidden');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (options.glob) {
|
|
246
|
-
for (const pattern of options.glob) {
|
|
247
|
-
args.push('--glob', pattern);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const proc = Bun.spawn([rgPath, ...args], {
|
|
252
|
-
cwd: options.cwd,
|
|
253
|
-
stdout: 'pipe',
|
|
254
|
-
stderr: 'pipe'
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const stdout = await new Response(proc.stdout).text();
|
|
258
|
-
await proc.exited;
|
|
259
|
-
|
|
260
|
-
for (const line of stdout.trim().split('\n')) {
|
|
261
|
-
if (line) {
|
|
262
|
-
yield line;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Search for a pattern in files
|
|
269
|
-
*/
|
|
270
|
-
export async function search(options: {
|
|
271
|
-
cwd: string;
|
|
272
|
-
pattern: string;
|
|
273
|
-
glob?: string;
|
|
274
|
-
hidden?: boolean;
|
|
275
|
-
maxResults?: number;
|
|
276
|
-
}): Promise<
|
|
277
|
-
Array<{
|
|
278
|
-
path: string;
|
|
279
|
-
lineNumber: number;
|
|
280
|
-
lineText: string;
|
|
281
|
-
}>
|
|
282
|
-
> {
|
|
283
|
-
const rgPath = await filepath();
|
|
284
|
-
|
|
285
|
-
const args = [
|
|
286
|
-
'-n', // line numbers
|
|
287
|
-
'-H', // filename
|
|
288
|
-
'--follow', // follow symlinks
|
|
289
|
-
'--no-messages', // suppress errors
|
|
290
|
-
'--field-match-separator=|' // use | as separator
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
if (options.hidden) {
|
|
294
|
-
args.push('--hidden');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (options.glob) {
|
|
298
|
-
args.push('--glob', options.glob);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
args.push('--regexp', options.pattern);
|
|
302
|
-
|
|
303
|
-
const proc = Bun.spawn([rgPath, ...args], {
|
|
304
|
-
cwd: options.cwd,
|
|
305
|
-
stdout: 'pipe',
|
|
306
|
-
stderr: 'pipe'
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
const stdout = await new Response(proc.stdout).text();
|
|
310
|
-
await proc.exited;
|
|
311
|
-
|
|
312
|
-
const results: Array<{
|
|
313
|
-
path: string;
|
|
314
|
-
lineNumber: number;
|
|
315
|
-
lineText: string;
|
|
316
|
-
}> = [];
|
|
317
|
-
|
|
318
|
-
for (const line of stdout.trim().split('\n')) {
|
|
319
|
-
if (!line) continue;
|
|
320
|
-
|
|
321
|
-
// Parse format: filepath|lineNum|lineText
|
|
322
|
-
const firstPipe = line.indexOf('|');
|
|
323
|
-
if (firstPipe === -1) continue;
|
|
324
|
-
|
|
325
|
-
const secondPipe = line.indexOf('|', firstPipe + 1);
|
|
326
|
-
if (secondPipe === -1) continue;
|
|
327
|
-
|
|
328
|
-
const filePath = line.substring(0, firstPipe);
|
|
329
|
-
const lineNumStr = line.substring(firstPipe + 1, secondPipe);
|
|
330
|
-
const lineText = line.substring(secondPipe + 1);
|
|
331
|
-
|
|
332
|
-
const lineNumber = parseInt(lineNumStr, 10);
|
|
333
|
-
if (isNaN(lineNumber)) continue;
|
|
334
|
-
|
|
335
|
-
results.push({
|
|
336
|
-
path: path.resolve(options.cwd, filePath),
|
|
337
|
-
lineNumber,
|
|
338
|
-
lineText
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (options.maxResults && results.length >= options.maxResults) {
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return results;
|
|
347
|
-
}
|
|
348
|
-
}
|
package/src/tools/sandbox.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path Sandboxing Utilities
|
|
3
|
-
* Ensures all file operations stay within the collections directory
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from 'node:fs/promises';
|
|
6
|
-
import * as path from 'node:path';
|
|
7
|
-
|
|
8
|
-
export namespace Sandbox {
|
|
9
|
-
export class PathEscapeError extends Error {
|
|
10
|
-
readonly _tag = 'PathEscapeError';
|
|
11
|
-
readonly requestedPath: string;
|
|
12
|
-
readonly basePath: string;
|
|
13
|
-
|
|
14
|
-
constructor(requestedPath: string, basePath: string) {
|
|
15
|
-
super(
|
|
16
|
-
`Path "${requestedPath}" is outside the allowed directory "${basePath}". Access denied.`
|
|
17
|
-
);
|
|
18
|
-
this.requestedPath = requestedPath;
|
|
19
|
-
this.basePath = basePath;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class PathNotFoundError extends Error {
|
|
24
|
-
readonly _tag = 'PathNotFoundError';
|
|
25
|
-
readonly requestedPath: string;
|
|
26
|
-
|
|
27
|
-
constructor(requestedPath: string) {
|
|
28
|
-
super(`Path "${requestedPath}" does not exist.`);
|
|
29
|
-
this.requestedPath = requestedPath;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Resolve a path relative to the base path and validate it stays within bounds
|
|
35
|
-
*
|
|
36
|
-
* @param basePath - The allowed base directory (collections path)
|
|
37
|
-
* @param requestedPath - The path requested by the user/agent
|
|
38
|
-
* @returns The resolved absolute path
|
|
39
|
-
* @throws PathEscapeError if the path would escape the base directory
|
|
40
|
-
*/
|
|
41
|
-
export function resolvePath(basePath: string, requestedPath: string): string {
|
|
42
|
-
// Normalize the base path
|
|
43
|
-
const normalizedBase = path.resolve(basePath);
|
|
44
|
-
|
|
45
|
-
// Resolve the requested path relative to the base
|
|
46
|
-
let resolved: string;
|
|
47
|
-
if (path.isAbsolute(requestedPath)) {
|
|
48
|
-
resolved = path.resolve(requestedPath);
|
|
49
|
-
} else {
|
|
50
|
-
resolved = path.resolve(normalizedBase, requestedPath);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Normalize to remove any .. or . components
|
|
54
|
-
resolved = path.normalize(resolved);
|
|
55
|
-
|
|
56
|
-
// Check that the resolved path starts with the base path
|
|
57
|
-
// We need to ensure the path is either exactly the base or within it
|
|
58
|
-
const relative = path.relative(normalizedBase, resolved);
|
|
59
|
-
|
|
60
|
-
// If the relative path starts with '..' or is absolute, it's outside the base
|
|
61
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
62
|
-
throw new PathEscapeError(requestedPath, basePath);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return resolved;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Resolve a path and follow symlinks, validating both the path and its target
|
|
70
|
-
*
|
|
71
|
-
* @param basePath - The allowed base directory (collections path)
|
|
72
|
-
* @param requestedPath - The path requested by the user/agent
|
|
73
|
-
* @returns The resolved real path (after following symlinks)
|
|
74
|
-
* @throws PathEscapeError if the path or symlink target would escape the base directory
|
|
75
|
-
*/
|
|
76
|
-
export async function resolvePathWithSymlinks(
|
|
77
|
-
basePath: string,
|
|
78
|
-
requestedPath: string
|
|
79
|
-
): Promise<string> {
|
|
80
|
-
// First validate the path itself
|
|
81
|
-
const resolved = resolvePath(basePath, requestedPath);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
// Get the real path (follows symlinks)
|
|
85
|
-
const realPath = await fs.realpath(resolved);
|
|
86
|
-
|
|
87
|
-
// For symlinks pointing outside, we allow it since the collection
|
|
88
|
-
// symlinks resources from various locations. The sandbox is about
|
|
89
|
-
// what the agent can ACCESS through the collection, not where the
|
|
90
|
-
// actual files live.
|
|
91
|
-
//
|
|
92
|
-
// The key security boundary is that:
|
|
93
|
-
// 1. The agent can only request paths within the collection directory
|
|
94
|
-
// 2. Those paths may be symlinks to actual resource locations
|
|
95
|
-
// 3. This is intentional - the collection IS the set of accessible resources
|
|
96
|
-
|
|
97
|
-
return realPath;
|
|
98
|
-
} catch (error) {
|
|
99
|
-
// If realpath fails, the file doesn't exist
|
|
100
|
-
// Return the resolved path anyway for error messages
|
|
101
|
-
return resolved;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if a path exists and is within the sandbox
|
|
107
|
-
*/
|
|
108
|
-
export async function exists(basePath: string, requestedPath: string): Promise<boolean> {
|
|
109
|
-
try {
|
|
110
|
-
const resolved = resolvePath(basePath, requestedPath);
|
|
111
|
-
const file = Bun.file(resolved);
|
|
112
|
-
return await file.exists();
|
|
113
|
-
} catch {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check if a path is a directory
|
|
120
|
-
*/
|
|
121
|
-
export async function isDirectory(basePath: string, requestedPath: string): Promise<boolean> {
|
|
122
|
-
try {
|
|
123
|
-
const resolved = resolvePath(basePath, requestedPath);
|
|
124
|
-
const stats = await fs.stat(resolved);
|
|
125
|
-
return stats.isDirectory();
|
|
126
|
-
} catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Check if a path is a file
|
|
133
|
-
*/
|
|
134
|
-
export async function isFile(basePath: string, requestedPath: string): Promise<boolean> {
|
|
135
|
-
try {
|
|
136
|
-
const resolved = resolvePath(basePath, requestedPath);
|
|
137
|
-
const stats = await fs.stat(resolved);
|
|
138
|
-
return stats.isFile();
|
|
139
|
-
} catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Validate a path exists and is within sandbox, throwing if not
|
|
146
|
-
*/
|
|
147
|
-
export async function validatePath(basePath: string, requestedPath: string): Promise<string> {
|
|
148
|
-
const resolved = resolvePath(basePath, requestedPath);
|
|
149
|
-
|
|
150
|
-
const file = Bun.file(resolved);
|
|
151
|
-
if (!(await file.exists())) {
|
|
152
|
-
throw new PathNotFoundError(requestedPath);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return resolved;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Get the relative path from base to the resolved path
|
|
160
|
-
*/
|
|
161
|
-
export function getRelativePath(basePath: string, resolvedPath: string): string {
|
|
162
|
-
return path.relative(basePath, resolvedPath);
|
|
163
|
-
}
|
|
164
|
-
}
|