btca-server 1.0.962 → 2.0.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.
Files changed (43) hide show
  1. package/package.json +3 -3
  2. package/src/agent/agent.test.ts +31 -24
  3. package/src/agent/index.ts +8 -2
  4. package/src/agent/loop.ts +303 -346
  5. package/src/agent/service.ts +252 -233
  6. package/src/agent/types.ts +2 -2
  7. package/src/collections/index.ts +2 -1
  8. package/src/collections/service.ts +352 -345
  9. package/src/config/config.test.ts +3 -1
  10. package/src/config/index.ts +615 -727
  11. package/src/config/remote.ts +214 -369
  12. package/src/context/index.ts +6 -12
  13. package/src/context/transaction.ts +23 -30
  14. package/src/effect/errors.ts +45 -0
  15. package/src/effect/layers.ts +26 -0
  16. package/src/effect/runtime.ts +19 -0
  17. package/src/effect/services.ts +154 -0
  18. package/src/index.ts +291 -369
  19. package/src/metrics/index.ts +46 -46
  20. package/src/pricing/models-dev.ts +104 -106
  21. package/src/providers/auth.ts +159 -200
  22. package/src/providers/index.ts +19 -2
  23. package/src/providers/model.ts +115 -135
  24. package/src/providers/openai.ts +3 -3
  25. package/src/resources/impls/git.ts +123 -146
  26. package/src/resources/impls/npm.test.ts +16 -5
  27. package/src/resources/impls/npm.ts +66 -75
  28. package/src/resources/index.ts +6 -1
  29. package/src/resources/schema.ts +7 -6
  30. package/src/resources/service.test.ts +13 -12
  31. package/src/resources/service.ts +153 -112
  32. package/src/stream/index.ts +1 -1
  33. package/src/stream/service.test.ts +5 -5
  34. package/src/stream/service.ts +282 -293
  35. package/src/tools/glob.ts +126 -141
  36. package/src/tools/grep.ts +205 -210
  37. package/src/tools/index.ts +8 -4
  38. package/src/tools/list.ts +118 -140
  39. package/src/tools/read.ts +209 -235
  40. package/src/tools/virtual-sandbox.ts +91 -83
  41. package/src/validation/index.ts +18 -22
  42. package/src/vfs/virtual-fs.test.ts +37 -25
  43. package/src/vfs/virtual-fs.ts +218 -216
@@ -4,7 +4,12 @@
4
4
  */
5
5
  import type { LanguageModel } from 'ai';
6
6
 
