@fitlab-ai/agent-infra 0.5.10 → 0.6.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 (82) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/{cli.js → cli.ts} +21 -17
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +48 -18
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/{create.js → create.ts} +224 -118
  54. package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
  56. package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
  57. package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
  60. package/lib/sandbox/{config.js → config.ts} +55 -10
  61. package/lib/sandbox/{constants.js → constants.ts} +30 -18
  62. package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
  65. package/lib/sandbox/{engine.js → engine.ts} +57 -25
  66. package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
  67. package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/{native.js → native.ts} +25 -6
  70. package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
  71. package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
  72. package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
  73. package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
  74. package/lib/sandbox/{index.js → index.ts} +8 -8
  75. package/lib/sandbox/{shell.js → shell.ts} +30 -17
  76. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  77. package/lib/sandbox/{tools.js → tools.ts} +30 -26
  78. package/lib/{update.js → update.ts} +33 -10
  79. package/package.json +17 -9
  80. package/lib/paths.js +0 -9
  81. package/lib/sandbox/engines/index.js +0 -27
  82. /package/lib/{version.js → version.ts} +0 -0
@@ -2,7 +2,103 @@ import { execFileSync } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { hostJoin } from './engines/wsl2-paths.js';
5
+ import { hostJoin } from './engines/wsl2-paths.ts';
6
+
7
+ type ExecFn = (file: string, args: string[], options?: Record<string, unknown>) => string | Buffer | void;
8
+ type ReadFn = (targetPath: string) => string;
9
+ type ExistsFn = (targetPath: string) => boolean;
10
+ type EnvFn = () => NodeJS.ProcessEnv;
11
+
12
+ type SecurityClassification = 'OK' | 'LOCKED' | 'NOT_FOUND' | 'OTHER';
13
+
14
+ type SecurityResult =
15
+ | { ok: true; stdout: string; stderr: ''; classification: 'OK' }
16
+ | { ok: false; stdout: ''; stderr: string; classification: Exclude<SecurityClassification, 'OK'> };
17
+
18
+ type CredentialStatus = 'OK'
19
+ | 'MISSING'
20
+ | 'STALE_ACCESS'
21
+ | 'KEYCHAIN_LOCKED'
22
+ | 'KEYCHAIN_ERROR'
23
+ | 'KEYCHAIN_WRITE_FAILED';
24
+
25
+ type CredentialInspection =
26
+ | { status: 'OK'; blob: string; expiresAt: unknown }
27
+ | { status: Exclude<CredentialStatus, 'OK' | 'KEYCHAIN_WRITE_FAILED'>; blob?: undefined; expiresAt?: unknown; detail?: string };
28
+
29
+ type CredentialEndpoint = CredentialInspection & {
30
+ name: string;
31
+ project?: string;
32
+ };
33
+
34
+ type OkCredentialEndpoint = CredentialEndpoint & {
35
+ status: 'OK';
36
+ blob: string;
37
+ };
38
+ type ExpiringOkCredentialEndpoint = OkCredentialEndpoint & {
39
+ expiresAt: number;
40
+ };
41
+
42
+ type Warning = {
43
+ source?: string;
44
+ classification?: string;
45
+ message?: string;
46
+ };
47
+
48
+ type WriteResult =
49
+ | { ok: true; classification?: undefined; error?: undefined }
50
+ | { ok: false; classification: Exclude<SecurityClassification, 'OK'>; error: string };
51
+
52
+ type InspectOptions = {
53
+ readFn?: ReadFn;
54
+ existsFn?: ExistsFn;
55
+ envFn?: EnvFn;
56
+ };
57
+
58
+ type WriteHostOptions = {
59
+ execFn?: ExecFn;
60
+ mkdirFn?: typeof fs.mkdirSync;
61
+ chmodFn?: typeof fs.chmodSync;
62
+ writeFileFn?: typeof fs.writeFileSync;
63
+ renameFn?: typeof fs.renameSync;
64
+ rmFn?: typeof fs.rmSync;
65
+ randomFn?: () => string;
66
+ envFn?: EnvFn;
67
+ };
68
+
69
+ type ReconcileOptions = InspectOptions & {
70
+ execFn?: ExecFn;
71
+ writeFn?: (home: string, project: string, blob: string) => void;
72
+ writeHostFn?: (home: string, blob: string, options?: WriteHostOptions) => WriteResult;
73
+ discoverFn?: (home: string) => string[];
74
+ projects?: string[] | null;
75
+ singleProject?: string | null;
76
+ inspection?: CredentialInspection | null;
77
+ };
78
+ type ReconcileResult = {
79
+ status: CredentialStatus;
80
+ authoritative: string | null;
81
+ expiresAt: unknown;
82
+ hostWritten: boolean;
83
+ filesWritten: string[];
84
+ fileErrors: Array<{ project: string; error: string }>;
85
+ warnings: Warning[];
86
+ detail?: string | null;
87
+ };
88
+
89
+ type ResolvedTool = {
90
+ tool: {
91
+ id: string;
92
+ };
93
+ };
94
+
95
+ type CredentialPayload = {
96
+ claudeAiOauth?: CredentialPayload;
97
+ scopes?: unknown;
98
+ accessToken?: unknown;
99
+ refreshToken?: unknown;
100
+ expiresAt?: unknown;
101
+ };
6
102
 
