@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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/{cli.js → cli.ts} +21 -17
- package/dist/bin/cli.js +116 -0
- package/dist/lib/defaults.json +61 -0
- package/dist/lib/init.js +238 -0
- package/dist/lib/log.js +18 -0
- package/dist/lib/merge.js +747 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/prompt.js +85 -0
- package/dist/lib/render.js +139 -0
- package/dist/lib/sandbox/commands/create.js +1173 -0
- package/dist/lib/sandbox/commands/enter.js +98 -0
- package/dist/lib/sandbox/commands/ls.js +93 -0
- package/dist/lib/sandbox/commands/rebuild.js +101 -0
- package/dist/lib/sandbox/commands/refresh.js +85 -0
- package/dist/lib/sandbox/commands/rm.js +226 -0
- package/dist/lib/sandbox/commands/vm.js +144 -0
- package/dist/lib/sandbox/config.js +85 -0
- package/dist/lib/sandbox/constants.js +104 -0
- package/dist/lib/sandbox/credentials.js +437 -0
- package/dist/lib/sandbox/dockerfile.js +76 -0
- package/dist/lib/sandbox/dotfiles.js +170 -0
- package/dist/lib/sandbox/engine.js +155 -0
- package/dist/lib/sandbox/engines/colima.js +64 -0
- package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
- package/dist/lib/sandbox/engines/index.js +25 -0
- package/dist/lib/sandbox/engines/native.js +96 -0
- package/dist/lib/sandbox/engines/orbstack.js +63 -0
- package/dist/lib/sandbox/engines/selinux.js +48 -0
- package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
- package/dist/lib/sandbox/engines/wsl2.js +57 -0
- package/dist/lib/sandbox/index.js +70 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
- package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +148 -0
- package/dist/lib/sandbox/task-resolver.js +35 -0
- package/dist/lib/sandbox/tools.js +115 -0
- package/dist/lib/update.js +186 -0
- package/dist/lib/version.js +5 -0
- package/dist/package.json +5 -0
- package/lib/{init.js → init.ts} +48 -18
- package/lib/{log.js → log.ts} +4 -4
- package/lib/{merge.js → merge.ts} +129 -63
- package/lib/paths.ts +18 -0
- package/lib/{prompt.js → prompt.ts} +12 -12
- package/lib/{render.js → render.ts} +30 -17
- package/lib/sandbox/commands/{create.js → create.ts} +224 -118
- package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
- package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
- package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
- package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
- package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
- package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
- package/lib/sandbox/{config.js → config.ts} +55 -10
- package/lib/sandbox/{constants.js → constants.ts} +30 -18
- package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
- package/lib/sandbox/{engine.js → engine.ts} +57 -25
- package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
- package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/{native.js → native.ts} +25 -6
- package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
- package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
- package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
- package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
- package/lib/sandbox/{index.js → index.ts} +8 -8
- package/lib/sandbox/{shell.js → shell.ts} +30 -17
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +30 -26
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +17 -9
- package/lib/paths.js +0 -9
- package/lib/sandbox/engines/index.js +0 -27
- /package/lib/{version.js → version.ts} +0 -0
|
@@ -3,11 +3,12 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
|
|
6
7
|
import { parseArgs } from 'node:util';
|
|
7
8
|
import * as p from '@clack/prompts';
|
|
8
9
|
import pc from 'picocolors';
|
|
9
10
|
import * as toml from 'smol-toml';
|
|
10
|
-
import { loadConfig } from '../config.
|
|
11
|
+
import { loadConfig } from '../config.ts';
|
|
11
12
|
import {
|
|
12
13
|
assertValidBranchName,
|
|
13
14
|
containerName,
|
|
@@ -20,9 +21,9 @@ import {
|
|
|
20
21
|
shareBranchDir,
|
|
21
22
|
shareCommonDir,
|
|
22
23
|
worktreeDirCandidates
|
|
23
|
-
} from '../constants.
|
|
24
|
-
import { prepareDockerfile } from '../dockerfile.
|
|
25
|
-
import { detectEngine, ensureDocker } from '../engine.
|
|
24
|
+
} from '../constants.ts';
|
|
25
|
+
import { prepareDockerfile } from '../dockerfile.ts';
|
|
26
|
+
import { detectEngine, ensureDocker } from '../engine.ts';
|
|
26
27
|
import {
|
|
27
28
|
commandForEngine,
|
|
28
29
|
execEngine,
|
|
@@ -33,18 +34,19 @@ import {
|
|
|
33
34
|
runSafe,
|
|
34
35
|
runSafeEngine,
|
|
35
36
|
runVerboseEngine
|
|
36
|
-
} from '../shell.
|
|
37
|
-
import { resolveTaskBranch } from '../task-resolver.
|
|
38
|
-
import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
37
|
+
} from '../shell.ts';
|
|
38
|
+
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
39
|
+
import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.ts';
|
|
40
|
+
import type { SandboxTool } from '../tools.ts';
|
|
41
|
+
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
42
|
+
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
43
|
+
import { resolveBuildUid } from '../engines/native.ts';
|
|
44
|
+
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
43
45
|
import {
|
|
44
46
|
assertClaudeCredentialsAvailable,
|
|
45
47
|
redactCommandError,
|
|
46
48
|
validateClaudeCredentialsEnvOverride
|
|
47
|
-
} from '../credentials.
|
|
49
|
+
} from '../credentials.ts';
|
|
48
50
|
|
|
49
51
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
50
52
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
@@ -79,7 +81,32 @@ Host aliases:
|
|
|
79
81
|
shell-config directory is bind-mounted at ${CONTAINER_SHELL_CONFIG_MOUNT} and
|
|
80
82
|
symlinked into $HOME).`;
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
type SandboxCreateConfig = ReturnType<typeof loadConfig>;
|
|
85
|
+
type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
|
|
86
|
+
type ResolvedTool = { tool: SandboxTool; dir: string };
|
|
87
|
+
type RuntimeCheck = { name: string; cmd: string[] };
|
|
88
|
+
type JsonObject = Record<string, unknown>;
|
|
89
|
+
type GpgCache = { pub: Buffer; sec: Buffer } | null;
|
|
90
|
+
type ExecSyncOptions = ExecFileSyncOptions & {
|
|
91
|
+
input?: Buffer | string;
|
|
92
|
+
env?: NodeJS.ProcessEnv;
|
|
93
|
+
stdio?: StdioOptions;
|
|
94
|
+
encoding?: BufferEncoding;
|
|
95
|
+
};
|
|
96
|
+
type ExecSyncFn = (cmd: string, args: string[], options?: ExecSyncOptions) => Buffer | string;
|
|
97
|
+
type EngineExecFn = (engine: string, cmd: string, args: string[], opts?: ExecFileSyncOptions) => Buffer | string;
|
|
98
|
+
type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
|
|
99
|
+
type EngineRunSafeFn = EngineRunFn;
|
|
100
|
+
type EngineRunVerboseFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => void;
|
|
101
|
+
type DirectRunFn = (cmd: string, args: string[], opts?: { cwd?: string }) => string;
|
|
102
|
+
type DirectRunSafeFn = DirectRunFn;
|
|
103
|
+
type DirectRunVerboseFn = (cmd: string, args: string[], opts?: { cwd?: string }) => void;
|
|
104
|
+
type HostShellConfig = {
|
|
105
|
+
hostDir: string;
|
|
106
|
+
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
|
|
83
110
|
return createHash('sha256')
|
|
84
111
|
.update(JSON.stringify({
|
|
85
112
|
dockerfile: preparedDockerfile.signature,
|
|
@@ -89,22 +116,22 @@ function buildSignature(preparedDockerfile, tools) {
|
|
|
89
116
|
.slice(0, 12);
|
|
90
117
|
}
|
|
91
118
|
|
|
92
|
-
function resolveToolDirs(config, tools, branch) {
|
|
119
|
+
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
93
120
|
return tools.map((tool) => {
|
|
94
121
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
95
122
|
return {
|
|
96
123
|
tool,
|
|
97
|
-
dir: candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0]
|
|
124
|
+
dir: candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0] ?? ''
|
|
98
125
|
};
|
|
99
126
|
});
|
|
100
127
|
}
|
|
101
128
|
|
|
102
|
-
export function hostShellConfigDir(home, project, branch) {
|
|
129
|
+
export function hostShellConfigDir(home: string, project: string, branch: string): string {
|
|
103
130
|
return hostJoin(home, '.agent-infra', 'config', project, sanitizeBranchName(branch));
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
function runtimeChecks(runtimes) {
|
|
107
|
-
const checks = [];
|
|
133
|
+
function runtimeChecks(runtimes: string[]): RuntimeCheck[] {
|
|
134
|
+
const checks: RuntimeCheck[] = [];
|
|
108
135
|
if (runtimes.some((runtime) => runtime.startsWith('node'))) {
|
|
109
136
|
checks.push({ name: 'Node.js', cmd: ['node', '--version'] });
|
|
110
137
|
}
|
|
@@ -118,25 +145,25 @@ function runtimeChecks(runtimes) {
|
|
|
118
145
|
return checks;
|
|
119
146
|
}
|
|
120
147
|
|
|
121
|
-
export function detectGpgConfig(gitconfig) {
|
|
148
|
+
export function detectGpgConfig(gitconfig: string): boolean {
|
|
122
149
|
return /\bgpgsign\s*=\s*true\b/i.test(gitconfig) || /^\s*\[gpg(?:\s|"|\])/im.test(gitconfig);
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
function appendSafeDirectories(lines, repoRoot) {
|
|
152
|
+
function appendSafeDirectories(lines: string[], repoRoot: string): string[] {
|
|
126
153
|
if (!repoRoot) {
|
|
127
154
|
return lines;
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
const requiredDirectories = ['/workspace', repoRoot];
|
|
131
|
-
const existingDirectories = new Set();
|
|
158
|
+
const existingDirectories = new Set<string>();
|
|
132
159
|
let firstSafeSectionIndex = -1;
|
|
133
160
|
let inSafeSection = false;
|
|
134
161
|
|
|
135
162
|
for (let index = 0; index < lines.length; index += 1) {
|
|
136
|
-
const line = lines[index];
|
|
163
|
+
const line = lines[index] ?? '';
|
|
137
164
|
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
138
165
|
if (sectionMatch) {
|
|
139
|
-
inSafeSection = sectionMatch[1].trim().toLowerCase() === 'safe';
|
|
166
|
+
inSafeSection = (sectionMatch[1] ?? '').trim().toLowerCase() === 'safe';
|
|
140
167
|
if (inSafeSection && firstSafeSectionIndex === -1) {
|
|
141
168
|
firstSafeSectionIndex = index;
|
|
142
169
|
}
|
|
@@ -149,7 +176,7 @@ function appendSafeDirectories(lines, repoRoot) {
|
|
|
149
176
|
|
|
150
177
|
const directoryMatch = line.match(/^\s*directory\s*=\s*(.+?)\s*$/i);
|
|
151
178
|
if (directoryMatch) {
|
|
152
|
-
existingDirectories.add(directoryMatch[1].trim());
|
|
179
|
+
existingDirectories.add((directoryMatch[1] ?? '').trim());
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -170,7 +197,7 @@ function appendSafeDirectories(lines, repoRoot) {
|
|
|
170
197
|
const updatedLines = [...lines];
|
|
171
198
|
let insertIndex = updatedLines.length;
|
|
172
199
|
for (let index = firstSafeSectionIndex + 1; index < updatedLines.length; index += 1) {
|
|
173
|
-
if (/^\s*\[([^\]]+)\]\s*$/.test(updatedLines[index])) {
|
|
200
|
+
if (/^\s*\[([^\]]+)\]\s*$/.test(updatedLines[index] ?? '')) {
|
|
174
201
|
insertIndex = index;
|
|
175
202
|
break;
|
|
176
203
|
}
|
|
@@ -184,12 +211,16 @@ function appendSafeDirectories(lines, repoRoot) {
|
|
|
184
211
|
return updatedLines;
|
|
185
212
|
}
|
|
186
213
|
|
|
187
|
-
function normalizeContainerHomeSeparators(content) {
|
|
214
|
+
function normalizeContainerHomeSeparators(content: string): string {
|
|
188
215
|
const containerHomePattern = new RegExp(`${escapeRegExp(CONTAINER_HOME)}\\S*`, 'g');
|
|
189
216
|
return content.replace(containerHomePattern, (value) => value.replaceAll('\\', '/'));
|
|
190
217
|
}
|
|
191
218
|
|
|
192
|
-
export function sanitizeGitConfig(
|
|
219
|
+
export function sanitizeGitConfig(
|
|
220
|
+
gitconfig: string,
|
|
221
|
+
home: string,
|
|
222
|
+
{ stripGpg = false, repoRoot = '' }: { stripGpg?: boolean; repoRoot?: string } = {}
|
|
223
|
+
): string {
|
|
193
224
|
const posixHome = home.replaceAll('\\', '/');
|
|
194
225
|
const normalizedGitconfig = gitconfig
|
|
195
226
|
.replaceAll(home, CONTAINER_HOME)
|
|
@@ -206,8 +237,8 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
|
|
|
206
237
|
for (const line of lines) {
|
|
207
238
|
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
208
239
|
if (sectionMatch) {
|
|
209
|
-
const sectionName = sectionMatch[1].trim();
|
|
210
|
-
currentSection = (sectionName.match(/^([^\s"]+)/)?.[1] ?? '').toLowerCase();
|
|
240
|
+
const sectionName = (sectionMatch[1] ?? '').trim();
|
|
241
|
+
currentSection = ((sectionName.match(/^([^\s"]+)/)?.[1]) ?? '').toLowerCase();
|
|
211
242
|
inGpgSection = /^gpg(?:\s+"[^"]+")?$/i.test(sectionName);
|
|
212
243
|
if (stripGpg && inGpgSection) {
|
|
213
244
|
continue;
|
|
@@ -241,11 +272,21 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
|
|
|
241
272
|
return appendSafeDirectories(sanitized, repoRoot).join('\n');
|
|
242
273
|
}
|
|
243
274
|
|
|
244
|
-
export function hostHasGpgKeys(home, execFn = execFileSync) {
|
|
275
|
+
export function hostHasGpgKeys(home: string, execFn: ExecSyncFn = execFileSync): boolean {
|
|
245
276
|
return currentKeyringFingerprint(home, execFn) !== null;
|
|
246
277
|
}
|
|
247
278
|
|
|
248
|
-
export function writeSanitizedGitconfig({
|
|
279
|
+
export function writeSanitizedGitconfig({
|
|
280
|
+
home,
|
|
281
|
+
hostConfigDir,
|
|
282
|
+
stripGpg,
|
|
283
|
+
repoRoot
|
|
284
|
+
}: {
|
|
285
|
+
home: string;
|
|
286
|
+
hostConfigDir: string;
|
|
287
|
+
stripGpg: boolean;
|
|
288
|
+
repoRoot: string;
|
|
289
|
+
}): string {
|
|
249
290
|
const gitconfigPath = hostJoin(home, '.gitconfig');
|
|
250
291
|
// Always emit a sanitized .gitconfig, even when the host has none. The
|
|
251
292
|
// container ~/.gitconfig is a symlink into the bound shell-config directory;
|
|
@@ -266,7 +307,7 @@ export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoo
|
|
|
266
307
|
// Keep in sync with the symlink block in lib/sandbox/runtimes/ai-tools.dockerfile.
|
|
267
308
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
268
309
|
|
|
269
|
-
export function ensureShellConfigSymlinks(engine, container, execFn = execEngine) {
|
|
310
|
+
export function ensureShellConfigSymlinks(engine: string, container: string, execFn: EngineExecFn = execEngine): void {
|
|
270
311
|
// Idempotent symlink setup. Runs against a started container so it also
|
|
271
312
|
// covers custom Dockerfiles that don't bake the symlinks into the image.
|
|
272
313
|
const script = SHELL_CONFIG_SYMLINKS
|
|
@@ -275,7 +316,17 @@ export function ensureShellConfigSymlinks(engine, container, execFn = execEngine
|
|
|
275
316
|
execFn(engine, 'docker', ['exec', container, 'bash', '-lc', script], { stdio: 'ignore' });
|
|
276
317
|
}
|
|
277
318
|
|
|
278
|
-
export function prepareHostShellConfig({
|
|
319
|
+
export function prepareHostShellConfig({
|
|
320
|
+
home,
|
|
321
|
+
project,
|
|
322
|
+
branch,
|
|
323
|
+
repoRoot
|
|
324
|
+
}: {
|
|
325
|
+
home: string;
|
|
326
|
+
project: string;
|
|
327
|
+
branch: string;
|
|
328
|
+
repoRoot: string;
|
|
329
|
+
}): HostShellConfig {
|
|
279
330
|
const hostDir = hostShellConfigDir(home, project, branch);
|
|
280
331
|
fs.rmSync(hostDir, { recursive: true, force: true });
|
|
281
332
|
fs.mkdirSync(hostDir, { recursive: true });
|
|
@@ -308,11 +359,11 @@ export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
|
|
|
308
359
|
return { hostDir, mounts };
|
|
309
360
|
}
|
|
310
361
|
|
|
311
|
-
function gpgCacheDir(home, project) {
|
|
362
|
+
function gpgCacheDir(home: string, project: string): string {
|
|
312
363
|
return hostJoin(home, '.agent-infra', 'gpg-cache', project);
|
|
313
364
|
}
|
|
314
365
|
|
|
315
|
-
function normalizeSigningKey(signingKey) {
|
|
366
|
+
function normalizeSigningKey(signingKey: unknown): string | null {
|
|
316
367
|
if (typeof signingKey !== 'string') {
|
|
317
368
|
return null;
|
|
318
369
|
}
|
|
@@ -321,7 +372,7 @@ function normalizeSigningKey(signingKey) {
|
|
|
321
372
|
return trimmed.length > 0 ? trimmed : null;
|
|
322
373
|
}
|
|
323
374
|
|
|
324
|
-
function normalizeWorktreePath(worktreePath) {
|
|
375
|
+
function normalizeWorktreePath(worktreePath: string): string {
|
|
325
376
|
if (!worktreePath) {
|
|
326
377
|
return '';
|
|
327
378
|
}
|
|
@@ -333,7 +384,15 @@ function normalizeWorktreePath(worktreePath) {
|
|
|
333
384
|
}
|
|
334
385
|
}
|
|
335
386
|
|
|
336
|
-
export function getGitSigningKey({
|
|
387
|
+
export function getGitSigningKey({
|
|
388
|
+
home,
|
|
389
|
+
repoPath = null,
|
|
390
|
+
execFn = execFileSync
|
|
391
|
+
}: {
|
|
392
|
+
home?: string;
|
|
393
|
+
repoPath?: string | null;
|
|
394
|
+
execFn?: ExecSyncFn;
|
|
395
|
+
} = {}): string | null {
|
|
337
396
|
if (!home) {
|
|
338
397
|
return null;
|
|
339
398
|
}
|
|
@@ -348,13 +407,13 @@ export function getGitSigningKey({ home, repoPath = null, execFn = execFileSync
|
|
|
348
407
|
env: { ...process.env, HOME: home },
|
|
349
408
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
350
409
|
});
|
|
351
|
-
return normalizeSigningKey(output);
|
|
410
|
+
return normalizeSigningKey(output.toString());
|
|
352
411
|
} catch {
|
|
353
412
|
return null;
|
|
354
413
|
}
|
|
355
414
|
}
|
|
356
415
|
|
|
357
|
-
export function currentKeyringFingerprint(home, execFn = execFileSync) {
|
|
416
|
+
export function currentKeyringFingerprint(home: string, execFn: ExecSyncFn = execFileSync): string | null {
|
|
358
417
|
const hostEnv = { ...process.env, HOME: home };
|
|
359
418
|
try {
|
|
360
419
|
const keyring = execFn('gpg', ['--list-secret-keys', '--with-colons'], {
|
|
@@ -362,23 +421,29 @@ export function currentKeyringFingerprint(home, execFn = execFileSync) {
|
|
|
362
421
|
env: hostEnv,
|
|
363
422
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
364
423
|
});
|
|
365
|
-
|
|
424
|
+
const keyringText = keyring.toString();
|
|
425
|
+
if (!keyringText || keyringText.trim().length === 0) {
|
|
366
426
|
return null;
|
|
367
427
|
}
|
|
368
|
-
return createHash('sha256').update(
|
|
428
|
+
return createHash('sha256').update(keyringText).digest('hex');
|
|
369
429
|
} catch {
|
|
370
430
|
return null;
|
|
371
431
|
}
|
|
372
432
|
}
|
|
373
433
|
|
|
374
|
-
export function readGpgCache(
|
|
434
|
+
export function readGpgCache(
|
|
435
|
+
home: string,
|
|
436
|
+
project: string,
|
|
437
|
+
execFn: ExecSyncFn = execFileSync,
|
|
438
|
+
signingKey: string | null = null
|
|
439
|
+
): GpgCache {
|
|
375
440
|
const cacheDir = gpgCacheDir(home, project);
|
|
376
441
|
const pubPath = path.join(cacheDir, 'public.asc');
|
|
377
442
|
const secPath = path.join(cacheDir, 'secret.asc');
|
|
378
443
|
const statePath = path.join(cacheDir, 'state.json');
|
|
379
444
|
|
|
380
445
|
try {
|
|
381
|
-
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
446
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8')) as { fingerprint?: unknown; signingKey?: unknown };
|
|
382
447
|
if (typeof state?.fingerprint !== 'string' || state.fingerprint.length === 0) {
|
|
383
448
|
return null;
|
|
384
449
|
}
|
|
@@ -403,7 +468,14 @@ export function readGpgCache(home, project, execFn = execFileSync, signingKey =
|
|
|
403
468
|
}
|
|
404
469
|
}
|
|
405
470
|
|
|
406
|
-
export function writeGpgCache(
|
|
471
|
+
export function writeGpgCache(
|
|
472
|
+
home: string,
|
|
473
|
+
project: string,
|
|
474
|
+
pub: Buffer | string,
|
|
475
|
+
sec: Buffer | string,
|
|
476
|
+
fingerprint: string | null,
|
|
477
|
+
signingKey: string | null = null
|
|
478
|
+
): boolean {
|
|
407
479
|
if (!fingerprint) {
|
|
408
480
|
return false;
|
|
409
481
|
}
|
|
@@ -414,7 +486,7 @@ export function writeGpgCache(home, project, pub, sec, fingerprint, signingKey =
|
|
|
414
486
|
const statePath = path.join(cacheDir, 'state.json');
|
|
415
487
|
|
|
416
488
|
try {
|
|
417
|
-
const state = { fingerprint };
|
|
489
|
+
const state: { fingerprint: string; signingKey?: string } = { fingerprint };
|
|
418
490
|
const normalizedSigningKey = normalizeSigningKey(signingKey);
|
|
419
491
|
if (normalizedSigningKey) {
|
|
420
492
|
state.signingKey = normalizedSigningKey;
|
|
@@ -439,13 +511,19 @@ export function writeGpgCache(home, project, pub, sec, fingerprint, signingKey =
|
|
|
439
511
|
}
|
|
440
512
|
|
|
441
513
|
export function syncGpgKeys(
|
|
442
|
-
container,
|
|
443
|
-
home,
|
|
444
|
-
project,
|
|
445
|
-
execFn = execFileSync,
|
|
446
|
-
runSafeFn = runSafe,
|
|
447
|
-
options
|
|
448
|
-
|
|
514
|
+
container: string,
|
|
515
|
+
home: string,
|
|
516
|
+
project: string,
|
|
517
|
+
execFn: ExecSyncFn = execFileSync,
|
|
518
|
+
runSafeFn: DirectRunSafeFn = runSafe,
|
|
519
|
+
options: {
|
|
520
|
+
cachedOverride?: GpgCache;
|
|
521
|
+
repoPath?: string | null;
|
|
522
|
+
signingKey?: string | null;
|
|
523
|
+
dockerExecFn?: ExecSyncFn;
|
|
524
|
+
dockerRunSafeFn?: DirectRunSafeFn;
|
|
525
|
+
} = {}
|
|
526
|
+
): boolean {
|
|
449
527
|
const {
|
|
450
528
|
cachedOverride = null,
|
|
451
529
|
repoPath = null,
|
|
@@ -477,18 +555,18 @@ export function syncGpgKeys(
|
|
|
477
555
|
? ['--export-secret-keys', signingKey]
|
|
478
556
|
: ['--export-secret-keys'];
|
|
479
557
|
|
|
480
|
-
pubKeys = execFn('gpg', exportArgs, {
|
|
558
|
+
pubKeys = Buffer.from(execFn('gpg', exportArgs, {
|
|
481
559
|
env: hostEnv,
|
|
482
560
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
483
|
-
});
|
|
561
|
+
}));
|
|
484
562
|
if (!pubKeys || pubKeys.length === 0) {
|
|
485
563
|
return false;
|
|
486
564
|
}
|
|
487
565
|
|
|
488
|
-
secKeys = execFn('gpg', exportSecretArgs, {
|
|
566
|
+
secKeys = Buffer.from(execFn('gpg', exportSecretArgs, {
|
|
489
567
|
env: hostEnv,
|
|
490
568
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
491
|
-
});
|
|
569
|
+
}));
|
|
492
570
|
if (!secKeys || secKeys.length === 0) {
|
|
493
571
|
return false;
|
|
494
572
|
}
|
|
@@ -505,11 +583,11 @@ export function syncGpgKeys(
|
|
|
505
583
|
}
|
|
506
584
|
|
|
507
585
|
dockerExecFn('docker', ['exec', '-i', container, 'gpg', '--import'], {
|
|
508
|
-
input: pubKeys,
|
|
586
|
+
input: pubKeys ?? undefined,
|
|
509
587
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
510
588
|
});
|
|
511
589
|
dockerExecFn('docker', ['exec', '-i', container, 'gpg', '--batch', '--import'], {
|
|
512
|
-
input: secKeys,
|
|
590
|
+
input: secKeys ?? undefined,
|
|
513
591
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
514
592
|
});
|
|
515
593
|
|
|
@@ -520,7 +598,7 @@ export function syncGpgKeys(
|
|
|
520
598
|
// Docker `--env-file` parsing has no quoting/escaping support and treats
|
|
521
599
|
// leading '#' as a comment. Newlines split entries, so reject them outright.
|
|
522
600
|
// Other shell metacharacters are safe because the values are not expanded.
|
|
523
|
-
function formatEnvFileEntry(key, value) {
|
|
601
|
+
function formatEnvFileEntry(key: string, value: string): string {
|
|
524
602
|
if (String(key).includes('\n') || String(value).includes('\n')) {
|
|
525
603
|
throw new Error(`Container environment variable ${key} must not contain newlines`);
|
|
526
604
|
}
|
|
@@ -528,11 +606,17 @@ function formatEnvFileEntry(key, value) {
|
|
|
528
606
|
}
|
|
529
607
|
|
|
530
608
|
export function buildContainerEnvFile(
|
|
531
|
-
resolvedTools,
|
|
532
|
-
engine,
|
|
533
|
-
runSafeEngineFn = runSafeEngine,
|
|
534
|
-
options
|
|
535
|
-
|
|
609
|
+
resolvedTools: ResolvedTool[],
|
|
610
|
+
engine: string,
|
|
611
|
+
runSafeEngineFn: EngineRunSafeFn = runSafeEngine,
|
|
612
|
+
options: {
|
|
613
|
+
mkdtempFn?: typeof fs.mkdtempSync;
|
|
614
|
+
writeFileFn?: typeof fs.writeFileSync;
|
|
615
|
+
chmodFn?: typeof fs.chmodSync;
|
|
616
|
+
rmFn?: typeof fs.rmSync;
|
|
617
|
+
tmpDir?: string;
|
|
618
|
+
} = {}
|
|
619
|
+
): { dockerArgs: string[]; cleanup: () => void } {
|
|
536
620
|
const {
|
|
537
621
|
mkdtempFn = fs.mkdtempSync,
|
|
538
622
|
writeFileFn = fs.writeFileSync,
|
|
@@ -541,7 +625,7 @@ export function buildContainerEnvFile(
|
|
|
541
625
|
tmpDir = os.tmpdir()
|
|
542
626
|
} = options;
|
|
543
627
|
|
|
544
|
-
const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
628
|
+
const entries: Array<[string, string]> = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
545
629
|
const ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
546
630
|
if (ghToken) {
|
|
547
631
|
entries.push(['GH_TOKEN', ghToken]);
|
|
@@ -579,7 +663,11 @@ export function buildContainerEnvFile(
|
|
|
579
663
|
}
|
|
580
664
|
}
|
|
581
665
|
|
|
582
|
-
export function buildDotfilesVolumeArgs(
|
|
666
|
+
export function buildDotfilesVolumeArgs(
|
|
667
|
+
engine: string,
|
|
668
|
+
snapshotDir: string | null | undefined,
|
|
669
|
+
existsFn: typeof fs.existsSync = fs.existsSync
|
|
670
|
+
): string[] {
|
|
583
671
|
if (!snapshotDir || !existsFn(snapshotDir)) {
|
|
584
672
|
return [];
|
|
585
673
|
}
|
|
@@ -587,10 +675,10 @@ export function buildDotfilesVolumeArgs(engine, snapshotDir, existsFn = fs.exist
|
|
|
587
675
|
}
|
|
588
676
|
|
|
589
677
|
export function assertBranchAvailable(
|
|
590
|
-
repoRoot,
|
|
591
|
-
branch,
|
|
592
|
-
{ allowedWorktrees = [], runFn = runSafe } = {}
|
|
593
|
-
) {
|
|
678
|
+
repoRoot: string,
|
|
679
|
+
branch: string,
|
|
680
|
+
{ allowedWorktrees = [], runFn = runSafe }: { allowedWorktrees?: string[]; runFn?: DirectRunSafeFn } = {}
|
|
681
|
+
): void {
|
|
594
682
|
const normalizedAllowedWorktrees = new Set(allowedWorktrees.map((worktree) => normalizeWorktreePath(worktree)));
|
|
595
683
|
const output = runFn('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain']);
|
|
596
684
|
if (!output) {
|
|
@@ -620,25 +708,29 @@ export function assertBranchAvailable(
|
|
|
620
708
|
}
|
|
621
709
|
}
|
|
622
710
|
|
|
623
|
-
function readHostJsonSafe(filePath) {
|
|
711
|
+
function readHostJsonSafe(filePath: string): JsonObject | null {
|
|
624
712
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
625
713
|
return null;
|
|
626
714
|
}
|
|
627
715
|
|
|
628
716
|
try {
|
|
629
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
630
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
717
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
|
|
718
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : null;
|
|
631
719
|
} catch {
|
|
632
720
|
return null;
|
|
633
721
|
}
|
|
634
722
|
}
|
|
635
723
|
|
|
636
|
-
export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
|
|
724
|
+
export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): void {
|
|
637
725
|
const claudeJsonPath = path.join(toolDir, '.claude.json');
|
|
638
|
-
let data
|
|
726
|
+
let data: JsonObject & {
|
|
727
|
+
hasCompletedOnboarding?: boolean;
|
|
728
|
+
projects?: Record<string, { hasTrustDialogAccepted?: boolean }>;
|
|
729
|
+
model?: string;
|
|
730
|
+
} = {};
|
|
639
731
|
if (fs.existsSync(claudeJsonPath)) {
|
|
640
732
|
try {
|
|
641
|
-
data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
|
|
733
|
+
data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')) as typeof data;
|
|
642
734
|
} catch {
|
|
643
735
|
// malformed JSON, start fresh
|
|
644
736
|
}
|
|
@@ -697,12 +789,12 @@ export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
|
|
|
697
789
|
}
|
|
698
790
|
}
|
|
699
791
|
|
|
700
|
-
export function ensureClaudeSettings(toolDir, hostHomeDir) {
|
|
792
|
+
export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): void {
|
|
701
793
|
const settingsPath = path.join(toolDir, 'settings.json');
|
|
702
|
-
let data = {};
|
|
794
|
+
let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean; effortLevel?: string } = {};
|
|
703
795
|
if (fs.existsSync(settingsPath)) {
|
|
704
796
|
try {
|
|
705
|
-
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
797
|
+
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as typeof data;
|
|
706
798
|
} catch {
|
|
707
799
|
// malformed JSON, start fresh
|
|
708
800
|
}
|
|
@@ -729,7 +821,7 @@ export function ensureClaudeSettings(toolDir, hostHomeDir) {
|
|
|
729
821
|
}
|
|
730
822
|
}
|
|
731
823
|
|
|
732
|
-
export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
|
|
824
|
+
export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: string): void {
|
|
733
825
|
if (!hostHomeDir) {
|
|
734
826
|
return;
|
|
735
827
|
}
|
|
@@ -739,19 +831,19 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
|
|
|
739
831
|
return;
|
|
740
832
|
}
|
|
741
833
|
|
|
742
|
-
let hostParsed;
|
|
834
|
+
let hostParsed: JsonObject;
|
|
743
835
|
try {
|
|
744
|
-
hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8'));
|
|
836
|
+
hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8')) as JsonObject;
|
|
745
837
|
} catch {
|
|
746
838
|
return;
|
|
747
839
|
}
|
|
748
840
|
|
|
749
841
|
const sandboxConfigPath = path.join(toolDir, 'config.toml');
|
|
750
842
|
// This rewrites sandbox-side TOML and drops comments; the host config stays untouched.
|
|
751
|
-
let sandboxParsed = {};
|
|
843
|
+
let sandboxParsed: JsonObject = {};
|
|
752
844
|
if (fs.existsSync(sandboxConfigPath)) {
|
|
753
845
|
try {
|
|
754
|
-
sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8'));
|
|
846
|
+
sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8')) as JsonObject;
|
|
755
847
|
} catch {
|
|
756
848
|
return;
|
|
757
849
|
}
|
|
@@ -775,7 +867,7 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
|
|
|
775
867
|
}
|
|
776
868
|
}
|
|
777
869
|
|
|
778
|
-
export function ensureCodexWorkspaceTrust(toolDir) {
|
|
870
|
+
export function ensureCodexWorkspaceTrust(toolDir: string): void {
|
|
779
871
|
const configPath = path.join(toolDir, 'config.toml');
|
|
780
872
|
let content = '';
|
|
781
873
|
if (fs.existsSync(configPath)) {
|
|
@@ -787,7 +879,7 @@ export function ensureCodexWorkspaceTrust(toolDir) {
|
|
|
787
879
|
}
|
|
788
880
|
}
|
|
789
881
|
|
|
790
|
-
export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
|
|
882
|
+
export function ensureOpenCodeModelInheritance(toolDir: string, hostHomeDir?: string): void {
|
|
791
883
|
if (!hostHomeDir) {
|
|
792
884
|
return;
|
|
793
885
|
}
|
|
@@ -799,7 +891,7 @@ export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
|
|
|
799
891
|
}
|
|
800
892
|
|
|
801
893
|
const sandboxConfigPath = path.join(toolDir, 'opencode.json');
|
|
802
|
-
let sandboxJson = {};
|
|
894
|
+
let sandboxJson: JsonObject = {};
|
|
803
895
|
if (fs.existsSync(sandboxConfigPath)) {
|
|
804
896
|
const existing = readHostJsonSafe(sandboxConfigPath);
|
|
805
897
|
if (!existing) {
|
|
@@ -825,12 +917,12 @@ export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
|
|
|
825
917
|
}
|
|
826
918
|
}
|
|
827
919
|
|
|
828
|
-
export function ensureGeminiWorkspaceTrust(toolDir) {
|
|
920
|
+
export function ensureGeminiWorkspaceTrust(toolDir: string): void {
|
|
829
921
|
const trustPath = path.join(toolDir, 'trustedFolders.json');
|
|
830
|
-
let data = {};
|
|
922
|
+
let data: Record<string, string> = {};
|
|
831
923
|
if (fs.existsSync(trustPath)) {
|
|
832
924
|
try {
|
|
833
|
-
data = JSON.parse(fs.readFileSync(trustPath, 'utf8'))
|
|
925
|
+
data = JSON.parse(fs.readFileSync(trustPath, 'utf8')) as Record<string, string>;
|
|
834
926
|
} catch {
|
|
835
927
|
// malformed JSON, start fresh
|
|
836
928
|
}
|
|
@@ -841,15 +933,15 @@ export function ensureGeminiWorkspaceTrust(toolDir) {
|
|
|
841
933
|
}
|
|
842
934
|
}
|
|
843
935
|
|
|
844
|
-
export function sandboxAliasesPath(home) {
|
|
936
|
+
export function sandboxAliasesPath(home: string): string {
|
|
845
937
|
return hostJoin(home, '.agent-infra', 'aliases', 'sandbox.sh');
|
|
846
938
|
}
|
|
847
939
|
|
|
848
|
-
function escapeRegExp(value) {
|
|
940
|
+
function escapeRegExp(value: string): string {
|
|
849
941
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
850
942
|
}
|
|
851
943
|
|
|
852
|
-
function stripManagedSandboxAliasBlocks(content) {
|
|
944
|
+
function stripManagedSandboxAliasBlocks(content: string): string {
|
|
853
945
|
const blockPattern = new RegExp(
|
|
854
946
|
`${escapeRegExp(SANDBOX_ALIAS_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(SANDBOX_ALIAS_BLOCK_END)}\\n?`,
|
|
855
947
|
'g'
|
|
@@ -857,7 +949,7 @@ function stripManagedSandboxAliasBlocks(content) {
|
|
|
857
949
|
return content.replace(blockPattern, '').trimEnd();
|
|
858
950
|
}
|
|
859
951
|
|
|
860
|
-
function isLegacyManagedSandboxAliasFile(content) {
|
|
952
|
+
function isLegacyManagedSandboxAliasFile(content: string): boolean {
|
|
861
953
|
const lines = content
|
|
862
954
|
.split(/\r?\n/)
|
|
863
955
|
.map((line) => line.trim())
|
|
@@ -871,7 +963,7 @@ function isLegacyManagedSandboxAliasFile(content) {
|
|
|
871
963
|
return lines.every((line) => aliasPattern.test(line));
|
|
872
964
|
}
|
|
873
965
|
|
|
874
|
-
export function ensureSandboxAliasesFile(home) {
|
|
966
|
+
export function ensureSandboxAliasesFile(home: string): { created: boolean; path: string } {
|
|
875
967
|
const aliasesPath = sandboxAliasesPath(home);
|
|
876
968
|
const managedBlock = `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${DEFAULT_SANDBOX_ALIASES}${SANDBOX_ALIAS_BLOCK_END}\n`;
|
|
877
969
|
fs.mkdirSync(path.dirname(aliasesPath), { recursive: true });
|
|
@@ -896,12 +988,19 @@ export function ensureSandboxAliasesFile(home) {
|
|
|
896
988
|
return { created, path: aliasesPath };
|
|
897
989
|
}
|
|
898
990
|
|
|
899
|
-
export function commandErrorMessage(error) {
|
|
900
|
-
const stderr = error
|
|
901
|
-
|
|
991
|
+
export function commandErrorMessage(error: unknown): string {
|
|
992
|
+
const stderr = typeof error === 'object' && error !== null && 'stderr' in error
|
|
993
|
+
? String(error.stderr).trim()
|
|
994
|
+
: '';
|
|
995
|
+
const message = error instanceof Error
|
|
996
|
+
? error.message
|
|
997
|
+
: typeof error === 'object' && error !== null && 'message' in error
|
|
998
|
+
? String(error.message)
|
|
999
|
+
: 'Command failed';
|
|
1000
|
+
return redactCommandError(stderr || message);
|
|
902
1001
|
}
|
|
903
1002
|
|
|
904
|
-
function runTaskCommand(cmd, args, opts = {}) {
|
|
1003
|
+
function runTaskCommand(cmd: string, args: string[], opts: { cwd?: string } = {}): string {
|
|
905
1004
|
try {
|
|
906
1005
|
return run(cmd, args, opts);
|
|
907
1006
|
} catch (error) {
|
|
@@ -909,32 +1008,39 @@ function runTaskCommand(cmd, args, opts = {}) {
|
|
|
909
1008
|
}
|
|
910
1009
|
}
|
|
911
1010
|
|
|
912
|
-
function runEngineTaskCommand(engine, cmd, args, opts = {}) {
|
|
1011
|
+
function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts: { cwd?: string } = {}): string {
|
|
913
1012
|
const command = commandForEngine(engine, cmd, args);
|
|
914
1013
|
return runTaskCommand(command.cmd, command.args, opts);
|
|
915
1014
|
}
|
|
916
1015
|
|
|
917
1016
|
export function buildImage(
|
|
918
|
-
config,
|
|
919
|
-
tools,
|
|
920
|
-
dockerfilePath,
|
|
921
|
-
imageSignature,
|
|
1017
|
+
config: SandboxCreateConfig,
|
|
1018
|
+
tools: SandboxTool[],
|
|
1019
|
+
dockerfilePath: string,
|
|
1020
|
+
imageSignature: string,
|
|
922
1021
|
{
|
|
923
1022
|
engine,
|
|
924
1023
|
runFn = runEngine,
|
|
925
1024
|
runSafeFn = runSafeEngine,
|
|
926
1025
|
runVerboseFn = runVerboseEngine,
|
|
927
1026
|
env = process.env
|
|
1027
|
+
}: {
|
|
1028
|
+
engine?: string;
|
|
1029
|
+
runFn?: EngineRunFn;
|
|
1030
|
+
runSafeFn?: EngineRunSafeFn;
|
|
1031
|
+
runVerboseFn?: EngineRunVerboseFn;
|
|
1032
|
+
env?: NodeJS.ProcessEnv;
|
|
928
1033
|
} = {}
|
|
929
|
-
) {
|
|
1034
|
+
): void {
|
|
1035
|
+
const selectedEngine = engine ?? detectEngine(config);
|
|
930
1036
|
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
931
|
-
engine,
|
|
1037
|
+
engine: selectedEngine,
|
|
932
1038
|
runFn,
|
|
933
1039
|
runSafeFn,
|
|
934
1040
|
env
|
|
935
1041
|
});
|
|
936
1042
|
|
|
937
|
-
runVerboseFn(
|
|
1043
|
+
runVerboseFn(selectedEngine, 'docker', [
|
|
938
1044
|
'build',
|
|
939
1045
|
'-t',
|
|
940
1046
|
config.imageName,
|
|
@@ -949,12 +1055,12 @@ export function buildImage(
|
|
|
949
1055
|
'--label',
|
|
950
1056
|
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
951
1057
|
'-f',
|
|
952
|
-
toEnginePath(
|
|
953
|
-
toEnginePath(
|
|
1058
|
+
toEnginePath(selectedEngine, dockerfilePath),
|
|
1059
|
+
toEnginePath(selectedEngine, config.repoRoot)
|
|
954
1060
|
], { cwd: config.repoRoot });
|
|
955
1061
|
}
|
|
956
1062
|
|
|
957
|
-
export async function create(args) {
|
|
1063
|
+
export async function create(args: string[]): Promise<void> {
|
|
958
1064
|
const { values, positionals } = parseArgs({
|
|
959
1065
|
args,
|
|
960
1066
|
allowPositionals: true,
|
|
@@ -979,7 +1085,7 @@ export async function create(args) {
|
|
|
979
1085
|
validateClaudeCredentialsEnvOverride();
|
|
980
1086
|
|
|
981
1087
|
const config = loadConfig();
|
|
982
|
-
const [branchOrTaskId, base] = positionals;
|
|
1088
|
+
const [branchOrTaskId = '', base] = positionals;
|
|
983
1089
|
const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
|
|
984
1090
|
assertValidBranchName(branch);
|
|
985
1091
|
const effectiveConfig = {
|
|
@@ -1004,7 +1110,7 @@ export async function create(args) {
|
|
|
1004
1110
|
resolvedTools
|
|
1005
1111
|
);
|
|
1006
1112
|
const container = containerName(effectiveConfig, branch);
|
|
1007
|
-
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0];
|
|
1113
|
+
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
1008
1114
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
1009
1115
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
1010
1116
|
const preparedDockerfile = prepareDockerfile(effectiveConfig);
|
|
@@ -1019,7 +1125,7 @@ export async function create(args) {
|
|
|
1019
1125
|
|
|
1020
1126
|
try {
|
|
1021
1127
|
p.log.step('Checking container engine...');
|
|
1022
|
-
await ensureDocker(effectiveConfig, (detail) => {
|
|
1128
|
+
await ensureDocker(effectiveConfig, (detail: string) => {
|
|
1023
1129
|
p.log.info(` ${detail}`);
|
|
1024
1130
|
});
|
|
1025
1131
|
p.log.success('Docker is ready');
|
|
@@ -1053,7 +1159,7 @@ export async function create(args) {
|
|
|
1053
1159
|
await p.tasks([
|
|
1054
1160
|
{
|
|
1055
1161
|
title: 'Setting up git worktree',
|
|
1056
|
-
task: async (message) => {
|
|
1162
|
+
task: async (message: (text: string) => void) => {
|
|
1057
1163
|
if (fs.existsSync(worktree)) {
|
|
1058
1164
|
if (fs.readdirSync(worktree).length > 0) {
|
|
1059
1165
|
return `Worktree exists at ${worktree}`;
|
|
@@ -1142,7 +1248,7 @@ export async function create(args) {
|
|
|
1142
1248
|
},
|
|
1143
1249
|
{
|
|
1144
1250
|
title: `Starting container '${container}'`,
|
|
1145
|
-
task: async (message) => {
|
|
1251
|
+
task: async (message: (text: string) => void) => {
|
|
1146
1252
|
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
1147
1253
|
const matchedContainers = containerNameCandidates(effectiveConfig, branch)
|
|
1148
1254
|
.filter((name) => existing.includes(name));
|
|
@@ -1178,7 +1284,7 @@ export async function create(args) {
|
|
|
1178
1284
|
)
|
|
1179
1285
|
: null;
|
|
1180
1286
|
const envFile = buildContainerEnvFile(resolvedTools, engine);
|
|
1181
|
-
let hostShellConfig;
|
|
1287
|
+
let hostShellConfig: HostShellConfig;
|
|
1182
1288
|
try {
|
|
1183
1289
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1184
1290
|
if (claudeCodeEntry) {
|
|
@@ -1303,8 +1409,8 @@ export async function create(args) {
|
|
|
1303
1409
|
cachedOverride: cachedGpg,
|
|
1304
1410
|
repoPath: worktree,
|
|
1305
1411
|
signingKey,
|
|
1306
|
-
dockerExecFn: (cmd, args, opts) => execEngine(engine, cmd, args, opts),
|
|
1307
|
-
dockerRunSafeFn: (cmd, args, opts) => runSafeEngine(engine, cmd, args, opts)
|
|
1412
|
+
dockerExecFn: (cmd: string, args: string[], opts?: ExecSyncOptions) => execEngine(engine, cmd, args, opts),
|
|
1413
|
+
dockerRunSafeFn: (cmd: string, args: string[], opts?: { cwd?: string }) => runSafeEngine(engine, cmd, args, opts)
|
|
1308
1414
|
}
|
|
1309
1415
|
)) {
|
|
1310
1416
|
writeSanitizedGitconfig({
|