7
- import { Auth } from './auth.ts';
7
+ import {
8
+ getAuthStatus,
9
+ getAuthenticatedProviders,
10
+ getProviderAuthHint,
11
+ isAuthenticated
12
+ } from './auth.ts';
8
13
  import {
9
14
  getProviderFactory,
10
15
  isProviderSupported,
@@ -12,162 +17,137 @@ import {
12
17
  type ProviderOptions
13
18
  } from './registry.ts';
14
19
 
15
- export namespace Model {
16
- export class ProviderNotFoundError extends Error {
17
- readonly _tag = 'ProviderNotFoundError';
18
- readonly providerId: string;
19
- readonly hint: string;
20
-
21
- constructor(providerId: string) {
22
- super(`Provider "${providerId}" is not supported`);
23
- this.providerId = providerId;
24
- this.hint =
25
- 'Open an issue to request this provider: https://github.com/davis7dotsh/better-context/issues.';
26
- }
20
+ export class ProviderNotFoundError extends Error {
21
+ readonly _tag = 'ProviderNotFoundError';
22
+ readonly providerId: string;
23
+ readonly hint: string;
24
+
25
+ constructor(providerId: string) {
26
+ super(`Provider "${providerId}" is not supported`);
27
+ this.providerId = providerId;
28
+ this.hint =
29
+ 'Open an issue to request this provider: https://github.com/davis7dotsh/better-context/issues.';
27
30
  }
31
+ }
28
32
 
29
- export class ProviderNotAuthenticatedError extends Error {
30
- readonly _tag = 'ProviderNotAuthenticatedError';
31
- readonly providerId: string;
32
- readonly hint: string;
33
+ export class ProviderNotAuthenticatedError extends Error {
34
+ readonly _tag = 'ProviderNotAuthenticatedError';
35
+ readonly providerId: string;
36
+ readonly hint: string;
33
37
 
34
- constructor(providerId: string) {
35
- super(`Provider "${providerId}" is not authenticated.`);
36
- this.providerId = providerId;
37
- this.hint = Auth.getProviderAuthHint(providerId);
38
- }
38
+ constructor(providerId: string) {
39
+ super(`Provider "${providerId}" is not authenticated.`);
40
+ this.providerId = providerId;
41
+ this.hint = getProviderAuthHint(providerId);
39
42
  }
43
+ }
40
44
 
41
- export class ProviderAuthTypeError extends Error {
42
- readonly _tag = 'ProviderAuthTypeError';
43
- readonly providerId: string;
44
- readonly authType: string;
45
- readonly hint: string;
46
-
47
- constructor(args: { providerId: string; authType: string }) {
48
- super(`Provider "${args.providerId}" does not support "${args.authType}" auth.`);
49
- this.providerId = args.providerId;
50
- this.authType = args.authType;
51
- this.hint = Auth.getProviderAuthHint(args.providerId);
52
- }
45
+ export class ProviderAuthTypeError extends Error {
46
+ readonly _tag = 'ProviderAuthTypeError';
47
+ readonly providerId: string;
48
+ readonly authType: string;
49
+ readonly hint: string;
50
+
51
+ constructor(args: { providerId: string; authType: string }) {
52
+ super(`Provider "${args.providerId}" does not support "${args.authType}" auth.`);
53
+ this.providerId = args.providerId;
54
+ this.authType = args.authType;
55
+ this.hint = getProviderAuthHint(args.providerId);
53
56
  }
57
+ }
54
58
 
55
- export class ProviderOptionsError extends Error {
56
- readonly _tag = 'ProviderOptionsError';
57
- readonly providerId: string;
58
- readonly hint: string;
59
+ export class ProviderOptionsError extends Error {
60
+ readonly _tag = 'ProviderOptionsError';
61
+ readonly providerId: string;
62
+ readonly hint: string;
59
63
 
60
- constructor(args: { providerId: string; message: string; hint: string }) {
61
- super(args.message);
62
- this.providerId = args.providerId;
63
- this.hint = args.hint;
64
- }
64
+ constructor(args: { providerId: string; message: string; hint: string }) {
65
+ super(args.message);
66
+ this.providerId = args.providerId;
67
+ this.hint = args.hint;
65
68
  }
69
+ }
66
70
 
67
- export type ModelOptions = {
68
- /** Additional provider options */
69
- providerOptions?: Partial<ProviderOptions>;
70
- /** Skip authentication check (useful for providers with wellknown auth) */
71
- skipAuth?: boolean;
72
- /** Allow missing auth for providers that can be used without credentials */
73
- allowMissingAuth?: boolean;
74
- };
75
-
76
- /**
77
- * Create an AI SDK model with authentication
78
- *
79
- * @param providerId - The provider ID (e.g., 'anthropic', 'openai')
80
- * @param modelId - The model ID (e.g., 'claude-sonnet-4-20250514', 'gpt-4o')
81
- * @param options - Additional options
82
- * @returns The AI SDK language model
83
- */
84
- export async function getModel(
85
- providerId: string,
86
- modelId: string,
87
- options: ModelOptions = {}
88
- ): Promise<LanguageModel> {
89
- const normalizedProviderId = normalizeProviderId(providerId);
90
-
91
- // Check if provider is supported
92
- if (!isProviderSupported(normalizedProviderId)) {
93
- throw new ProviderNotFoundError(providerId);
94
- }
71
+ export type ModelOptions = {
72
+ providerOptions?: Partial<ProviderOptions>;
73
+ skipAuth?: boolean;
74
+ allowMissingAuth?: boolean;
75
+ };
76
+
77
+ export const getModel = async (
78
+ providerId: string,
79
+ modelId: string,
80
+ options: ModelOptions = {}
81
+ ): Promise<LanguageModel> => {
82
+ const normalizedProviderId = normalizeProviderId(providerId);
83
+
84
+ if (!isProviderSupported(normalizedProviderId)) {
85
+ throw new ProviderNotFoundError(providerId);
86
+ }
95
87
 
96
- // Get the provider factory
97
- const factory = getProviderFactory(normalizedProviderId);
98
- if (!factory) {
99
- throw new ProviderNotFoundError(providerId);
100
- }
88
+ const factory = getProviderFactory(normalizedProviderId);
89
+ if (!factory) {
90
+ throw new ProviderNotFoundError(providerId);
91
+ }
101
92
 
102
- // Get authentication
103
- let apiKey: string | undefined;
104
- let accountId: string | undefined;
93
+ let apiKey: string | undefined;
94
+ let accountId: string | undefined;
105
95
 
106
- if (!options.skipAuth) {
107
- const status = await Auth.getAuthStatus(normalizedProviderId);
108
- if (status.status === 'missing') {
109
- if (!options.allowMissingAuth) {
110
- throw new ProviderNotAuthenticatedError(providerId);
111
- }
112
- }
113
- if (status.status === 'invalid') {
114
- throw new ProviderAuthTypeError({ providerId, authType: status.authType });
115
- }
116
- if (status.status === 'ok') {
117
- apiKey = status.apiKey;
118
- accountId = status.accountId;
96
+ if (!options.skipAuth) {
97
+ const status = await getAuthStatus(normalizedProviderId);
98
+ if (status.status === 'missing') {
99
+ if (!options.allowMissingAuth) {
100
+ throw new ProviderNotAuthenticatedError(providerId);
119
101
  }
120
102
  }
121
-
122
- // Build provider options
123
- const providerOptions: ProviderOptions = {
124
- ...options.providerOptions,
125
- ...(accountId ? { accountId } : {})
126
- };
127
-
128
- if (apiKey) {
129
- providerOptions.apiKey = apiKey;
103
+ if (status.status === 'invalid') {
104
+ throw new ProviderAuthTypeError({ providerId, authType: status.authType });
130
105
  }
131
-
132
- if (normalizedProviderId === 'openai-compat') {
133
- const baseURL = providerOptions.baseURL?.trim();
134
- const name = providerOptions.name?.trim();
135
- if (!baseURL || !name) {
136
- throw new ProviderOptionsError({
137
- providerId: normalizedProviderId,
138
- message: 'openai-compat requires baseURL and name',
139
- hint: 'Run "btca connect -p openai-compat" to configure baseURL and name.'
140
- });
141
- }
142
- providerOptions.baseURL = baseURL;
143
- providerOptions.name = name;
106
+ if (status.status === 'ok') {
107
+ apiKey = status.apiKey;
108
+ accountId = status.accountId;
144
109
  }
110
+ }
145
111
 
146
- // Create the provider and get the model
147
- const provider = factory(providerOptions);
148
- const model = provider(modelId);
112
+ const providerOptions: ProviderOptions = {
113
+ ...options.providerOptions,
114
+ ...(accountId ? { accountId } : {})
115
+ };
149
116
 
150
- return model as LanguageModel;
117
+ if (apiKey) {
118
+ providerOptions.apiKey = apiKey;
151
119
  }
152
120
 
153
- /**
154
- * Check if a model can be used (provider is supported and authenticated)
155
- */
156
- export async function canUseModel(providerId: string): Promise<boolean> {
157
- const normalizedProviderId = normalizeProviderId(providerId);
158
-
159
- if (!isProviderSupported(normalizedProviderId)) {
160
- return false;
121
+ if (normalizedProviderId === 'openai-compat') {
122
+ const baseURL = providerOptions.baseURL?.trim();
123
+ const name = providerOptions.name?.trim();
124
+ if (!baseURL || !name) {
125
+ throw new ProviderOptionsError({
126
+ providerId: normalizedProviderId,
127
+ message: 'openai-compat requires baseURL and name',
128
+ hint: 'Run "btca connect -p openai-compat" to configure baseURL and name.'
129
+ });
161
130
  }
162
-
163
- return Auth.isAuthenticated(normalizedProviderId);
131
+ providerOptions.baseURL = baseURL;
132
+ providerOptions.name = name;
164
133
  }
165
134
 
166
- /**
167
- * Get all available providers (supported and authenticated)
168
- */
169
- export async function getAvailableProviders(): Promise<string[]> {
170
- const authenticatedProviders = await Auth.getAuthenticatedProviders();
171
- return authenticatedProviders.filter((provider) => isProviderSupported(provider));
135
+ const provider = factory(providerOptions);
136
+ const model = provider(modelId);
137
+ return model as LanguageModel;
138
+ };
139
+
140
+ export const canUseModel = async (providerId: string): Promise<boolean> => {
141
+ const normalizedProviderId = normalizeProviderId(providerId);
142
+
143
+ if (!isProviderSupported(normalizedProviderId)) {
144
+ return false;
172
145
  }
173
- }
146
+
147
+ return isAuthenticated(normalizedProviderId);
148
+ };
149
+
150
+ export const getAvailableProviders = async (): Promise<string[]> => {
151
+ const authenticatedProviders = await getAuthenticatedProviders();
152
+ return authenticatedProviders.filter((provider) => isProviderSupported(provider));
153
+ };
@@ -1,6 +1,6 @@
1
1
  import { createOpenAI } from '@ai-sdk/openai';
2
2
  import * as os from 'node:os';
3
- import { Auth } from './auth.ts';
3
+ import { getCredentials, setCredentials } from './auth.ts';
4
4
 
5
5
  const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
6
6
  const ISSUER = 'https://auth.openai.com';
@@ -165,7 +165,7 @@ export function createOpenAICodex(
165
165
  } = {}
166
166
  ) {
167
167
  const customFetch = (async (requestInput, init) => {
168
- const storedAuth = await Auth.getCredentials('openai');
168
+ const storedAuth = await getCredentials('openai');
169
169
  let accessToken = options.apiKey;
170
170
  let accountId = options.accountId;
171
171
 
@@ -178,7 +178,7 @@ export function createOpenAICodex(
178
178
  const refreshedAccountId = extractAccountId(tokens) ?? accountId;
179
179
  accessToken = tokens.access_token;
180
180
  accountId = refreshedAccountId;
181
- await Auth.setCredentials('openai', {
181
+ await setCredentials('openai', {
182
182
  type: 'oauth',
183
183
  refresh: tokens.refresh_token ?? storedAuth.refresh,
184
184
  access: tokens.access_token,
@@ -1,9 +1,7 @@
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
- import { Metrics } from '../../metrics/index.ts';
4
+ import { metricsInfo, withMetricsSpan } from '../../metrics/index.ts';
7
5
  import { CommonHints } from '../../errors.ts';
8
6
  import { ResourceError, resourceNameToKey } from '../helpers.ts';
9
7
  import { GitResourceSchema } from '../schema.ts';
@@ -26,7 +24,11 @@ const isBranchNotFoundError = (cause: unknown) => {
26
24
  };
27
25
 
28
26
  const cleanupDirectory = async (pathToRemove: string) => {
29
- await Result.tryPromise(() => fs.rm(pathToRemove, { recursive: true, force: true }));
27
+ try {
28
+ await fs.rm(pathToRemove, { recursive: true, force: true });
29
+ } catch {
30
+ return;
31
+ }
30
32
  };
31
33
 
32
34
  const validateGitUrl = (url: string): { success: true } | { success: false; error: string } => {
@@ -50,11 +52,12 @@ const validateSearchPath = (
50
52
  };
51
53
 
52
54
  const directoryExists = async (path: string): Promise<boolean> => {
53
- const result = await Result.tryPromise(() => fs.stat(path));
54
- return result.match({
55
- ok: (stat) => stat.isDirectory(),
56
- err: () => false
57
- });
55
+ try {
56
+ const stat = await fs.stat(path);
57
+ return stat.isDirectory();
58
+ } catch {
59
+ return false;
60
+ }
58
61
  };
59
62
 
60
63
  /**
@@ -172,10 +175,11 @@ const runGitChecked = async (
172
175
  options: { cwd?: string; quiet: boolean },
173
176
  buildError: (result: GitRunResult) => ResourceError
174
177
  ) => {
175
- const result = await Result.tryPromise(() => runGit(args, options));
176
- return result.andThen((runResult) =>
177
- runResult.exitCode === 0 ? Result.ok(runResult) : Result.err(buildError(runResult))
178
- );
178
+ const runResult = await runGit(args, options);
179
+ if (runResult.exitCode !== 0) {
180
+ throw buildError(runResult);
181
+ }
182
+ return runResult;
179
183
  };
180
184
 
181
185
  const runGit = async (
@@ -268,62 +272,50 @@ const gitClone = async (args: {
268
272
  ]
269
273
  : ['clone', '--depth', '1', '-b', args.repoBranch, args.repoUrl, args.localAbsolutePath];
270
274
 
271
- const result = await Result.gen(async function* () {
272
- yield* Result.await(
273
- runGitChecked(cloneArgs, { quiet: args.quiet }, (cloneResult) => {
274
- const errorType = detectGitErrorType(cloneResult.stderr);
275
- const { message, hint } = getGitErrorDetails(errorType, {
276
- operation: 'clone',
277
- branch: args.repoBranch,
278
- url: args.repoUrl
279
- });
275
+ await runGitChecked(cloneArgs, { quiet: args.quiet }, (cloneResult) => {
276
+ const errorType = detectGitErrorType(cloneResult.stderr);
277
+ const { message, hint } = getGitErrorDetails(errorType, {
278
+ operation: 'clone',
279
+ branch: args.repoBranch,
280
+ url: args.repoUrl
281
+ });
280
282
 
281
- return new ResourceError({
282
- message,
283
- hint,
283
+ return new ResourceError({
284
+ message,
285
+ hint,
286
+ cause: new Error(
287
+ `git clone failed with exit code ${cloneResult.exitCode}: ${cloneResult.stderr}`
288
+ )
289
+ });
290
+ });
291
+
292
+ if (needsSparseCheckout) {
293
+ await runGitChecked(
294
+ ['sparse-checkout', 'set', ...args.repoSubPaths],
295
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
296
+ (sparseResult) =>
297
+ new ResourceError({
298
+ message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
299
+ hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
284
300
  cause: new Error(
285
- `git clone failed with exit code ${cloneResult.exitCode}: ${cloneResult.stderr}`
301
+ `git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
286
302
  )
287
- });
288
- })
303
+ })
289
304
  );
290
305
 
291
- if (needsSparseCheckout) {
292
- yield* Result.await(
293
- runGitChecked(
294
- ['sparse-checkout', 'set', ...args.repoSubPaths],
295
- { cwd: args.localAbsolutePath, quiet: args.quiet },
296
- (sparseResult) =>
297
- new ResourceError({
298
- message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
299
- hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
300
- cause: new Error(
301
- `git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
302
- )
303
- })
304
- )
305
- );
306
-
307
- yield* Result.await(
308
- runGitChecked(
309
- ['checkout'],
310
- { cwd: args.localAbsolutePath, quiet: args.quiet },
311
- (checkout) =>
312
- new ResourceError({
313
- message: 'Failed to checkout repository',
314
- hint: CommonHints.CLEAR_CACHE,
315
- cause: new Error(
316
- `git checkout failed with exit code ${checkout.exitCode}: ${checkout.stderr}`
317
- )
318
- })
319
- )
320
- );
321
- }
322
-
323
- return Result.ok(undefined);
324
- });
325
-
326
- if (!Result.isOk(result)) throw result.error;
306
+ await runGitChecked(
307
+ ['checkout'],
308
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
309
+ (checkout) =>
310
+ new ResourceError({
311
+ message: 'Failed to checkout repository',
312
+ hint: CommonHints.CLEAR_CACHE,
313
+ cause: new Error(
314
+ `git checkout failed with exit code ${checkout.exitCode}: ${checkout.stderr}`
315
+ )
316
+ })
317
+ );
318
+ }
327
319
  };
328
320
 
329
321
  const gitUpdate = async (args: {
@@ -332,80 +324,66 @@ const gitUpdate = async (args: {
332
324
  repoSubPaths: readonly string[];
333
325
  quiet: boolean;
334
326
  }) => {
335
- const result = await Result.gen(async function* () {
336
- yield* Result.await(
337
- runGitChecked(
338
- ['fetch', '--depth', '1', 'origin', args.branch],
339
- { cwd: args.localAbsolutePath, quiet: args.quiet },
340
- (fetchResult) => {
341
- const errorType = detectGitErrorType(fetchResult.stderr);
342
- const { message, hint } = getGitErrorDetails(errorType, {
343
- operation: 'fetch',
344
- branch: args.branch
345
- });
346
-
347
- return new ResourceError({
348
- message,
349
- hint,
350
- cause: new Error(
351
- `git fetch failed with exit code ${fetchResult.exitCode}: ${fetchResult.stderr}`
352
- )
353
- });
354
- }
355
- )
356
- );
357
-
358
- yield* Result.await(
359
- runGitChecked(
360
- ['reset', '--hard', `origin/${args.branch}`],
361
- { cwd: args.localAbsolutePath, quiet: args.quiet },
362
- (resetResult) =>
363
- new ResourceError({
364
- message: 'Failed to update local repository',
365
- hint: `${CommonHints.CLEAR_CACHE} This will re-clone the repository from scratch.`,
366
- cause: new Error(
367
- `git reset failed with exit code ${resetResult.exitCode}: ${resetResult.stderr}`
368
- )
369
- })
370
- )
371
- );
327
+ await runGitChecked(
328
+ ['fetch', '--depth', '1', 'origin', args.branch],
329
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
330
+ (fetchResult) => {
331
+ const errorType = detectGitErrorType(fetchResult.stderr);
332
+ const { message, hint } = getGitErrorDetails(errorType, {
333
+ operation: 'fetch',
334
+ branch: args.branch
335
+ });
372
336
 
373
- if (args.repoSubPaths.length > 0) {
374
- yield* Result.await(
375
- runGitChecked(
376
- ['sparse-checkout', 'set', ...args.repoSubPaths],
377
- { cwd: args.localAbsolutePath, quiet: args.quiet },
378
- (sparseResult) =>
379
- new ResourceError({
380
- message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
381
- hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
382
- cause: new Error(
383
- `git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
384
- )
385
- })
386
- )
387
- );
388
-
389
- yield* Result.await(
390
- runGitChecked(
391
- ['checkout'],
392
- { cwd: args.localAbsolutePath, quiet: args.quiet },
393
- (checkoutResult) =>
394
- new ResourceError({
395
- message: 'Failed to checkout repository',
396
- hint: CommonHints.CLEAR_CACHE,
397
- cause: new Error(
398
- `git checkout failed with exit code ${checkoutResult.exitCode}: ${checkoutResult.stderr}`
399
- )
400
- })
337
+ return new ResourceError({
338
+ message,
339
+ hint,
340
+ cause: new Error(
341
+ `git fetch failed with exit code ${fetchResult.exitCode}: ${fetchResult.stderr}`
401
342
  )
402
- );
343
+ });
403
344
  }
345
+ );
404
346
 
405
- return Result.ok(undefined);
406
- });
347
+ await runGitChecked(
348
+ ['reset', '--hard', `origin/${args.branch}`],
349
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
350
+ (resetResult) =>
351
+ new ResourceError({
352
+ message: 'Failed to update local repository',
353
+ hint: `${CommonHints.CLEAR_CACHE} This will re-clone the repository from scratch.`,
354
+ cause: new Error(
355
+ `git reset failed with exit code ${resetResult.exitCode}: ${resetResult.stderr}`
356
+ )
357
+ })
358
+ );
359
+
360
+ if (args.repoSubPaths.length > 0) {
361
+ await runGitChecked(
362
+ ['sparse-checkout', 'set', ...args.repoSubPaths],
363
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
364
+ (sparseResult) =>
365
+ new ResourceError({
366
+ message: `Failed to set sparse-checkout path(s): "${args.repoSubPaths.join(', ')}"`,
367
+ hint: 'Verify the search paths exist in the repository. Check the repository structure to find the correct path.',
368
+ cause: new Error(
369
+ `git sparse-checkout failed with exit code ${sparseResult.exitCode}: ${sparseResult.stderr}`
370
+ )
371
+ })
372
+ );
407
373
 
408
- if (!Result.isOk(result)) throw result.error;
374
+ await runGitChecked(
375
+ ['checkout'],
376
+ { cwd: args.localAbsolutePath, quiet: args.quiet },
377
+ (checkoutResult) =>
378
+ new ResourceError({
379
+ message: 'Failed to checkout repository',
380
+ hint: CommonHints.CLEAR_CACHE,
381
+ cause: new Error(
382
+ `git checkout failed with exit code ${checkoutResult.exitCode}: ${checkoutResult.stderr}`
383
+ )
384
+ })
385
+ );
386
+ }
409
387
  };
410
388
 
411
389
  /**
@@ -459,28 +437,27 @@ const ensureGitResource = async (config: BtcaGitResourceArgs): Promise<string> =
459
437
  : config.resourcesDirectoryPath;
460
438
  const localPath = path.join(basePath, resourceKey);
461
439
 
462
- const mkdirResult = await Result.tryPromise({
463
- try: () => fs.mkdir(basePath, { recursive: true }),
464
- catch: (cause) =>
465
- new ResourceError({
466
- message: 'Failed to create resources directory',
467
- hint: 'Check that you have write permissions to the btca data directory.',
468
- cause
469
- })
470
- });
471
- if (!Result.isOk(mkdirResult)) throw mkdirResult.error;
440
+ try {
441
+ await fs.mkdir(basePath, { recursive: true });
442
+ } catch (cause) {
443
+ throw new ResourceError({
444
+ message: 'Failed to create resources directory',
445
+ hint: 'Check that you have write permissions to the btca data directory.',
446
+ cause
447
+ });
448
+ }
472
449
 
473
450
  if (config.ephemeral) {
474
451
  await cleanupDirectory(localPath);
475
452
  }
476
453
 
477
- return Metrics.span(
454
+ return withMetricsSpan(
478
455
  'resource.git.ensure',
479
456
  async () => {
480
457
  const exists = await directoryExists(localPath);
481
458
 
482
459
  if (exists && !config.ephemeral) {
483
- Metrics.info('resource.git.update', {
460
+ metricsInfo('resource.git.update', {
484
461
  name: config.name,
485
462
  branch: config.branch,
486
463
  repoSubPaths: config.repoSubPaths
@@ -497,7 +474,7 @@ const ensureGitResource = async (config: BtcaGitResourceArgs): Promise<string> =
497
474
  return localPath;
498
475
  }
499
476
 
500
- Metrics.info('resource.git.clone', {
477
+ metricsInfo('resource.git.clone', {
501
478
  name: config.name,
502
479
  branch: config.ephemeral ? 'fallback' : config.branch,
503
480
  repoSubPaths: config.repoSubPaths