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
|
@@ -1,381 +1,388 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import type { ConfigService as ConfigServiceShape } from '../config/index.ts';
|
|
6
|
+
import { runTransaction } from '../context/transaction.ts';
|
|
7
7
|
import { CommonHints, getErrorHint, getErrorMessage } from '../errors.ts';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { metricsInfo } from '../metrics/index.ts';
|
|
9
|
+
import type { ResourcesService } from '../resources/service.ts';
|
|
10
10
|
import { isGitResource, isNpmResource } from '../resources/schema.ts';
|
|
11
11
|
import { FS_RESOURCE_SYSTEM_NOTE, type BtcaFsResource } from '../resources/types.ts';
|
|
12
12
|
import { parseNpmReference } from '../validation/index.ts';
|
|
13
13
|
import { CollectionError, getCollectionKey, type CollectionResult } from './types.ts';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createVirtualFs,
|
|
16
|
+
disposeVirtualFs,
|
|
17
|
+
importDirectoryIntoVirtualFs,
|
|
18
|
+
mkdirVirtualFs,
|
|
19
|
+
rmVirtualFs
|
|
20
|
+
} from '../vfs/virtual-fs.ts';
|
|
15
21
|
import {
|
|
16
22
|
clearVirtualCollectionMetadata,
|
|
17
23
|
setVirtualCollectionMetadata,
|
|
18
24
|
type VirtualResourceMetadata
|
|
19
25
|
} from './virtual-metadata.ts';
|
|
20
26
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
27
|
+
export type CollectionsService = {
|
|
28
|
+
load: (args: { resourceNames: readonly string[]; quiet?: boolean }) => Promise<CollectionResult>;
|
|
29
|
+
loadEffect: (args: {
|
|
30
|
+
resourceNames: readonly string[];
|
|
31
|
+
quiet?: boolean;
|
|
32
|
+
}) => Effect.Effect<CollectionResult, CollectionError>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const encodePathSegments = (value: string) => value.split('/').map(encodeURIComponent).join('/');
|
|
36
|
+
|
|
37
|
+
const trimGitSuffix = (url: string) => url.replace(/\.git$/u, '').replace(/\/+$/u, '');
|
|
38
|
+
const getNpmCitationAlias = (metadata?: VirtualResourceMetadata) => {
|
|
39
|
+
if (!metadata?.package) return undefined;
|
|
40
|
+
return `npm:${metadata.package}@${metadata.version ?? 'latest'}`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createCollectionInstructionBlock = (
|
|
44
|
+
resource: BtcaFsResource,
|
|
45
|
+
metadata?: VirtualResourceMetadata
|
|
46
|
+
): string => {
|
|
47
|
+
const focusLines = resource.repoSubPaths.map(
|
|
48
|
+
(subPath) => `Focus: ./${resource.fsName}/${subPath}`
|
|
49
|
+
);
|
|
50
|
+
const gitRef = metadata?.branch ?? metadata?.commit;
|
|
51
|
+
const githubPrefix =
|
|
52
|
+
resource.type === 'git' && metadata?.url && gitRef
|
|
53
|
+
? `${trimGitSuffix(metadata.url)}/blob/${encodeURIComponent(gitRef)}`
|
|
54
|
+
: undefined;
|
|
55
|
+
const npmCitationAlias = resource.type === 'npm' ? getNpmCitationAlias(metadata) : undefined;
|
|
56
|
+
const lines = [
|
|
57
|
+
`## Resource: ${resource.name}`,
|
|
58
|
+
FS_RESOURCE_SYSTEM_NOTE,
|
|
59
|
+
`Path: ./${resource.fsName}`,
|
|
60
|
+
resource.type === 'git' && metadata?.url ? `Repo URL: ${trimGitSuffix(metadata.url)}` : '',
|
|
61
|
+
resource.type === 'git' && metadata?.branch ? `Repo Branch: ${metadata.branch}` : '',
|
|
62
|
+
resource.type === 'git' && metadata?.commit ? `Repo Commit: ${metadata.commit}` : '',
|
|
63
|
+
resource.type === 'npm' && metadata?.package ? `NPM Package: ${metadata.package}` : '',
|
|
64
|
+
resource.type === 'npm' && metadata?.version ? `NPM Version: ${metadata.version}` : '',
|
|
65
|
+
resource.type === 'npm' && metadata?.url ? `NPM URL: ${metadata.url}` : '',
|
|
66
|
+
npmCitationAlias ? `NPM Citation Alias: ${npmCitationAlias}` : '',
|
|
67
|
+
githubPrefix ? `GitHub Blob Prefix: ${githubPrefix}` : '',
|
|
68
|
+
githubPrefix
|
|
69
|
+
? `GitHub Citation Rule: Convert virtual paths under ./${resource.fsName}/ to repo-relative paths, then encode each path segment for GitHub URLs (example segment: "+page.server.js" -> "${encodeURIComponent('+page.server.js')}").`
|
|
70
|
+
: '',
|
|
71
|
+
githubPrefix
|
|
72
|
+
? `GitHub Citation Example: ${githubPrefix}/${encodePathSegments('src/routes/blog/+page.server.js')}`
|
|
73
|
+
: '',
|
|
74
|
+
resource.type !== 'git'
|
|
75
|
+
? 'Citation Rule: Cite local file paths only for this resource (no GitHub URL).'
|
|
76
|
+
: '',
|
|
77
|
+
npmCitationAlias
|
|
78
|
+
? `NPM Citation Rule: In "Sources", cite npm files using "${npmCitationAlias}/<file>" (for example, "${npmCitationAlias}/README.md"). Do not cite encoded virtual folder names.`
|
|
79
|
+
: '',
|
|
80
|
+
...focusLines,
|
|
81
|
+
resource.specialAgentInstructions ? `Notes: ${resource.specialAgentInstructions}` : ''
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ignoreErrors = async (action: () => Promise<unknown>) => {
|
|
88
|
+
try {
|
|
89
|
+
await action();
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const initVirtualRoot = async (collectionPath: string, vfsId: string) => {
|
|
96
|
+
try {
|
|
97
|
+
await mkdirVirtualFs(collectionPath, { recursive: true }, vfsId);
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
throw new CollectionError({
|
|
100
|
+
message: `Failed to initialize virtual collection root: "${collectionPath}"`,
|
|
101
|
+
hint: 'Check that the virtual filesystem is available.',
|
|
102
|
+
cause
|
|
86
103
|
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const loadResource = async (resources: ResourcesService, name: string, quiet: boolean) => {
|
|
108
|
+
try {
|
|
109
|
+
return await resources.load(name, { quiet });
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
const underlyingHint = getErrorHint(cause);
|
|
112
|
+
const underlyingMessage = getErrorMessage(cause);
|
|
113
|
+
throw new CollectionError({
|
|
114
|
+
message: `Failed to load resource "${name}": ${underlyingMessage}`,
|
|
115
|
+
hint:
|
|
116
|
+
underlyingHint ??
|
|
117
|
+
`${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
|
|
118
|
+
cause
|
|
98
119
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`${CommonHints.CLEAR_CACHE} Check that the resource "${name}" is correctly configured.`,
|
|
111
|
-
cause
|
|
112
|
-
});
|
|
113
|
-
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resolveResourcePath = async (resource: BtcaFsResource) => {
|
|
124
|
+
try {
|
|
125
|
+
return await resource.getAbsoluteDirectoryPath();
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
throw new CollectionError({
|
|
128
|
+
message: `Failed to get path for resource "${resource.name}"`,
|
|
129
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
130
|
+
cause
|
|
114
131
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const virtualizeResource = async (args: {
|
|
136
|
+
resource: BtcaFsResource;
|
|
137
|
+
resourcePath: string;
|
|
138
|
+
virtualResourcePath: string;
|
|
139
|
+
vfsId: string;
|
|
140
|
+
}) => {
|
|
141
|
+
try {
|
|
142
|
+
await importDirectoryIntoVirtualFs({
|
|
143
|
+
sourcePath: args.resourcePath,
|
|
144
|
+
destinationPath: args.virtualResourcePath,
|
|
145
|
+
vfsId: args.vfsId,
|
|
146
|
+
ignore: (relativePath) => {
|
|
147
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
148
|
+
return (
|
|
149
|
+
normalized === '.git' || normalized.startsWith('.git/') || normalized.includes('/.git/')
|
|
150
|
+
);
|
|
151
|
+
}
|
|
125
152
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
vfsId: string;
|
|
132
|
-
}) =>
|
|
133
|
-
Result.tryPromise({
|
|
134
|
-
try: () =>
|
|
135
|
-
VirtualFs.importDirectoryFromDisk({
|
|
136
|
-
sourcePath: args.resourcePath,
|
|
137
|
-
destinationPath: args.virtualResourcePath,
|
|
138
|
-
vfsId: args.vfsId,
|
|
139
|
-
ignore: (relativePath) => {
|
|
140
|
-
const normalized = relativePath.split(path.sep).join('/');
|
|
141
|
-
return (
|
|
142
|
-
normalized === '.git' ||
|
|
143
|
-
normalized.startsWith('.git/') ||
|
|
144
|
-
normalized.includes('/.git/')
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}),
|
|
148
|
-
catch: (cause) =>
|
|
149
|
-
new CollectionError({
|
|
150
|
-
message: `Failed to virtualize resource "${args.resource.name}"`,
|
|
151
|
-
hint: CommonHints.CLEAR_CACHE,
|
|
152
|
-
cause
|
|
153
|
-
})
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
throw new CollectionError({
|
|
155
|
+
message: `Failed to virtualize resource "${args.resource.name}"`,
|
|
156
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
157
|
+
cause
|
|
154
158
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const exitCode = await proc.exited;
|
|
165
|
-
if (exitCode !== 0) return undefined;
|
|
166
|
-
const trimmed = stdout.trim();
|
|
167
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const getGitHeadHash = async (resourcePath: string) => {
|
|
163
|
+
try {
|
|
164
|
+
const proc = Bun.spawn(['git', 'rev-parse', 'HEAD'], {
|
|
165
|
+
cwd: resourcePath,
|
|
166
|
+
stdout: 'pipe',
|
|
167
|
+
stderr: 'pipe'
|
|
168
168
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
const stdout = await new Response(proc.stdout).text();
|
|
170
|
+
const exitCode = await proc.exited;
|
|
171
|
+
if (exitCode !== 0) return undefined;
|
|
172
|
+
const trimmed = stdout.trim();
|
|
173
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
174
|
+
} catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const getGitHeadBranch = async (resourcePath: string) => {
|
|
180
|
+
try {
|
|
181
|
+
const proc = Bun.spawn(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
182
|
+
cwd: resourcePath,
|
|
183
|
+
stdout: 'pipe',
|
|
184
|
+
stderr: 'pipe'
|
|
173
185
|
});
|
|
186
|
+
const stdout = await new Response(proc.stdout).text();
|
|
187
|
+
const exitCode = await proc.exited;
|
|
188
|
+
if (exitCode !== 0) return undefined;
|
|
189
|
+
const trimmed = stdout.trim();
|
|
190
|
+
if (!trimmed || trimmed === 'HEAD') return undefined;
|
|
191
|
+
return trimmed;
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const ANON_PREFIX = 'anonymous:';
|
|
198
|
+
const getAnonymousUrlFromName = (name: string) =>
|
|
199
|
+
name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
200
|
+
const NPM_ANON_PREFIX = `${ANON_PREFIX}npm:`;
|
|
201
|
+
const NPM_META_FILE = '.btca-npm-meta.json';
|
|
202
|
+
const getAnonymousNpmReferenceFromName = (name: string) =>
|
|
203
|
+
name.startsWith(NPM_ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
204
|
+
|
|
205
|
+
const readNpmMeta = async (resourcePath: string) => {
|
|
206
|
+
try {
|
|
207
|
+
const content = await Bun.file(path.join(resourcePath, NPM_META_FILE)).text();
|
|
208
|
+
return JSON.parse(content) as {
|
|
209
|
+
packageName?: string;
|
|
210
|
+
resolvedVersion?: string;
|
|
211
|
+
packageUrl?: string;
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const buildVirtualMetadata = async (args: {
|
|
219
|
+
resource: BtcaFsResource;
|
|
220
|
+
resourcePath: string;
|
|
221
|
+
loadedAt: string;
|
|
222
|
+
definition?: ReturnType<ConfigServiceShape['getResource']>;
|
|
223
|
+
}) => {
|
|
224
|
+
const base = {
|
|
225
|
+
name: args.resource.name,
|
|
226
|
+
fsName: args.resource.fsName,
|
|
227
|
+
type: args.resource.type,
|
|
228
|
+
path: args.resourcePath,
|
|
229
|
+
repoSubPaths: args.resource.repoSubPaths,
|
|
230
|
+
loadedAt: args.loadedAt
|
|
174
231
|
};
|
|
175
232
|
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!trimmed || trimmed === 'HEAD') return undefined;
|
|
188
|
-
return trimmed;
|
|
189
|
-
});
|
|
233
|
+
if (args.resource.type === 'npm') {
|
|
234
|
+
const configuredDefinition =
|
|
235
|
+
args.definition && isNpmResource(args.definition) ? args.definition : null;
|
|
236
|
+
const anonymousReference = getAnonymousNpmReferenceFromName(args.resource.name);
|
|
237
|
+
const anonymousNpm = anonymousReference ? parseNpmReference(anonymousReference) : null;
|
|
238
|
+
const cached = await readNpmMeta(args.resourcePath);
|
|
239
|
+
const packageName =
|
|
240
|
+
configuredDefinition?.package ?? cached?.packageName ?? anonymousNpm?.packageName;
|
|
241
|
+
const version =
|
|
242
|
+
configuredDefinition?.version ?? cached?.resolvedVersion ?? anonymousNpm?.version;
|
|
243
|
+
const url = cached?.packageUrl ?? anonymousNpm?.packageUrl;
|
|
190
244
|
|
|
191
|
-
return
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
245
|
+
return {
|
|
246
|
+
...base,
|
|
247
|
+
...(packageName ? { package: packageName } : {}),
|
|
248
|
+
...(version ? { version } : {}),
|
|
249
|
+
...(url ? { url } : {})
|
|
250
|
+
};
|
|
251
|
+
}
|
|
196
252
|
|
|
197
|
-
|
|
198
|
-
const getAnonymousUrlFromName = (name: string) =>
|
|
199
|
-
name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
200
|
-
const NPM_ANON_PREFIX = `${ANON_PREFIX}npm:`;
|
|
201
|
-
const NPM_META_FILE = '.btca-npm-meta.json';
|
|
202
|
-
const getAnonymousNpmReferenceFromName = (name: string) =>
|
|
203
|
-
name.startsWith(NPM_ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
204
|
-
|
|
205
|
-
const readNpmMeta = async (resourcePath: string) => {
|
|
206
|
-
const result = await Result.gen(async function* () {
|
|
207
|
-
const content = yield* Result.await(
|
|
208
|
-
Result.tryPromise(() => Bun.file(path.join(resourcePath, NPM_META_FILE)).text())
|
|
209
|
-
);
|
|
210
|
-
const parsed = yield* Result.try(
|
|
211
|
-
() =>
|
|
212
|
-
JSON.parse(content) as {
|
|
213
|
-
packageName?: string;
|
|
214
|
-
resolvedVersion?: string;
|
|
215
|
-
packageUrl?: string;
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
return Result.ok(parsed);
|
|
219
|
-
});
|
|
253
|
+
if (args.resource.type !== 'git') return base;
|
|
220
254
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
const configuredDefinition =
|
|
256
|
+
args.definition && isGitResource(args.definition) ? args.definition : null;
|
|
257
|
+
const url = configuredDefinition?.url ?? getAnonymousUrlFromName(args.resource.name);
|
|
258
|
+
const branch = configuredDefinition?.branch ?? (await getGitHeadBranch(args.resourcePath));
|
|
259
|
+
const commit = await getGitHeadHash(args.resourcePath);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
...base,
|
|
263
|
+
...(url ? { url } : {}),
|
|
264
|
+
...(branch ? { branch } : {}),
|
|
265
|
+
...(commit ? { commit } : {})
|
|
225
266
|
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const createCollectionsService = (args: {
|
|
270
|
+
config: ConfigServiceShape;
|
|
271
|
+
resources: ResourcesService;
|
|
272
|
+
}): CollectionsService => {
|
|
273
|
+
const load: CollectionsService['load'] = ({ resourceNames, quiet = false }) =>
|
|
274
|
+
runTransaction('collections.load', async () => {
|
|
275
|
+
const uniqueNames = Array.from(new Set(resourceNames));
|
|
276
|
+
if (uniqueNames.length === 0)
|
|
277
|
+
throw new CollectionError({
|
|
278
|
+
message: 'Cannot create collection with no resources',
|
|
279
|
+
hint: `${CommonHints.LIST_RESOURCES} ${CommonHints.ADD_RESOURCE}`
|
|
280
|
+
});
|
|
226
281
|
|
|
227
|
-
|
|
228
|
-
resource: BtcaFsResource;
|
|
229
|
-
resourcePath: string;
|
|
230
|
-
loadedAt: string;
|
|
231
|
-
definition?: ReturnType<Config.Service['getResource']>;
|
|
232
|
-
}) => {
|
|
233
|
-
const base = {
|
|
234
|
-
name: args.resource.name,
|
|
235
|
-
fsName: args.resource.fsName,
|
|
236
|
-
type: args.resource.type,
|
|
237
|
-
path: args.resourcePath,
|
|
238
|
-
repoSubPaths: args.resource.repoSubPaths,
|
|
239
|
-
loadedAt: args.loadedAt
|
|
240
|
-
};
|
|
282
|
+
metricsInfo('collections.load', { resources: uniqueNames, quiet });
|
|
241
283
|
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
configuredDefinition?.package ?? cached?.packageName ?? anonymousNpm?.packageName;
|
|
250
|
-
const version =
|
|
251
|
-
configuredDefinition?.version ?? cached?.resolvedVersion ?? anonymousNpm?.version;
|
|
252
|
-
const url = cached?.packageUrl ?? anonymousNpm?.packageUrl;
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
...base,
|
|
256
|
-
...(packageName ? { package: packageName } : {}),
|
|
257
|
-
...(version ? { version } : {}),
|
|
258
|
-
...(url ? { url } : {})
|
|
284
|
+
const sortedNames = [...uniqueNames].sort((a, b) => a.localeCompare(b));
|
|
285
|
+
const key = getCollectionKey(sortedNames);
|
|
286
|
+
const collectionPath = '/';
|
|
287
|
+
const vfsId = createVirtualFs();
|
|
288
|
+
const cleanupVirtual = () => {
|
|
289
|
+
disposeVirtualFs(vfsId);
|
|
290
|
+
clearVirtualCollectionMetadata(vfsId);
|
|
259
291
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
292
|
+
const cleanupResources = (resources: BtcaFsResource[]) =>
|
|
293
|
+
Promise.all(
|
|
294
|
+
resources.map(async (resource) => {
|
|
295
|
+
if (!resource.cleanup) return;
|
|
296
|
+
await ignoreErrors(() => resource.cleanup!());
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const loadedResources: BtcaFsResource[] = [];
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await initVirtualRoot(collectionPath, vfsId);
|
|
304
|
+
|
|
305
|
+
for (const name of sortedNames) {
|
|
306
|
+
const resource = await loadResource(args.resources, name, quiet);
|
|
307
|
+
loadedResources.push(resource);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const metadataResources: VirtualResourceMetadata[] = [];
|
|
311
|
+
const loadedAt = new Date().toISOString();
|
|
312
|
+
for (const resource of loadedResources) {
|
|
313
|
+
const resourcePath = await resolveResourcePath(resource);
|
|
314
|
+
const virtualResourcePath = path.posix.join('/', resource.fsName);
|
|
315
|
+
|
|
316
|
+
await ignoreErrors(() =>
|
|
317
|
+
rmVirtualFs(virtualResourcePath, { recursive: true, force: true }, vfsId)
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
await virtualizeResource({
|
|
321
|
+
resource,
|
|
322
|
+
resourcePath,
|
|
323
|
+
virtualResourcePath,
|
|
324
|
+
vfsId
|
|
325
|
+
});
|
|
277
326
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
Transaction.run('collections.load', async () => {
|
|
285
|
-
const uniqueNames = Array.from(new Set(resourceNames));
|
|
286
|
-
if (uniqueNames.length === 0)
|
|
287
|
-
throw new CollectionError({
|
|
288
|
-
message: 'Cannot create collection with no resources',
|
|
289
|
-
hint: `${CommonHints.LIST_RESOURCES} ${CommonHints.ADD_RESOURCE}`
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
Metrics.info('collections.load', { resources: uniqueNames, quiet });
|
|
293
|
-
|
|
294
|
-
const sortedNames = [...uniqueNames].sort((a, b) => a.localeCompare(b));
|
|
295
|
-
const key = getCollectionKey(sortedNames);
|
|
296
|
-
const collectionPath = '/';
|
|
297
|
-
const vfsId = VirtualFs.create();
|
|
298
|
-
const cleanupVirtual = () => {
|
|
299
|
-
VirtualFs.dispose(vfsId);
|
|
300
|
-
clearVirtualCollectionMetadata(vfsId);
|
|
301
|
-
};
|
|
302
|
-
const cleanupResources = (resources: BtcaFsResource[]) =>
|
|
303
|
-
Promise.all(
|
|
304
|
-
resources.map(async (resource) => {
|
|
305
|
-
if (!resource.cleanup) return;
|
|
306
|
-
await ignoreErrors(() => resource.cleanup!());
|
|
307
|
-
})
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const loadedResources: BtcaFsResource[] = [];
|
|
311
|
-
const result = await Result.gen(async function* () {
|
|
312
|
-
yield* Result.await(initVirtualRoot(collectionPath, vfsId));
|
|
313
|
-
|
|
314
|
-
for (const name of sortedNames) {
|
|
315
|
-
const resource = yield* Result.await(loadResource(args.resources, name, quiet));
|
|
316
|
-
loadedResources.push(resource);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const metadataResources: VirtualResourceMetadata[] = [];
|
|
320
|
-
const loadedAt = new Date().toISOString();
|
|
321
|
-
for (const resource of loadedResources) {
|
|
322
|
-
const resourcePath = yield* Result.await(resolveResourcePath(resource));
|
|
323
|
-
const virtualResourcePath = path.posix.join('/', resource.fsName);
|
|
324
|
-
|
|
325
|
-
await ignoreErrors(() =>
|
|
326
|
-
VirtualFs.rm(virtualResourcePath, { recursive: true, force: true }, vfsId)
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
yield* Result.await(
|
|
330
|
-
virtualizeResource({
|
|
331
|
-
resource,
|
|
332
|
-
resourcePath,
|
|
333
|
-
virtualResourcePath,
|
|
334
|
-
vfsId
|
|
335
|
-
})
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
const definition = args.config.getResource(resource.name);
|
|
339
|
-
const metadata = await buildVirtualMetadata({
|
|
340
|
-
resource,
|
|
341
|
-
resourcePath,
|
|
342
|
-
loadedAt,
|
|
343
|
-
definition
|
|
344
|
-
});
|
|
345
|
-
if (metadata) metadataResources.push(metadata);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
setVirtualCollectionMetadata({
|
|
349
|
-
vfsId,
|
|
350
|
-
collectionKey: key,
|
|
351
|
-
createdAt: loadedAt,
|
|
352
|
-
resources: metadataResources
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const metadataByName = new Map(
|
|
356
|
-
metadataResources.map((resource) => [resource.name, resource])
|
|
357
|
-
);
|
|
358
|
-
const instructionBlocks = loadedResources.map((resource) =>
|
|
359
|
-
createCollectionInstructionBlock(resource, metadataByName.get(resource.name))
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
return Result.ok({
|
|
363
|
-
path: collectionPath,
|
|
364
|
-
agentInstructions: instructionBlocks.join('\n\n'),
|
|
365
|
-
vfsId,
|
|
366
|
-
cleanup: async () => {
|
|
367
|
-
await cleanupResources(loadedResources);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
327
|
+
const definition = args.config.getResource(resource.name);
|
|
328
|
+
const metadata = await buildVirtualMetadata({
|
|
329
|
+
resource,
|
|
330
|
+
resourcePath,
|
|
331
|
+
loadedAt,
|
|
332
|
+
definition
|
|
370
333
|
});
|
|
334
|
+
if (metadata) metadataResources.push(metadata);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setVirtualCollectionMetadata({
|
|
338
|
+
vfsId,
|
|
339
|
+
collectionKey: key,
|
|
340
|
+
createdAt: loadedAt,
|
|
341
|
+
resources: metadataResources
|
|
342
|
+
});
|
|
371
343
|
|
|
372
|
-
|
|
373
|
-
|
|
344
|
+
const metadataByName = new Map(
|
|
345
|
+
metadataResources.map((resource) => [resource.name, resource])
|
|
346
|
+
);
|
|
347
|
+
const instructionBlocks = loadedResources.map((resource) =>
|
|
348
|
+
createCollectionInstructionBlock(resource, metadataByName.get(resource.name))
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
path: collectionPath,
|
|
353
|
+
agentInstructions: instructionBlocks.join('\n\n'),
|
|
354
|
+
vfsId,
|
|
355
|
+
cleanup: async () => {
|
|
374
356
|
await cleanupResources(loadedResources);
|
|
375
|
-
throw result.error;
|
|
376
357
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
358
|
+
};
|
|
359
|
+
} catch (cause) {
|
|
360
|
+
cleanupVirtual();
|
|
361
|
+
await cleanupResources(loadedResources);
|
|
362
|
+
if (cause instanceof CollectionError) throw cause;
|
|
363
|
+
throw new CollectionError({
|
|
364
|
+
message: 'Failed to load resource collection',
|
|
365
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
366
|
+
cause
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const loadEffect: CollectionsService['loadEffect'] = ({ resourceNames, quiet }) =>
|
|
372
|
+
Effect.tryPromise({
|
|
373
|
+
try: () => load({ resourceNames, quiet }),
|
|
374
|
+
catch: (cause) =>
|
|
375
|
+
cause instanceof CollectionError
|
|
376
|
+
? cause
|
|
377
|
+
: new CollectionError({
|
|
378
|
+
message: 'Failed to load resource collection',
|
|
379
|
+
hint: CommonHints.CLEAR_CACHE,
|
|
380
|
+
cause
|
|
381
|
+
})
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
load,
|
|
386
|
+
loadEffect
|
|
380
387
|
};
|
|
381
|
-
}
|
|
388
|
+
};
|