@fitlab-ai/agent-infra 0.8.2 → 0.8.3
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/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +1 -1
- package/dist/lib/decide.js +2 -2
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +7 -7
- package/dist/lib/merge.js +1 -1
- package/dist/lib/prompt.js +1 -1
- package/dist/lib/run/index.js +7 -7
- package/dist/lib/run/prompt.js +1 -1
- package/dist/lib/sandbox/capture.js +5 -5
- package/dist/lib/sandbox/clipboard/bridge.js +48 -13
- package/dist/lib/sandbox/clipboard/index.js +5 -6
- package/dist/lib/sandbox/clipboard/linux.js +91 -0
- package/dist/lib/sandbox/clipboard/paths.js +1 -1
- package/dist/lib/sandbox/clipboard/win32.js +144 -0
- package/dist/lib/sandbox/commands/create.js +67 -65
- package/dist/lib/sandbox/commands/enter.js +9 -9
- package/dist/lib/sandbox/commands/list-running.js +2 -2
- package/dist/lib/sandbox/commands/ls.js +7 -7
- package/dist/lib/sandbox/commands/prune.js +8 -8
- package/dist/lib/sandbox/commands/rebuild.js +38 -50
- package/dist/lib/sandbox/commands/refresh.js +2 -2
- package/dist/lib/sandbox/commands/rm.js +11 -11
- package/dist/lib/sandbox/commands/show.js +4 -4
- package/dist/lib/sandbox/commands/start.js +4 -4
- package/dist/lib/sandbox/commands/vm.js +4 -4
- package/dist/lib/sandbox/config.js +10 -4
- package/dist/lib/sandbox/constants.js +4 -1
- package/dist/lib/sandbox/credentials.js +1 -1
- package/dist/lib/sandbox/dotfiles.js +1 -1
- package/dist/lib/sandbox/engine.js +3 -3
- package/dist/lib/sandbox/engines/index.js +5 -5
- package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
- package/dist/lib/sandbox/image-build.js +80 -0
- package/dist/lib/sandbox/image-prune.js +2 -2
- package/dist/lib/sandbox/index.js +10 -10
- package/dist/lib/sandbox/managed-fs.js +1 -1
- package/dist/lib/sandbox/readme-scaffold.js +1 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/adapters/feishu/index.js +10 -3
- package/dist/lib/server/adapters/feishu/renderer.js +88 -0
- package/dist/lib/server/adapters/feishu/transport.js +2 -13
- package/dist/lib/server/daemon.js +64 -43
- package/dist/lib/server/display.js +83 -0
- package/dist/lib/server/index.js +2 -2
- package/dist/lib/server/process-control.js +2 -2
- package/dist/lib/server/protocol.js +2 -2
- package/dist/lib/server/streamer.js +5 -4
- package/dist/lib/task/commands/cat.js +2 -2
- package/dist/lib/task/commands/decisions.js +4 -4
- package/dist/lib/task/commands/files.js +3 -3
- package/dist/lib/task/commands/grep.js +3 -3
- package/dist/lib/task/commands/issue-body.js +4 -4
- package/dist/lib/task/commands/log.js +5 -5
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/commands/show.js +1 -1
- package/dist/lib/task/commands/status.js +71 -25
- package/dist/lib/task/index.js +9 -9
- package/dist/lib/task/resolve-ref.js +1 -1
- package/dist/lib/task/workflow-warnings.js +94 -0
- package/dist/lib/update.js +4 -4
- package/lib/defaults.json +1 -0
- package/lib/sandbox/clipboard/bridge.ts +50 -8
- package/lib/sandbox/clipboard/index.ts +5 -6
- package/lib/sandbox/clipboard/linux.ts +124 -0
- package/lib/sandbox/clipboard/win32.ts +173 -0
- package/lib/sandbox/commands/create.ts +84 -64
- package/lib/sandbox/commands/rebuild.ts +42 -54
- package/lib/sandbox/config.ts +13 -1
- package/lib/sandbox/constants.ts +4 -0
- package/lib/sandbox/image-build.ts +134 -0
- package/lib/server/adapters/_contract.ts +3 -0
- package/lib/server/adapters/feishu/index.ts +11 -3
- package/lib/server/adapters/feishu/renderer.ts +99 -0
- package/lib/server/adapters/feishu/transport.ts +5 -18
- package/lib/server/daemon.ts +74 -38
- package/lib/server/display.ts +136 -0
- package/lib/server/streamer.ts +5 -4
- package/lib/task/commands/log.ts +3 -3
- package/lib/task/commands/status.ts +102 -21
- package/lib/task/workflow-warnings.ts +121 -0
- package/package.json +2 -2
- package/templates/.agents/rules/create-issue.github.en.md +3 -3
- package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
- package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
- package/templates/.agents/rules/issue-sync.github.en.md +13 -0
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
- package/templates/.agents/rules/next-step-output.en.md +15 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
- package/templates/.agents/rules/pr-sync.github.en.md +17 -2
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
- package/templates/.agents/scripts/validate-artifact.js +120 -0
- package/templates/.agents/scripts/workflow-warnings.js +290 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +7 -0
- package/templates/.agents/templates/task.en.md +7 -0
- package/templates/.agents/templates/task.zh-CN.md +7 -0
- package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
- package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
- package/templates/.github/workflows/metadata-sync.yml +5 -0
- package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
|
@@ -7,21 +7,21 @@ import { parseArgs } from 'node:util';
|
|
|
7
7
|
import * as p from '@clack/prompts';
|
|
8
8
|
import pc from 'picocolors';
|
|
9
9
|
import * as toml from 'smol-toml';
|
|
10
|
-
import { loadConfig } from
|
|
11
|
-
import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from
|
|
12
|
-
import { prepareDockerfile } from
|
|
13
|
-
import { detectEngine, ensureDocker } from
|
|
14
|
-
import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from
|
|
15
|
-
import { resolveTaskBranch } from
|
|
16
|
-
import {
|
|
17
|
-
import { hostJoin, toEnginePath, volumeArg } from
|
|
18
|
-
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from
|
|
19
|
-
import { validateSelinuxDisableEnv } from
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
10
|
+
import { loadConfig } from '../config.js';
|
|
11
|
+
import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
|
|
12
|
+
import { prepareDockerfile } from '../dockerfile.js';
|
|
13
|
+
import { detectEngine, ensureDocker } from '../engine.js';
|
|
14
|
+
import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from '../shell.js';
|
|
15
|
+
import { resolveTaskBranch } from '../task-resolver.js';
|
|
16
|
+
import { resolveTools, toolConfigDirCandidates } from '../tools.js';
|
|
17
|
+
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
|
|
18
|
+
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
|
|
19
|
+
import { validateSelinuxDisableEnv } from '../engines/selinux.js';
|
|
20
|
+
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
|
|
21
|
+
import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.js';
|
|
22
|
+
import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
|
|
23
|
+
import { detectHostTimezone } from '../host-timezone.js';
|
|
24
|
+
import { buildImageSignature, buildSandboxImageArgs, isRefreshDisabled, isRefreshDue, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
|
|
25
25
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
26
26
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
27
27
|
const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
|
|
@@ -47,22 +47,13 @@ alias gy='gemini --yolo; tput ed'
|
|
|
47
47
|
`;
|
|
48
48
|
const CONTAINER_HOME = '/home/devuser';
|
|
49
49
|
const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
|
|
50
|
-
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>]
|
|
50
|
+
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh]
|
|
51
51
|
|
|
52
52
|
Host aliases:
|
|
53
53
|
${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
|
|
54
54
|
as ${CONTAINER_HOME}/.bash_aliases inside the sandbox container (the host
|
|
55
55
|
shell-config directory is bind-mounted at ${CONTAINER_SHELL_CONFIG_MOUNT} and
|
|
56
56
|
symlinked into $HOME).`;
|
|
57
|
-
function buildSignature(preparedDockerfile, tools) {
|
|
58
|
-
return createHash('sha256')
|
|
59
|
-
.update(JSON.stringify({
|
|
60
|
-
dockerfile: preparedDockerfile.signature,
|
|
61
|
-
tools: imageSignatureFields(tools)
|
|
62
|
-
}))
|
|
63
|
-
.digest('hex')
|
|
64
|
-
.slice(0, 12);
|
|
65
|
-
}
|
|
66
57
|
function resolveToolDirs(config, tools, branch) {
|
|
67
58
|
return tools.map((tool) => {
|
|
68
59
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -908,34 +899,25 @@ export function buildTmpfsRunArgs(containerMount, tmpfs) {
|
|
|
908
899
|
const size = tmpfs.size ?? '512m';
|
|
909
900
|
return ['--tmpfs', `${containerMount}:rw,size=${size}`];
|
|
910
901
|
}
|
|
911
|
-
export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
|
|
902
|
+
export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh } = {}) {
|
|
912
903
|
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
913
|
-
|
|
904
|
+
runVerboseFn(selectedEngine, 'docker', buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
|
|
914
905
|
engine: selectedEngine,
|
|
915
906
|
runFn,
|
|
916
907
|
runSafeFn,
|
|
917
|
-
env
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
'
|
|
926
|
-
|
|
927
|
-
'
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
931
|
-
'--label',
|
|
932
|
-
sandboxLabel(config),
|
|
933
|
-
'--label',
|
|
934
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
935
|
-
'-f',
|
|
936
|
-
toEnginePath(selectedEngine, dockerfilePath),
|
|
937
|
-
toEnginePath(selectedEngine, config.repoRoot)
|
|
938
|
-
], { cwd: config.repoRoot });
|
|
908
|
+
env,
|
|
909
|
+
refresh,
|
|
910
|
+
lastRefresh
|
|
911
|
+
}), { cwd: config.repoRoot });
|
|
912
|
+
}
|
|
913
|
+
function readImageLabels(config, engine) {
|
|
914
|
+
return parseImageLabels(runSafeEngine(engine, 'docker', [
|
|
915
|
+
'image',
|
|
916
|
+
'inspect',
|
|
917
|
+
'--format',
|
|
918
|
+
'{{ json .Config.Labels }}',
|
|
919
|
+
config.imageName
|
|
920
|
+
]));
|
|
939
921
|
}
|
|
940
922
|
export async function create(args) {
|
|
941
923
|
const { values, positionals } = parseArgs({
|
|
@@ -945,6 +927,7 @@ export async function create(args) {
|
|
|
945
927
|
options: {
|
|
946
928
|
cpu: { type: 'string' },
|
|
947
929
|
memory: { type: 'string' },
|
|
930
|
+
'no-refresh': { type: 'boolean' },
|
|
948
931
|
help: { type: 'boolean', short: 'h' }
|
|
949
932
|
}
|
|
950
933
|
});
|
|
@@ -986,7 +969,7 @@ export async function create(args) {
|
|
|
986
969
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
987
970
|
const preparedDockerfile = prepareDockerfile(effectiveConfig);
|
|
988
971
|
const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
|
|
989
|
-
const expectedImageSignature =
|
|
972
|
+
const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
990
973
|
const engine = detectEngine(effectiveConfig);
|
|
991
974
|
p.intro(pc.cyan('AI Sandbox'));
|
|
992
975
|
p.log.info(`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`);
|
|
@@ -1002,20 +985,41 @@ export async function create(args) {
|
|
|
1002
985
|
});
|
|
1003
986
|
p.log.success('Docker is ready');
|
|
1004
987
|
const imageExists = runOkEngine(engine, 'docker', ['image', 'inspect', effectiveConfig.imageName]);
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
])
|
|
1013
|
-
|
|
1014
|
-
const needsImageBuild =
|
|
988
|
+
const imageLabels = imageExists ? readImageLabels(effectiveConfig, engine) : {};
|
|
989
|
+
const currentImageSignature = imageLabels[sandboxImageConfigLabel(effectiveConfig)] ?? '';
|
|
990
|
+
const currentLastRefresh = parseRefreshTimestamp(imageLabels[sandboxImageRefreshLabel(effectiveConfig)] ?? '');
|
|
991
|
+
const signatureStale = !imageExists || currentImageSignature !== expectedImageSignature;
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
const refreshDue = imageExists
|
|
994
|
+
&& !signatureStale
|
|
995
|
+
&& !isRefreshDisabled(process.env, values['no-refresh'] ?? false)
|
|
996
|
+
&& isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
|
|
997
|
+
const needsImageBuild = signatureStale || refreshDue;
|
|
1015
998
|
if (needsImageBuild) {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
p.log.
|
|
999
|
+
const buildRefresh = !imageExists || refreshDue;
|
|
1000
|
+
const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
|
|
1001
|
+
p.log.step(refreshDue
|
|
1002
|
+
? 'Refreshing stale image...'
|
|
1003
|
+
: imageExists
|
|
1004
|
+
? 'Rebuilding stale image...'
|
|
1005
|
+
: 'Building image for first use...');
|
|
1006
|
+
try {
|
|
1007
|
+
buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, { engine, refresh: buildRefresh, lastRefresh: buildLastRefresh });
|
|
1008
|
+
p.log.success(refreshDue
|
|
1009
|
+
? 'Image refreshed'
|
|
1010
|
+
: imageExists
|
|
1011
|
+
? 'Image rebuilt'
|
|
1012
|
+
: 'Image built');
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
if (refreshDue && !signatureStale && imageExists) {
|
|
1016
|
+
p.log.warn('Scheduled sandbox image refresh failed; continuing with the existing image. ' +
|
|
1017
|
+
commandErrorMessage(error));
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1019
1023
|
}
|
|
1020
1024
|
else {
|
|
1021
1025
|
p.log.step(`Using existing image ${effectiveConfig.imageName}`);
|
|
@@ -1231,8 +1235,6 @@ export async function create(args) {
|
|
|
1231
1235
|
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1232
1236
|
'-v',
|
|
1233
1237
|
volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
|
|
1234
|
-
'-v',
|
|
1235
|
-
volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
|
|
1236
1238
|
...dotfilesMount,
|
|
1237
1239
|
...toolVolumes,
|
|
1238
1240
|
...tmpfsArgs,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { loadConfig } from
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from
|
|
3
|
-
import { detectEngine } from
|
|
4
|
-
import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from
|
|
5
|
-
import { runInteractiveEngine } from
|
|
6
|
-
import { dotfilesCacheDir, materializeDotfiles } from
|
|
7
|
-
import { runInteractiveWithClipboardBridge } from
|
|
8
|
-
import { detectHostTimezone } from
|
|
9
|
-
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
3
|
+
import { detectEngine } from '../engine.js';
|
|
4
|
+
import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
|
|
5
|
+
import { runInteractiveEngine } from '../shell.js';
|
|
6
|
+
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
|
|
7
|
+
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
|
|
8
|
+
import { detectHostTimezone } from '../host-timezone.js';
|
|
9
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from './list-running.js';
|
|
10
10
|
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
11
11
|
|
|
12
12
|
N (bare) and '#N' both reference the same active task short id from
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { runSafeEngine, runVerboseEngine } from
|
|
5
|
-
import { resolveTaskBranch } from
|
|
4
|
+
import { runSafeEngine, runVerboseEngine } from '../shell.js';
|
|
5
|
+
import { resolveTaskBranch } from '../task-resolver.js';
|
|
6
6
|
export function containerListFormat() {
|
|
7
7
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
8
8
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { sandboxBranchLabel, sandboxLabel } from
|
|
5
|
-
import { detectEngine } from
|
|
6
|
-
import { formatTable } from
|
|
7
|
-
import { lookupShortIdByBranch } from
|
|
8
|
-
import { fetchSandboxRows } from
|
|
9
|
-
export { containerListFormat, parseLabels } from
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
5
|
+
import { detectEngine } from '../engine.js';
|
|
6
|
+
import { formatTable } from '../../table.js';
|
|
7
|
+
import { lookupShortIdByBranch } from '../../task/short-id.js';
|
|
8
|
+
import { fetchSandboxRows } from './list-running.js';
|
|
9
|
+
export { containerListFormat, parseLabels } from './list-running.js';
|
|
10
10
|
const USAGE = `Usage: ai sandbox ls
|
|
11
11
|
|
|
12
12
|
Lists all containers for the current project. The '#' column is a
|
|
@@ -3,14 +3,14 @@ import path from 'node:path';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
-
import { loadConfig } from
|
|
7
|
-
import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from
|
|
8
|
-
import { detectEngine } from
|
|
9
|
-
import { hostJoin } from
|
|
10
|
-
import { removeManagedDir, removeWorktreeDir } from
|
|
11
|
-
import { parseLabels } from
|
|
12
|
-
import { runEngine, runSafe } from
|
|
13
|
-
import { resolveTools } from
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
8
|
+
import { detectEngine } from '../engine.js';
|
|
9
|
+
import { hostJoin } from '../engines/wsl2-paths.js';
|
|
10
|
+
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
|
+
import { parseLabels } from './ls.js';
|
|
12
|
+
import { runEngine, runSafe } from '../shell.js';
|
|
13
|
+
import { resolveTools } from '../tools.js';
|
|
14
14
|
const USAGE = `Usage: ai sandbox prune [--dry-run]`;
|
|
15
15
|
function listChildDirs(base) {
|
|
16
16
|
if (!fs.existsSync(base)) {
|
|
@@ -1,58 +1,37 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
2
|
import * as p from '@clack/prompts';
|
|
4
3
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from
|
|
6
|
-
import { prepareDockerfile } from
|
|
7
|
-
import {
|
|
8
|
-
import { detectEngine, ensureDocker } from
|
|
9
|
-
import { runEngine, runSafeEngine, runVerboseEngine } from
|
|
10
|
-
import { pruneSandboxDanglingImages } from
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { resolveBuildUid } from "../engines/native.js";
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { prepareDockerfile } from '../dockerfile.js';
|
|
6
|
+
import { sandboxImageRefreshLabel } from '../constants.js';
|
|
7
|
+
import { detectEngine, ensureDocker } from '../engine.js';
|
|
8
|
+
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.js';
|
|
9
|
+
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
|
+
import { resolveTools } from '../tools.js';
|
|
11
|
+
import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
|
|
14
12
|
const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
|
|
15
|
-
function
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
dockerfile: preparedDockerfile.signature,
|
|
19
|
-
tools: imageSignatureFields(tools)
|
|
20
|
-
}))
|
|
21
|
-
.digest('hex')
|
|
22
|
-
.slice(0, 12);
|
|
23
|
-
}
|
|
24
|
-
export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false } = {}) {
|
|
25
|
-
const selectedEngine = engine ?? detectEngine(config);
|
|
26
|
-
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
27
|
-
engine: selectedEngine,
|
|
13
|
+
export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
|
|
14
|
+
return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
|
|
15
|
+
engine: engine ?? detectEngine(config),
|
|
28
16
|
runFn,
|
|
29
17
|
runSafeFn,
|
|
30
|
-
env
|
|
18
|
+
env,
|
|
19
|
+
refresh,
|
|
20
|
+
lastRefresh
|
|
31
21
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'--build-arg',
|
|
37
|
-
`HOST_UID=${hostUid}`,
|
|
38
|
-
'--build-arg',
|
|
39
|
-
`HOST_GID=${hostGid}`,
|
|
40
|
-
'--build-arg',
|
|
41
|
-
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
42
|
-
'--build-arg',
|
|
43
|
-
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
44
|
-
'--label',
|
|
45
|
-
sandboxLabel(config),
|
|
46
|
-
'--label',
|
|
47
|
-
`${sandboxImageConfigLabel(config)}=${imageSignature}`,
|
|
48
|
-
'-f',
|
|
49
|
-
toEnginePath(selectedEngine, dockerfilePath),
|
|
50
|
-
toEnginePath(selectedEngine, config.repoRoot)
|
|
51
|
-
];
|
|
52
|
-
if (refresh) {
|
|
53
|
-
args.splice(1, 0, '--no-cache', '--pull');
|
|
22
|
+
}
|
|
23
|
+
function readExistingLastRefresh(config, engine) {
|
|
24
|
+
if (!runOkEngine(engine, 'docker', ['image', 'inspect', config.imageName])) {
|
|
25
|
+
return 0;
|
|
54
26
|
}
|
|
55
|
-
|
|
27
|
+
const labels = parseImageLabels(runSafeEngine(engine, 'docker', [
|
|
28
|
+
'image',
|
|
29
|
+
'inspect',
|
|
30
|
+
'--format',
|
|
31
|
+
'{{ json .Config.Labels }}',
|
|
32
|
+
config.imageName
|
|
33
|
+
]));
|
|
34
|
+
return parseRefreshTimestamp(labels[sandboxImageRefreshLabel(config)] ?? '');
|
|
56
35
|
}
|
|
57
36
|
export async function rebuild(args) {
|
|
58
37
|
const { values } = parseArgs({
|
|
@@ -72,24 +51,33 @@ export async function rebuild(args) {
|
|
|
72
51
|
const config = loadConfig();
|
|
73
52
|
const tools = resolveTools(config);
|
|
74
53
|
const preparedDockerfile = prepareDockerfile(config);
|
|
75
|
-
const imageSignature =
|
|
54
|
+
const imageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
76
55
|
const quiet = values.quiet ?? false;
|
|
77
56
|
const refresh = values.refresh ?? false;
|
|
78
57
|
const engine = detectEngine(config);
|
|
79
58
|
await ensureDocker(config, undefined);
|
|
59
|
+
const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
|
|
80
60
|
p.intro(pc.cyan('Rebuilding sandbox image'));
|
|
81
61
|
try {
|
|
82
62
|
if (quiet) {
|
|
83
63
|
const spinner = p.spinner();
|
|
84
64
|
spinner.start('Building image...');
|
|
85
|
-
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
65
|
+
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
66
|
+
engine,
|
|
67
|
+
refresh,
|
|
68
|
+
lastRefresh
|
|
69
|
+
}), {
|
|
86
70
|
cwd: config.repoRoot
|
|
87
71
|
});
|
|
88
72
|
spinner.stop(pc.green('Sandbox image rebuilt'));
|
|
89
73
|
}
|
|
90
74
|
else {
|
|
91
75
|
p.log.step('Building image');
|
|
92
|
-
runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
76
|
+
runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
|
|
77
|
+
engine,
|
|
78
|
+
refresh,
|
|
79
|
+
lastRefresh
|
|
80
|
+
}), { cwd: config.repoRoot });
|
|
93
81
|
p.log.success(pc.green('Sandbox image rebuilt'));
|
|
94
82
|
}
|
|
95
83
|
pruneSandboxDanglingImages(config, engine);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import { buildLockedGuidance, discoverProjects, formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from
|
|
4
|
-
import { runProbe } from
|
|
3
|
+
import { buildLockedGuidance, discoverProjects, formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
|
|
4
|
+
import { runProbe } from '../shell.js';
|
|
5
5
|
const USAGE = 'Usage: ai sandbox refresh';
|
|
6
6
|
export function probeClaudeStatus(spawnFn = runProbe) {
|
|
7
7
|
const result = spawnFn('claude', ['/status'], {
|
|
@@ -3,21 +3,21 @@ import path from 'node:path';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
-
import { loadConfig } from
|
|
7
|
-
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from
|
|
8
|
-
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from
|
|
9
|
-
import { pruneSandboxDanglingImages } from
|
|
10
|
-
import { removeManagedDir, removeWorktreeDir } from
|
|
11
|
-
import { runOk, runSafe, runSafeEngine } from
|
|
12
|
-
import { resolveTaskBranch } from
|
|
13
|
-
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from
|
|
14
|
-
import { fetchSandboxRows } from
|
|
15
|
-
import { lookupShortIdByBranch } from
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from '../constants.js';
|
|
8
|
+
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
|
|
9
|
+
import { pruneSandboxDanglingImages } from '../image-prune.js';
|
|
10
|
+
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
|
|
11
|
+
import { runOk, runSafe, runSafeEngine } from '../shell.js';
|
|
12
|
+
import { resolveTaskBranch } from '../task-resolver.js';
|
|
13
|
+
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
14
|
+
import { fetchSandboxRows } from './list-running.js';
|
|
15
|
+
import { lookupShortIdByBranch } from '../../task/short-id.js';
|
|
16
16
|
const USAGE = `Usage:
|
|
17
17
|
ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
|
|
18
18
|
ai sandbox rm --all [--dry-run] [--yes] Remove every sandbox not bound to an active task
|
|
19
19
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
20
|
-
export { assertManagedPath } from
|
|
20
|
+
export { assertManagedPath } from '../managed-fs.js';
|
|
21
21
|
function projectToolDirs(config, tools) {
|
|
22
22
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
23
23
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig } from
|
|
5
|
-
import { assertValidBranchName, worktreeDirCandidates } from
|
|
6
|
-
import { resolveBranchArg } from
|
|
7
|
-
import { resolveTools, toolConfigDirCandidates } from
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { assertValidBranchName, worktreeDirCandidates } from '../constants.js';
|
|
6
|
+
import { resolveBranchArg } from './list-running.js';
|
|
7
|
+
import { resolveTools, toolConfigDirCandidates } from '../tools.js';
|
|
8
8
|
const USAGE = `Usage: ai sandbox show <branch | TASK-id | N | '#N'>
|
|
9
9
|
|
|
10
10
|
Shows one sandbox's worktree path and per-tool state paths (Claude Code,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from
|
|
3
|
-
import { detectEngine } from
|
|
4
|
-
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
|
|
3
|
+
import { detectEngine } from '../engine.js';
|
|
4
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from './list-running.js';
|
|
5
5
|
const USAGE = `Usage: ai sandbox start <branch | TASK-id | N | '#N'>
|
|
6
6
|
|
|
7
7
|
Start an existing sandbox container that has stopped (for example after the
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig } from
|
|
5
|
-
import { parsePositiveIntegerOption } from
|
|
6
|
-
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, startManagedVm, stopManagedVm } from
|
|
7
|
-
import { runOk, runSafe } from
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { parsePositiveIntegerOption } from '../constants.js';
|
|
6
|
+
import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, startManagedVm, stopManagedVm } from '../engine.js';
|
|
7
|
+
import { runOk, runSafe } from '../shell.js';
|
|
8
8
|
const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
|
|
9
9
|
export function ensureManagedVm(engine) {
|
|
10
10
|
if (engine === ENGINES.NATIVE) {
|
|
@@ -3,14 +3,15 @@ import path from 'node:path';
|
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
-
import { validateSandboxEngine } from
|
|
7
|
-
import { hostJoin } from
|
|
8
|
-
import { findRuntimeEngineMismatches } from
|
|
9
|
-
import { parseCustomTools } from
|
|
6
|
+
import { validateSandboxEngine } from './engine.js';
|
|
7
|
+
import { hostJoin } from './engines/wsl2-paths.js';
|
|
8
|
+
import { findRuntimeEngineMismatches } from './runtime-engines.js';
|
|
9
|
+
import { parseCustomTools } from './tools.js';
|
|
10
10
|
const DEFAULTS = Object.freeze({
|
|
11
11
|
engine: null,
|
|
12
12
|
runtimes: ['node22'],
|
|
13
13
|
tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
14
|
+
refreshIntervalDays: 7,
|
|
14
15
|
dockerfile: null,
|
|
15
16
|
vm: {
|
|
16
17
|
cpu: null,
|
|
@@ -32,11 +33,15 @@ function detectRepoRoot() {
|
|
|
32
33
|
function asPositiveNumberOrNull(value) {
|
|
33
34
|
return typeof value === 'number' ? value : null;
|
|
34
35
|
}
|
|
36
|
+
function asNonNegativeIntegerOrDefault(value, fallback) {
|
|
37
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
38
|
+
}
|
|
35
39
|
function cloneDefaults() {
|
|
36
40
|
return {
|
|
37
41
|
engine: DEFAULTS.engine,
|
|
38
42
|
runtimes: [...DEFAULTS.runtimes],
|
|
39
43
|
tools: [...DEFAULTS.tools],
|
|
44
|
+
refreshIntervalDays: DEFAULTS.refreshIntervalDays,
|
|
40
45
|
dockerfile: DEFAULTS.dockerfile,
|
|
41
46
|
vm: { ...DEFAULTS.vm }
|
|
42
47
|
};
|
|
@@ -96,6 +101,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
96
101
|
? [...sandbox.tools]
|
|
97
102
|
: defaults.tools,
|
|
98
103
|
customTools,
|
|
104
|
+
refreshIntervalDays: asNonNegativeIntegerOrDefault(sandbox.refreshIntervalDays, defaults.refreshIntervalDays),
|
|
99
105
|
dockerfile,
|
|
100
106
|
vm: {
|
|
101
107
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { hostJoin } from
|
|
3
|
+
import { hostJoin } from './engines/wsl2-paths.js';
|
|
4
4
|
const validatedBranches = new Set();
|
|
5
5
|
function dedupe(items) {
|
|
6
6
|
return [...new Set(items)];
|
|
@@ -72,6 +72,9 @@ export function sandboxBranchLabel(config) {
|
|
|
72
72
|
export function sandboxImageConfigLabel(config) {
|
|
73
73
|
return `${sandboxLabel(config)}.image-config`;
|
|
74
74
|
}
|
|
75
|
+
export function sandboxImageRefreshLabel(config) {
|
|
76
|
+
return `${sandboxLabel(config)}.last-refresh`;
|
|
77
|
+
}
|
|
75
78
|
export function parsePositiveIntegerOption(value, optionName) {
|
|
76
79
|
if (value === undefined || value === null) {
|
|
77
80
|
return undefined;
|
|
@@ -2,7 +2,7 @@ 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
|
|
5
|
+
import { hostJoin } from './engines/wsl2-paths.js';
|
|
6
6
|
const LOCKED_PATTERN = /errSecInteractionNotAllowed|User interaction is not allowed/i;
|
|
7
7
|
const NOT_FOUND_PATTERN = /errSecItemNotFound|specified item could not be found/i;
|
|
8
8
|
const REDACTION_PATTERNS = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { hostJoin } from
|
|
3
|
+
import { hostJoin } from './engines/wsl2-paths.js';
|
|
4
4
|
export function dotfilesCacheDir(home, project) {
|
|
5
5
|
return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
|
|
6
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { platform } from 'node:os';
|
|
2
|
-
import { detectHostResources } from
|
|
3
|
-
import { ADAPTERS, enginesForPlatform, getAdapter } from
|
|
4
|
-
import { run, runOk, runSafe, runVerbose } from
|
|
2
|
+
import { detectHostResources } from './constants.js';
|
|
3
|
+
import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.js';
|
|
4
|
+
import { run, runOk, runSafe, runVerbose } from './shell.js';
|
|
5
5
|
export const ENGINES = Object.freeze({
|
|
6
6
|
COLIMA: 'colima',
|
|
7
7
|
ORBSTACK: 'orbstack',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { colimaAdapter } from
|
|
2
|
-
import { dockerDesktopAdapter } from
|
|
3
|
-
import { nativeAdapter } from
|
|
4
|
-
import { orbstackAdapter } from
|
|
5
|
-
import { wsl2Adapter } from
|
|
1
|
+
import { colimaAdapter } from './colima.js';
|
|
2
|
+
import { dockerDesktopAdapter } from './docker-desktop.js';
|
|
3
|
+
import { nativeAdapter } from './native.js';
|
|
4
|
+
import { orbstackAdapter } from './orbstack.js';
|
|
5
|
+
import { wsl2Adapter } from './wsl2.js';
|
|
6
6
|
export const ADAPTERS = Object.freeze({
|
|
7
7
|
colima: colimaAdapter,
|
|
8
8
|
orbstack: orbstackAdapter,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { selinuxLabelForMount } from
|
|
2
|
+
import { selinuxLabelForMount } from './selinux.js';
|
|
3
3
|
const WINDOWS_DRIVE_PATH_PATTERN = /^([A-Za-z]):[\\/](.*)$/;
|
|
4
4
|
const UNC_PATH_PATTERN = /^(?:\\\\|\/\/)[^\\/]+[\\/][^\\/]+/;
|
|
5
5
|
export function hostJoin(basePath, ...segments) {
|