7
103
  const LOCKED_PATTERN = /errSecInteractionNotAllowed|User interaction is not allowed/i;
8
104
  const NOT_FOUND_PATTERN = /errSecItemNotFound|specified item could not be found/i;
@@ -13,7 +109,11 @@ const REDACTION_PATTERNS = [
13
109
  { pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, replacement: 'Bearer [REDACTED]' }
14
110
  ];
15
111
 
16
- export function redactCommandError(text) {
112
+ function errorMessage(error: unknown): string {
113
+ return error instanceof Error ? error.message : 'unknown error';
114
+ }
115
+
116
+ export function redactCommandError(text: unknown): string {
17
117
  if (!text || typeof text !== 'string') {
18
118
  return '';
19
119
  }
@@ -26,8 +126,10 @@ export function redactCommandError(text) {
26
126
 
27
127
  export const redactSecurityOutput = redactCommandError;
28
128
 
29
- function extractStderrSafely(error) {
30
- const stderr = error?.stderr;
129
+ function extractStderrSafely(error: unknown): string {
130
+ const stderr = typeof error === 'object' && error !== null && 'stderr' in error
131
+ ? error.stderr
132
+ : undefined;
31
133
  if (Buffer.isBuffer(stderr)) {
32
134
  return stderr.toString('utf8');
33
135
  }
@@ -37,7 +139,7 @@ function extractStderrSafely(error) {
37
139
  return '';
38
140
  }
39
141
 
40
- function classifySecurityFailure(text) {
142
+ function classifySecurityFailure(text: string): Exclude<SecurityClassification, 'OK'> {
41
143
  if (LOCKED_PATTERN.test(text)) {
42
144
  return 'LOCKED';
43
145
  }
@@ -47,14 +149,16 @@ function classifySecurityFailure(text) {
47
149
  return 'OTHER';
48
150
  }
49
151
 
50
- function runSecurity(args, options = {}, execFn = execFileSync) {
152
+ function runSecurity(args: string[], options: Record<string, unknown> = {}, execFn: ExecFn = execFileSync): SecurityResult {
51
153
  try {
52
154
  const stdout = execFn('security', args, {
53
155
  encoding: 'utf8',
54
156
  stdio: ['ignore', 'pipe', 'pipe'],
55
157
  ...options
56
158
  });
57
- const text = typeof stdout === 'string' ? stdout : (stdout?.toString?.('utf8') ?? '');
159
+ const text = typeof stdout === 'string'
160
+ ? stdout
161
+ : (Buffer.isBuffer(stdout) ? stdout.toString('utf8') : '');
58
162
  return { ok: true, stdout: text, stderr: '', classification: 'OK' };
59
163
  } catch (error) {
60
164
  const stderr = redactCommandError(extractStderrSafely(error));
@@ -67,7 +171,7 @@ function runSecurity(args, options = {}, execFn = execFileSync) {
67
171
  }
68
172
  }
69
173
 
70
- export function buildLockedGuidance() {
174
+ export function buildLockedGuidance(): string {
71
175
  return [
72
176
  'macOS keychain is locked (errSecInteractionNotAllowed).',
73
177
  'Options to recover:',
@@ -90,7 +194,7 @@ export function buildLockedGuidance() {
90
194
  ].join('\n');
91
195
  }
92
196
 
93
- export function claudeCredentialsEnvOverride(env = process.env) {
197
+ export function claudeCredentialsEnvOverride(env: NodeJS.ProcessEnv = process.env): { path: string; source: string } | null {
94
198
  const raw = env?.AGENT_INFRA_CLAUDE_CREDENTIALS_FILE;
95
199
  if (!raw || typeof raw !== 'string') {
96
200
  return null;
@@ -98,7 +202,7 @@ export function claudeCredentialsEnvOverride(env = process.env) {
98
202
  return { path: raw, source: 'AGENT_INFRA_CLAUDE_CREDENTIALS_FILE' };
99
203
  }
100
204
 
101
- export function validateClaudeCredentialsEnvOverride(env = process.env) {
205
+ export function validateClaudeCredentialsEnvOverride(env: NodeJS.ProcessEnv = process.env): void {
102
206
  const raw = env?.AGENT_INFRA_CLAUDE_CREDENTIALS_FILE;
103
207
  if (raw === undefined || raw === '') {
104
208
  return;
@@ -112,15 +216,15 @@ export function validateClaudeCredentialsEnvOverride(env = process.env) {
112
216
 
113
217
  // Reconcile treats the freshest valid endpoint as authoritative so sandbox
114
218
  // token rotations can flow back to the host credential store.
115
- function validateClaudeCredentialsBlob(raw, blob = null) {
219
+ function validateClaudeCredentialsBlob(raw: unknown, blob: string | null = null): CredentialInspection {
116
220
  const trimmed = typeof raw === 'string' ? raw.trim() : '';
117
221
  if (!trimmed) {
118
222
  return { status: 'MISSING' };
119
223
  }
120
224
 
121
- let parsed;
225
+ let parsed: CredentialPayload;
122
226
  try {
123
- parsed = JSON.parse(trimmed);
227
+ parsed = JSON.parse(trimmed) as CredentialPayload;
124
228
  } catch {
125
229
  return { status: 'STALE_ACCESS' };
126
230
  }
@@ -140,7 +244,11 @@ function validateClaudeCredentialsBlob(raw, blob = null) {
140
244
  };
141
245
  }
142
246
 
143
- export function readClaudeCredentialsFile(filePath, readFn = (targetPath) => fs.readFileSync(targetPath, 'utf8'), existsFn = fs.existsSync) {
247
+ export function readClaudeCredentialsFile(
248
+ filePath: string,
249
+ readFn: ReadFn = (targetPath) => fs.readFileSync(targetPath, 'utf8'),
250
+ existsFn: ExistsFn = fs.existsSync
251
+ ): CredentialInspection {
144
252
  if (!existsFn(filePath)) {
145
253
  return { status: 'MISSING' };
146
254
  }
@@ -153,7 +261,11 @@ export function readClaudeCredentialsFile(filePath, readFn = (targetPath) => fs.
153
261
  }
154
262
  }
155
263
 
156
- export function inspectClaudeKeychainStatus(home, execFn = execFileSync, options = {}) {
264
+ export function inspectClaudeKeychainStatus(
265
+ home: string,
266
+ execFn: ExecFn = execFileSync,
267
+ options: InspectOptions = {}
268
+ ): CredentialInspection {
157
269
  const {
158
270
  readFn,
159
271
  existsFn,
@@ -191,7 +303,7 @@ export function inspectClaudeKeychainStatus(home, execFn = execFileSync, options
191
303
  return readClaudeCredentialsFile(credentialsPath, readFn, existsFn);
192
304
  }
193
305
 
194
- export function inspectClaudeMountFile(home, project, options = {}) {
306
+ export function inspectClaudeMountFile(home: string, project: string, options: InspectOptions = {}): CredentialInspection {
195
307
  return readClaudeCredentialsFile(
196
308
  claudeCredentialsPath(home, project),
197
309
  options.readFn,
@@ -199,20 +311,20 @@ export function inspectClaudeMountFile(home, project, options = {}) {
199
311
  );
200
312
  }
201
313
 
202
- export function extractClaudeCredentialsBlob(home, execFn = execFileSync) {
314
+ export function extractClaudeCredentialsBlob(home: string, execFn: ExecFn = execFileSync): string | null {
203
315
  const inspection = inspectClaudeKeychainStatus(home, execFn);
204
316
  return inspection.status === 'OK' ? inspection.blob : null;
205
317
  }
206
318
 
207
- export function claudeCredentialsDir(home, project) {
319
+ export function claudeCredentialsDir(home: string, project: string): string {
208
320
  return hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code');
209
321
  }
210
322
 
211
- export function claudeCredentialsPath(home, project) {
323
+ export function claudeCredentialsPath(home: string, project: string): string {
212
324
  return hostJoin(claudeCredentialsDir(home, project), '.credentials.json');
213
325
  }
214
326
 
215
- export function writeClaudeCredentialsFile(home, project, blob) {
327
+ export function writeClaudeCredentialsFile(home: string, project: string, blob: string): void {
216
328
  const dir = claudeCredentialsDir(home, project);
217
329
  const filePath = claudeCredentialsPath(home, project);
218
330
 
@@ -222,7 +334,7 @@ export function writeClaudeCredentialsFile(home, project, blob) {
222
334
  fs.chmodSync(filePath, 0o600);
223
335
  }
224
336
 
225
- export function discoverProjects(home) {
337
+ export function discoverProjects(home: string): string[] {
226
338
  const credentialsRoot = hostJoin(home, '.agent-infra', 'credentials');
227
339
  if (!fs.existsSync(credentialsRoot)) {
228
340
  return [];
@@ -234,7 +346,7 @@ export function discoverProjects(home) {
234
346
  .filter((project) => fs.existsSync(claudeCredentialsPath(home, project)));
235
347
  }
236
348
 
237
- export function writeClaudeCredentialsToHost(home, blob, options = {}) {
349
+ export function writeClaudeCredentialsToHost(home: string, blob: string, options: WriteHostOptions = {}): WriteResult {
238
350
  const {
239
351
  execFn = execFileSync,
240
352
  mkdirFn = fs.mkdirSync,
@@ -293,24 +405,24 @@ export function writeClaudeCredentialsToHost(home, blob, options = {}) {
293
405
  return {
294
406
  ok: false,
295
407
  classification: 'OTHER',
296
- error: redactCommandError(error?.message ?? 'unknown error')
408
+ error: redactCommandError(errorMessage(error))
297
409
  };
298
410
  }
299
411
  }
300
412
 
301
- export function formatCredentialWarnings(warnings = []) {
413
+ export function formatCredentialWarnings(warnings: Array<string | Warning> = []): string {
302
414
  return warnings
303
415
  .map((warning) => (typeof warning === 'string' ? warning : warning?.message))
304
416
  .filter(Boolean)
305
417
  .join('; ');
306
418
  }
307
419
 
308
- function endpointNameForFile(project) {
420
+ function endpointNameForFile(project: string): string {
309
421
  return `file:${project}`;
310
422
  }
311
423
 
312
- function chooseAuthoritativeEndpoint(endpoints) {
313
- const okEndpoints = endpoints.filter((endpoint) => endpoint.status === 'OK');
424
+ function chooseAuthoritativeEndpoint(endpoints: CredentialEndpoint[]): OkCredentialEndpoint | null {
425
+ const okEndpoints = endpoints.filter((endpoint): endpoint is OkCredentialEndpoint => endpoint.status === 'OK');
314
426
  if (okEndpoints.length === 0) {
315
427
  return null;
316
428
  }
@@ -320,17 +432,19 @@ function chooseAuthoritativeEndpoint(endpoints) {
320
432
  return hostEndpoint;
321
433
  }
322
434
 
323
- const withExpiresAt = okEndpoints.filter((endpoint) => typeof endpoint.expiresAt === 'number');
435
+ const withExpiresAt = okEndpoints.filter(
436
+ (endpoint): endpoint is ExpiringOkCredentialEndpoint => typeof endpoint.expiresAt === 'number'
437
+ );
324
438
  if (withExpiresAt.length > 0) {
325
439
  return withExpiresAt.reduce((best, endpoint) => (
326
440
  endpoint.expiresAt > best.expiresAt ? endpoint : best
327
441
  ));
328
442
  }
329
443
 
330
- return okEndpoints.find((endpoint) => endpoint.name === 'host') ?? okEndpoints[0];
444
+ return okEndpoints.find((endpoint) => endpoint.name === 'host') ?? okEndpoints[0] ?? null;
331
445
  }
332
446
 
333
- function shouldWriteEndpoint(authoritative, target) {
447
+ function shouldWriteEndpoint(authoritative: OkCredentialEndpoint, target: CredentialEndpoint): boolean {
334
448
  if (target.status !== 'OK') {
335
449
  return true;
336
450
  }
@@ -347,7 +461,7 @@ function shouldWriteEndpoint(authoritative, target) {
347
461
  return false;
348
462
  }
349
463
 
350
- export function reconcileClaudeCredentials(home, options = {}) {
464
+ export function reconcileClaudeCredentials(home: string, options: ReconcileOptions = {}): ReconcileResult {
351
465
  const {
352
466
  execFn = execFileSync,
353
467
  writeFn = writeClaudeCredentialsFile,
@@ -387,13 +501,13 @@ export function reconcileClaudeCredentials(home, options = {}) {
387
501
  filesWritten: [],
388
502
  fileErrors: [],
389
503
  warnings: [],
390
- detail: hostEndpoint.detail ?? null
504
+ detail: 'detail' in hostEndpoint ? hostEndpoint.detail ?? null : null
391
505
  };
392
506
  }
393
507
 
394
- const warnings = [];
395
- const filesWritten = [];
396
- const fileErrors = [];
508
+ const warnings: Warning[] = [];
509
+ const filesWritten: string[] = [];
510
+ const fileErrors: Array<{ project: string; error: string }> = [];
397
511
  let hostWritten = false;
398
512
  let hostWriteFailed = false;
399
513
 
@@ -420,7 +534,7 @@ export function reconcileClaudeCredentials(home, options = {}) {
420
534
  writeFn(home, endpoint.project, authoritative.blob);
421
535
  filesWritten.push(endpoint.project);
422
536
  } catch (error) {
423
- fileErrors.push({ project: endpoint.project, error: error.message });
537
+ fileErrors.push({ project: endpoint.project, error: errorMessage(error) });
424
538
  }
425
539
  }
426
540
 
@@ -435,7 +549,7 @@ export function reconcileClaudeCredentials(home, options = {}) {
435
549
  };
436
550
  }
437
551
 
438
- export function syncClaudeCredentialsFromKeychain(home, project, options = {}) {
552
+ export function syncClaudeCredentialsFromKeychain(home: string, project: string, options: ReconcileOptions = {}) {
439
553
  const result = reconcileClaudeCredentials(home, {
440
554
  ...options,
441
555
  singleProject: project
@@ -454,7 +568,7 @@ export function syncClaudeCredentialsFromKeychain(home, project, options = {}) {
454
568
  };
455
569
  }
456
570
 
457
- export function formatRemaining(expiresAt) {
571
+ export function formatRemaining(expiresAt: unknown): string {
458
572
  if (typeof expiresAt !== 'number') {
459
573
  return 'unknown';
460
574
  }
@@ -471,19 +585,19 @@ export function formatRemaining(expiresAt) {
471
585
  }
472
586
 
473
587
  export function assertClaudeCredentialsAvailable(
474
- home,
475
- project,
476
- resolvedTools,
477
- extractFn = extractClaudeCredentialsBlob,
478
- writeFn = writeClaudeCredentialsFile,
479
- inspectFn = inspectClaudeKeychainStatus
480
- ) {
588
+ home: string,
589
+ project: string,
590
+ resolvedTools: ResolvedTool[],
591
+ extractFn: (home: string) => string | null = extractClaudeCredentialsBlob,
592
+ writeFn: (home: string, project: string, blob: string) => void = writeClaudeCredentialsFile,
593
+ inspectFn: (home: string) => CredentialInspection = inspectClaudeKeychainStatus
594
+ ): void {
481
595
  const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
482
596
  if (!claudeCodeEntry) {
483
597
  return;
484
598
  }
485
599
 
486
- let blob = null;
600
+ let blob: string | null = null;
487
601
  const hasCustomInspectFn = inspectFn !== inspectClaudeKeychainStatus;
488
602
  const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
489
603
  if (hasCustomInspectFn || !hasCustomExtractFn) {
@@ -9,19 +9,26 @@ const RUNTIMES_DIR = path.join(
9
9
  'runtimes'
10
10
  );
11
11
 
12
- function listRuntimeFragments() {
12
+ type DockerfileConfig = {
13
+ repoRoot: string;
14
+ project: string;
15
+ dockerfile: string | null;
16
+ runtimes: string[];
17
+ };
18
+
19
+ function listRuntimeFragments(): string[] {
13
20
  return fs.readdirSync(RUNTIMES_DIR)
14
21
  .filter((file) => file.endsWith('.dockerfile'))
15
22
  .map((file) => file.replace(/\.dockerfile$/, ''));
16
23
  }
17
24
 
18
- export function availableRuntimes() {
25
+ export function availableRuntimes(): string[] {
19
26
  return listRuntimeFragments()
20
27
  .filter((name) => name !== 'base' && name !== 'ai-tools')
21
28
  .sort();
22
29
  }
23
30
 
24
- function dockerfileContent(config) {
31
+ function dockerfileContent(config: DockerfileConfig): string {
25
32
  if (config.dockerfile) {
26
33
  const customPath = path.resolve(config.repoRoot, config.dockerfile);
27
34
  if (!fs.existsSync(customPath)) {
@@ -52,14 +59,14 @@ function dockerfileContent(config) {
52
59
  return `${content}\n`;
53
60
  }
54
61
 
55
- export function dockerfileSignature(config) {
62
+ export function dockerfileSignature(config: DockerfileConfig): string {
56
63
  return createHash('sha256')
57
64
  .update(dockerfileContent(config))
58
65
  .digest('hex')
59
66
  .slice(0, 12);
60
67
  }
61
68
 
62
- export function prepareDockerfile(config) {
69
+ export function prepareDockerfile(config: DockerfileConfig): { path: string; signature: string; cleanup: () => void } {
63
70
  if (config.dockerfile) {
64
71
  const customPath = path.resolve(config.repoRoot, config.dockerfile);
65
72
  if (!fs.existsSync(customPath)) {
@@ -86,7 +93,7 @@ export function prepareDockerfile(config) {
86
93
  };
87
94
  }
88
95
 
89
- export function composeDockerfile(config) {
96
+ export function composeDockerfile(config: DockerfileConfig): string {
90
97
  const content = dockerfileContent(config);
91
98
 
92
99
  const tempPath = path.join(os.tmpdir(), `${config.project}-sandbox.Dockerfile`);
@@ -1,13 +1,45 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { hostJoin } from './engines/wsl2-paths.js';
4
-
5
- export function dotfilesCacheDir(home, project) {
3
+ import { hostJoin } from './engines/wsl2-paths.ts';
4
+
5
+ type DotfilesWarning = {
6
+ rel: string;
7
+ reason: string;
8
+ detail?: string;
9
+ };
10
+
11
+ type DotfilesFs = Pick<typeof fs, 'copyFileSync' | 'existsSync' | 'mkdirSync' | 'readdirSync' | 'realpathSync' | 'rmSync' | 'statSync'>;
12
+
13
+ type MaterializeOptions = {
14
+ writeStderr?: (message: string) => void;
15
+ maxDepth?: number;
16
+ fsModule?: DotfilesFs;
17
+ };
18
+
19
+ type WalkContext = {
20
+ srcDir: string;
21
+ dstDir: string;
22
+ relParts: string[];
23
+ depth: number;
24
+ maxDepth: number;
25
+ activeDirs: Set<string>;
26
+ warnings: DotfilesWarning[];
27
+ writeStderr: (message: string) => void;
28
+ fsModule: DotfilesFs;
29
+ };
30
+
31
+ export function dotfilesCacheDir(home: string, project: string): string {
6
32
  return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
7
33
  }
8
34
 
9
- function dotfilesWarning(warnings, writeStderr, relPath, reason, detail = '') {
10
- const warning = { rel: relPath, reason };
35
+ function dotfilesWarning(
36
+ warnings: DotfilesWarning[],
37
+ writeStderr: (message: string) => void,
38
+ relPath: string,
39
+ reason: string,
40
+ detail = ''
41
+ ): void {
42
+ const warning: DotfilesWarning = { rel: relPath, reason };
11
43
  if (detail) {
12
44
  warning.detail = detail;
13
45
  }
@@ -17,17 +49,31 @@ function dotfilesWarning(warnings, writeStderr, relPath, reason, detail = '') {
17
49
  writeStderr(`sandbox-dotfiles (host): skipping ${relPath} (${reason}${suffix})\n`);
18
50
  }
19
51
 
20
- function copyDotfile(srcPath, dstPath, context) {
52
+ function errorDetail(error: unknown): string {
53
+ return error instanceof Error ? error.message : 'unknown error';
54
+ }
55
+
56
+ function errorCodeOrDetail(error: unknown): string {
57
+ return typeof error === 'object' && error !== null && 'code' in error
58
+ ? String(error.code)
59
+ : errorDetail(error);
60
+ }
61
+
62
+ function copyDotfile(
63
+ srcPath: string,
64
+ dstPath: string,
65
+ context: Pick<WalkContext, 'fsModule' | 'warnings' | 'writeStderr'> & { relPath: string }
66
+ ): void {
21
67
  const { fsModule, relPath, warnings, writeStderr } = context;
22
68
  try {
23
69
  fsModule.mkdirSync(path.dirname(dstPath), { recursive: true });
24
70
  fsModule.copyFileSync(srcPath, dstPath);
25
71
  } catch (error) {
26
- dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', error?.code ?? error?.message ?? 'unknown error');
72
+ dotfilesWarning(warnings, writeStderr, relPath, 'copy failed', errorCodeOrDetail(error));
27
73
  }
28
74
  }
29
75
 
30
- function walkAndMaterializeDotfiles(context) {
76
+ function walkAndMaterializeDotfiles(context: WalkContext): void {
31
77
  const {
32
78
  srcDir,
33
79
  dstDir,
@@ -46,11 +92,11 @@ function walkAndMaterializeDotfiles(context) {
46
92
  return;
47
93
  }
48
94
 
49
- let entries;
95
+ let entries: fs.Dirent[];
50
96
  try {
51
97
  entries = fsModule.readdirSync(srcDir, { withFileTypes: true });
52
98
  } catch (error) {
53
- dotfilesWarning(warnings, writeStderr, relPath, 'read failed', error?.code ?? error?.message ?? 'unknown error');
99
+ dotfilesWarning(warnings, writeStderr, relPath, 'read failed', errorCodeOrDetail(error));
54
100
  return;
55
101
  }
56
102
 
@@ -61,20 +107,21 @@ function walkAndMaterializeDotfiles(context) {
61
107
  const childRelPath = childRelParts.join('/');
62
108
 
63
109
  if (entry.isSymbolicLink()) {
64
- let resolvedTarget;
110
+ let resolvedTarget: string;
65
111
  try {
66
112
  resolvedTarget = fsModule.realpathSync(childSrc);
67
113
  } catch (error) {
68
- const reason = error?.code === 'ELOOP' ? 'symlink loop' : 'dangling symlink';
69
- dotfilesWarning(warnings, writeStderr, childRelPath, reason, error?.code ?? 'unresolved');
114
+ const code = errorCodeOrDetail(error);
115
+ const reason = code === 'ELOOP' ? 'symlink loop' : 'dangling symlink';
116
+ dotfilesWarning(warnings, writeStderr, childRelPath, reason, code || 'unresolved');
70
117
  continue;
71
118
  }
72
119
 
73
- let targetStat;
120
+ let targetStat: fs.Stats;
74
121
  try {
75
122
  targetStat = fsModule.statSync(resolvedTarget);
76
123
  } catch (error) {
77
- dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', error?.code ?? error?.message ?? 'unknown error');
124
+ dotfilesWarning(warnings, writeStderr, childRelPath, 'target stat failed', errorCodeOrDetail(error));
78
125
  continue;
79
126
  }
80
127
 
@@ -112,7 +159,7 @@ function walkAndMaterializeDotfiles(context) {
112
159
  }
113
160
 
114
161
  if (entry.isDirectory()) {
115
- let childRealPath = null;
162
+ let childRealPath: string | null = null;
116
163
  try {
117
164
  childRealPath = fsModule.realpathSync(childSrc);
118
165
  } catch {
@@ -149,7 +196,7 @@ function walkAndMaterializeDotfiles(context) {
149
196
  }
150
197
  }
151
198
 
152
- export function materializeDotfiles(srcDir, cacheDir, options = {}) {
199
+ export function materializeDotfiles(srcDir: string, cacheDir: string, options: MaterializeOptions = {}) {
153
200
  const {
154
201
  writeStderr = (message) => process.stderr.write(message),
155
202
  maxDepth = 32,
@@ -165,8 +212,8 @@ export function materializeDotfiles(srcDir, cacheDir, options = {}) {
165
212
  fsModule.rmSync(path.join(cacheDir, entry), { recursive: true, force: true });
166
213
  }
167
214
 
168
- const warnings = [];
169
- const activeDirs = new Set();
215
+ const warnings: DotfilesWarning[] = [];
216
+ const activeDirs = new Set<string>();
170
217
  try {
171
218
  activeDirs.add(fsModule.realpathSync(srcDir));
172
219
  } catch {