btca-server 1.0.962 → 2.0.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/package.json +3 -3
- package/src/agent/agent.test.ts +31 -24
- package/src/agent/index.ts +8 -2
- package/src/agent/loop.ts +303 -346
- package/src/agent/service.ts +252 -233
- package/src/agent/types.ts +2 -2
- package/src/collections/index.ts +2 -1
- package/src/collections/service.ts +352 -345
- package/src/config/config.test.ts +3 -1
- package/src/config/index.ts +615 -727
- package/src/config/remote.ts +214 -369
- package/src/context/index.ts +6 -12
- package/src/context/transaction.ts +23 -30
- package/src/effect/errors.ts +45 -0
- package/src/effect/layers.ts +26 -0
- package/src/effect/runtime.ts +19 -0
- package/src/effect/services.ts +154 -0
- package/src/index.ts +291 -369
- package/src/metrics/index.ts +46 -46
- package/src/pricing/models-dev.ts +104 -106
- package/src/providers/auth.ts +159 -200
- package/src/providers/index.ts +19 -2
- package/src/providers/model.ts +115 -135
- package/src/providers/openai.ts +3 -3
- package/src/resources/impls/git.ts +123 -146
- package/src/resources/impls/npm.test.ts +16 -5
- package/src/resources/impls/npm.ts +66 -75
- package/src/resources/index.ts +6 -1
- package/src/resources/schema.ts +7 -6
- package/src/resources/service.test.ts +13 -12
- package/src/resources/service.ts +153 -112
- package/src/stream/index.ts +1 -1
- package/src/stream/service.test.ts +5 -5
- package/src/stream/service.ts +282 -293
- package/src/tools/glob.ts +126 -141
- package/src/tools/grep.ts +205 -210
- package/src/tools/index.ts +8 -4
- package/src/tools/list.ts +118 -140
- package/src/tools/read.ts +209 -235
- package/src/tools/virtual-sandbox.ts +91 -83
- package/src/validation/index.ts +18 -22
- package/src/vfs/virtual-fs.test.ts +37 -25
- package/src/vfs/virtual-fs.ts +218 -216
|
@@ -3,7 +3,6 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { Result } from 'better-result';
|
|
7
6
|
|
|
8
7
|
import { loadNpmResource } from './npm.ts';
|
|
9
8
|
import type { BtcaNpmResourceArgs } from '../types.ts';
|
|
@@ -209,12 +208,24 @@ describe('NPM Resource', () => {
|
|
|
209
208
|
expect(resource.cleanup).toBeDefined();
|
|
210
209
|
|
|
211
210
|
const resourcePath = await resource.getAbsoluteDirectoryPath();
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
let existsBefore = false;
|
|
212
|
+
try {
|
|
213
|
+
await fs.stat(resourcePath);
|
|
214
|
+
existsBefore = true;
|
|
215
|
+
} catch {
|
|
216
|
+
existsBefore = false;
|
|
217
|
+
}
|
|
218
|
+
expect(existsBefore).toBe(true);
|
|
214
219
|
|
|
215
220
|
await resource.cleanup?.();
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
let existsAfter = false;
|
|
222
|
+
try {
|
|
223
|
+
await fs.stat(resourcePath);
|
|
224
|
+
existsAfter = true;
|
|
225
|
+
} catch {
|
|
226
|
+
existsAfter = false;
|
|
227
|
+
}
|
|
228
|
+
expect(existsAfter).toBe(false);
|
|
218
229
|
});
|
|
219
230
|
|
|
220
231
|
it('uses readable fsName aliases for anonymous npm resources', async () => {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { Result } from 'better-result';
|
|
5
|
-
|
|
6
4
|
import { CommonHints } from '../../errors.ts';
|
|
7
5
|
import { ResourceError, resourceNameToKey } from '../helpers.ts';
|
|
8
6
|
import type { BtcaFsResource, BtcaNpmResourceArgs } from '../types.ts';
|
|
@@ -42,15 +40,20 @@ type NpmPackageVersion = {
|
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
const cleanupDirectory = async (pathToRemove: string) => {
|
|
45
|
-
|
|
43
|
+
try {
|
|
44
|
+
await fs.rm(pathToRemove, { recursive: true, force: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const directoryExists = async (directoryPath: string) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.stat(directoryPath);
|
|
53
|
+
return stat.isDirectory();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
const encodePackagePath = (packageName: string) =>
|
|
@@ -80,64 +83,61 @@ const resolveRequestedVersion = (packument: NpmPackument, requestedVersion?: str
|
|
|
80
83
|
};
|
|
81
84
|
|
|
82
85
|
const fetchJson = async <T>(url: string, resourceName: string): Promise<T> => {
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
let response: Response;
|
|
87
|
+
try {
|
|
88
|
+
response = await fetch(url, {
|
|
85
89
|
headers: {
|
|
86
90
|
accept: 'application/json'
|
|
87
91
|
}
|
|
88
|
-
})
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
if (!Result.isOk(response)) {
|
|
92
|
+
});
|
|
93
|
+
} catch (cause) {
|
|
92
94
|
throw new ResourceError({
|
|
93
95
|
message: `Failed to fetch npm metadata for "${resourceName}"`,
|
|
94
96
|
hint: CommonHints.CHECK_NETWORK,
|
|
95
|
-
cause
|
|
97
|
+
cause
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
if (!response.
|
|
101
|
+
if (!response.ok) {
|
|
100
102
|
throw new ResourceError({
|
|
101
|
-
message: `Failed to fetch npm metadata for "${resourceName}" (${response.
|
|
103
|
+
message: `Failed to fetch npm metadata for "${resourceName}" (${response.status})`,
|
|
102
104
|
hint:
|
|
103
|
-
response.
|
|
104
|
-
|
|
105
|
-
: CommonHints.CHECK_NETWORK,
|
|
106
|
-
cause: new Error(`Unexpected status ${response.value.status}`)
|
|
105
|
+
response.status === 404 ? 'Check that the npm package exists.' : CommonHints.CHECK_NETWORK,
|
|
106
|
+
cause: new Error(`Unexpected status ${response.status}`)
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
return (await response.json()) as T;
|
|
112
|
+
} catch (cause) {
|
|
112
113
|
throw new ResourceError({
|
|
113
114
|
message: `Failed to parse npm metadata for "${resourceName}"`,
|
|
114
115
|
hint: 'Try again. If the issue persists, the npm registry may be returning malformed data.',
|
|
115
|
-
cause
|
|
116
|
+
cause
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
return parsed.value;
|
|
120
119
|
};
|
|
121
120
|
|
|
122
121
|
const fetchText = async (url: string, resourceName: string) => {
|
|
123
122
|
const fallbackContent = (reason: string) =>
|
|
124
123
|
`<!-- npm package page unavailable for "${resourceName}" (${reason}) -->`;
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
let response: Response;
|
|
126
|
+
try {
|
|
127
|
+
response = await fetch(url);
|
|
128
|
+
} catch {
|
|
128
129
|
return fallbackContent('request failed');
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
if (!response.
|
|
132
|
-
return fallbackContent(`status ${response.
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
return fallbackContent(`status ${response.status}`);
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
try {
|
|
137
|
+
return await response.text();
|
|
138
|
+
} catch {
|
|
137
139
|
return fallbackContent('response read failed');
|
|
138
140
|
}
|
|
139
|
-
|
|
140
|
-
return textResult.value;
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
const readProcessOutput = async (stream: ReadableStream<Uint8Array> | null) => {
|
|
@@ -243,18 +243,12 @@ const formatResourceOverview = (args: {
|
|
|
243
243
|
};
|
|
244
244
|
|
|
245
245
|
const readCacheMeta = async (localPath: string): Promise<NpmCacheMeta | null> => {
|
|
246
|
-
|
|
247
|
-
const content =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return result.match({
|
|
255
|
-
ok: (value) => value,
|
|
256
|
-
err: () => null
|
|
257
|
-
});
|
|
246
|
+
try {
|
|
247
|
+
const content = await Bun.file(path.join(localPath, NPM_CACHE_META_FILE)).text();
|
|
248
|
+
return JSON.parse(content) as NpmCacheMeta;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
258
252
|
};
|
|
259
253
|
|
|
260
254
|
const shouldReuseCache = async (
|
|
@@ -282,19 +276,18 @@ const installPackageFiles = async (args: {
|
|
|
282
276
|
const installDirectory = path.join(args.localPath, NPM_INSTALL_STAGING_DIR);
|
|
283
277
|
const packagePath = path.join(installDirectory, 'node_modules', ...args.packageName.split('/'));
|
|
284
278
|
|
|
285
|
-
|
|
286
|
-
fs.mkdir(installDirectory, { recursive: true })
|
|
287
|
-
)
|
|
288
|
-
if (!Result.isOk(createInstallDirectory)) {
|
|
279
|
+
try {
|
|
280
|
+
await fs.mkdir(installDirectory, { recursive: true });
|
|
281
|
+
} catch (cause) {
|
|
289
282
|
throw new ResourceError({
|
|
290
283
|
message: `Failed to prepare npm install workspace for "${args.packageName}"`,
|
|
291
284
|
hint: 'Check that the btca data directory is writable.',
|
|
292
|
-
cause
|
|
285
|
+
cause
|
|
293
286
|
});
|
|
294
287
|
}
|
|
295
288
|
|
|
296
|
-
|
|
297
|
-
Bun.write(
|
|
289
|
+
try {
|
|
290
|
+
await Bun.write(
|
|
298
291
|
path.join(installDirectory, 'package.json'),
|
|
299
292
|
JSON.stringify(
|
|
300
293
|
{
|
|
@@ -304,13 +297,12 @@ const installPackageFiles = async (args: {
|
|
|
304
297
|
null,
|
|
305
298
|
2
|
|
306
299
|
)
|
|
307
|
-
)
|
|
308
|
-
)
|
|
309
|
-
if (!Result.isOk(writeManifest)) {
|
|
300
|
+
);
|
|
301
|
+
} catch (cause) {
|
|
310
302
|
throw new ResourceError({
|
|
311
303
|
message: `Failed to prepare npm install workspace for "${args.packageName}"`,
|
|
312
304
|
hint: 'Check that the btca data directory is writable.',
|
|
313
|
-
cause
|
|
305
|
+
cause
|
|
314
306
|
});
|
|
315
307
|
}
|
|
316
308
|
|
|
@@ -329,14 +321,13 @@ const installPackageFiles = async (args: {
|
|
|
329
321
|
});
|
|
330
322
|
}
|
|
331
323
|
|
|
332
|
-
|
|
333
|
-
fs.cp(packagePath, args.localPath, { recursive: true, force: true })
|
|
334
|
-
)
|
|
335
|
-
if (!Result.isOk(copyResult)) {
|
|
324
|
+
try {
|
|
325
|
+
await fs.cp(packagePath, args.localPath, { recursive: true, force: true });
|
|
326
|
+
} catch (cause) {
|
|
336
327
|
throw new ResourceError({
|
|
337
328
|
message: `Failed to copy installed npm package files for "${args.packageName}"`,
|
|
338
329
|
hint: 'Check filesystem permissions and available disk space.',
|
|
339
|
-
cause
|
|
330
|
+
cause
|
|
340
331
|
});
|
|
341
332
|
}
|
|
342
333
|
} finally {
|
|
@@ -431,27 +422,27 @@ const ensureNpmResource = async (config: BtcaNpmResourceArgs): Promise<string> =
|
|
|
431
422
|
: config.resourcesDirectoryPath;
|
|
432
423
|
const localPath = path.join(basePath, resourceKey);
|
|
433
424
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
if (!Result.isOk(mkdirResult)) throw mkdirResult.error;
|
|
425
|
+
try {
|
|
426
|
+
await fs.mkdir(basePath, { recursive: true });
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
throw new ResourceError({
|
|
429
|
+
message: 'Failed to create resources directory',
|
|
430
|
+
hint: 'Check that you have write permissions to the btca data directory.',
|
|
431
|
+
cause
|
|
432
|
+
});
|
|
433
|
+
}
|
|
444
434
|
|
|
445
435
|
const canReuse = await shouldReuseCache(config, localPath);
|
|
446
436
|
if (canReuse) return localPath;
|
|
447
437
|
|
|
448
438
|
await cleanupDirectory(localPath);
|
|
449
|
-
|
|
450
|
-
|
|
439
|
+
try {
|
|
440
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
441
|
+
} catch (cause) {
|
|
451
442
|
throw new ResourceError({
|
|
452
443
|
message: `Failed to prepare npm resource directory for "${config.name}"`,
|
|
453
444
|
hint: 'Check that the btca data directory is writable.',
|
|
454
|
-
cause
|
|
445
|
+
cause
|
|
455
446
|
});
|
|
456
447
|
}
|
|
457
448
|
|
package/src/resources/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { ResourceError } from './helpers.ts';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
createResourcesService,
|
|
4
|
+
createAnonymousResource,
|
|
5
|
+
resolveResourceDefinition
|
|
6
|
+
} from './service.ts';
|
|
7
|
+
export type { ResourcesService } from './service.ts';
|
|
3
8
|
export {
|
|
4
9
|
GitResourceSchema,
|
|
5
10
|
LocalResourceSchema,
|
package/src/resources/schema.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Result } from 'better-result';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
|
|
4
3
|
import { LIMITS } from '../validation/index.ts';
|
|
@@ -21,11 +20,13 @@ const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.-]+$/;
|
|
|
21
20
|
const NPM_PACKAGE_SEGMENT_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
|
|
22
21
|
const NPM_VERSION_OR_TAG_REGEX = /^[^\s/]+$/;
|
|
23
22
|
|
|
24
|
-
const parseUrl = (value: string) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
const parseUrl = (value: string) => {
|
|
24
|
+
try {
|
|
25
|
+
return new URL(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
29
30
|
|
|
30
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
32
|
// Field Schemas
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createAnonymousDirectoryKey,
|
|
5
|
+
createAnonymousResource,
|
|
6
|
+
resolveResourceDefinition
|
|
7
|
+
} from './service.ts';
|
|
4
8
|
import { resourceNameToKey } from './helpers.ts';
|
|
5
9
|
import { type ResourceDefinition } from './schema.ts';
|
|
6
10
|
|
|
@@ -16,13 +20,13 @@ describe('Resources.resolveResourceDefinition', () => {
|
|
|
16
20
|
const getResource = (name: string) => (name === 'svelte' ? configuredResource : undefined);
|
|
17
21
|
|
|
18
22
|
it('resolves configured resources by name first', () => {
|
|
19
|
-
const definition =
|
|
23
|
+
const definition = resolveResourceDefinition('svelte', getResource);
|
|
20
24
|
expect(definition.type).toBe('git');
|
|
21
25
|
expect(definition.name).toBe('svelte');
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
it('creates anonymous git resources from valid URLs', () => {
|
|
25
|
-
const definition =
|
|
29
|
+
const definition = resolveResourceDefinition(
|
|
26
30
|
'https://github.com/sveltejs/svelte.dev/tree/main/packages',
|
|
27
31
|
() => undefined
|
|
28
32
|
);
|
|
@@ -35,10 +39,7 @@ describe('Resources.resolveResourceDefinition', () => {
|
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
it('creates anonymous npm resources from npm references', () => {
|
|
38
|
-
const definition =
|
|
39
|
-
'npm:@types/node@22.10.1',
|
|
40
|
-
() => undefined
|
|
41
|
-
);
|
|
42
|
+
const definition = resolveResourceDefinition('npm:@types/node@22.10.1', () => undefined);
|
|
42
43
|
expect(definition.type).toBe('npm');
|
|
43
44
|
if (definition.type === 'npm') {
|
|
44
45
|
expect(definition.package).toBe('@types/node');
|
|
@@ -48,7 +49,7 @@ describe('Resources.resolveResourceDefinition', () => {
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
it('creates anonymous npm resources from npm package URLs', () => {
|
|
51
|
-
const definition =
|
|
52
|
+
const definition = resolveResourceDefinition(
|
|
52
53
|
'https://www.npmjs.com/package/react/v/19.0.0',
|
|
53
54
|
() => undefined
|
|
54
55
|
);
|
|
@@ -60,8 +61,8 @@ describe('Resources.resolveResourceDefinition', () => {
|
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
it('reuses the same cache key for repeated normalized URLs', () => {
|
|
63
|
-
const first =
|
|
64
|
-
const second =
|
|
64
|
+
const first = createAnonymousResource('https://github.com/sveltejs/svelte.dev');
|
|
65
|
+
const second = createAnonymousResource(
|
|
65
66
|
'https://github.com/sveltejs/svelte.dev/blob/main/packages'
|
|
66
67
|
);
|
|
67
68
|
expect(first).not.toBeNull();
|
|
@@ -72,8 +73,8 @@ describe('Resources.resolveResourceDefinition', () => {
|
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
it('uses short deterministic keys for anonymous repository paths', () => {
|
|
75
|
-
const main =
|
|
76
|
-
const withPath =
|
|
76
|
+
const main = createAnonymousResource('https://github.com/sveltejs/svelte.dev');
|
|
77
|
+
const withPath = createAnonymousResource(
|
|
77
78
|
'https://github.com/sveltejs/svelte.dev/tree/main/packages'
|
|
78
79
|
);
|
|
79
80
|
expect(main).not.toBeNull();
|