@fitlab-ai/agent-infra 0.6.5 → 0.7.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 +51 -25
- package/README.zh-CN.md +49 -23
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +3 -0
- package/dist/lib/sandbox/commands/create.js +4 -2
- package/dist/lib/sandbox/commands/enter.js +15 -4
- package/dist/lib/sandbox/commands/list-running.js +108 -0
- package/dist/lib/sandbox/commands/ls.js +24 -45
- package/dist/lib/sandbox/commands/rebuild.js +4 -2
- package/dist/lib/sandbox/config.js +3 -0
- package/dist/lib/sandbox/index.js +2 -1
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
- package/dist/lib/sandbox/tools.js +213 -8
- package/dist/lib/update.js +12 -1
- package/lib/defaults.json +1 -0
- package/lib/init.ts +10 -0
- package/lib/sandbox/commands/create.ts +10 -2
- package/lib/sandbox/commands/enter.ts +14 -4
- package/lib/sandbox/commands/list-running.ts +135 -0
- package/lib/sandbox/commands/ls.ts +28 -49
- package/lib/sandbox/commands/rebuild.ts +9 -2
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/index.ts +2 -1
- package/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
- package/lib/sandbox/tools.ts +248 -9
- package/lib/update.ts +15 -1
- package/package.json +1 -1
- package/templates/.agents/QUICKSTART.en.md +1 -1
- package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
- package/templates/.agents/README.en.md +79 -2
- package/templates/.agents/README.zh-CN.md +79 -2
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +6 -5
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
- package/templates/.agents/rules/pr-sync.github.en.md +4 -5
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
- package/templates/.agents/rules/task-management.en.md +9 -6
- package/templates/.agents/rules/task-management.zh-CN.md +9 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -2
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
- package/templates/.agents/scripts/validate-artifact.js +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
- package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
- package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
- package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
- package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
- package/templates/.agents/skills/commit/SKILL.en.md +2 -2
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
- package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
- package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
- package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
- package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
- package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/test/SKILL.en.md +2 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
- package/templates/.agents/workflows/feature-development.en.yaml +115 -70
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
- package/templates/.agents/workflows/refactoring.en.yaml +123 -78
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
- package/templates/.claude/commands/code-task.en.md +8 -0
- package/templates/.claude/commands/code-task.zh-CN.md +8 -0
- package/templates/.claude/commands/review-analysis.en.md +8 -0
- package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
- package/templates/.claude/commands/review-code.en.md +8 -0
- package/templates/.claude/commands/review-code.zh-CN.md +8 -0
- package/templates/.claude/commands/review-plan.en.md +8 -0
- package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
- package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
- package/templates/.opencode/commands/code-task.en.md +11 -0
- package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-analysis.en.md +11 -0
- package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-code.en.md +11 -0
- package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-plan.en.md +11 -0
- package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
- package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
- package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
- package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
- package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
- package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
- package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
- package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
- package/templates/.claude/commands/implement-task.en.md +0 -8
- package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
- package/templates/.claude/commands/refine-task.en.md +0 -8
- package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
- package/templates/.claude/commands/review-task.en.md +0 -8
- package/templates/.claude/commands/review-task.zh-CN.md +0 -8
- package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
- package/templates/.opencode/commands/implement-task.en.md +0 -11
- package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/refine-task.en.md +0 -11
- package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/review-task.en.md +0 -11
- package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
- /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
|
@@ -7,7 +7,7 @@ import { prepareDockerfile } from "../dockerfile.js";
|
|
|
7
7
|
import { sandboxImageConfigLabel, sandboxLabel } from "../constants.js";
|
|
8
8
|
import { detectEngine, ensureDocker } from "../engine.js";
|
|
9
9
|
import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
|
|
10
|
-
import { resolveTools, toolNpmPackagesArg } from "../tools.js";
|
|
10
|
+
import { imageSignatureFields, resolveTools, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
|
|
11
11
|
import { toEnginePath } from "../engines/wsl2-paths.js";
|
|
12
12
|
import { resolveBuildUid } from "../engines/native.js";
|
|
13
13
|
const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
|
|
@@ -15,7 +15,7 @@ function buildSignature(preparedDockerfile, tools) {
|
|
|
15
15
|
return createHash('sha256')
|
|
16
16
|
.update(JSON.stringify({
|
|
17
17
|
dockerfile: preparedDockerfile.signature,
|
|
18
|
-
tools: tools
|
|
18
|
+
tools: imageSignatureFields(tools)
|
|
19
19
|
}))
|
|
20
20
|
.digest('hex')
|
|
21
21
|
.slice(0, 12);
|
|
@@ -38,6 +38,8 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
|
|
|
38
38
|
`HOST_GID=${hostGid}`,
|
|
39
39
|
'--build-arg',
|
|
40
40
|
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
41
|
+
'--build-arg',
|
|
42
|
+
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
41
43
|
'--label',
|
|
42
44
|
sandboxLabel(config),
|
|
43
45
|
'--label',
|
|
@@ -6,6 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { validateSandboxEngine } from "./engine.js";
|
|
7
7
|
import { hostJoin } from "./engines/wsl2-paths.js";
|
|
8
8
|
import { findRuntimeEngineMismatches } from "./runtime-engines.js";
|
|
9
|
+
import { parseCustomTools } from "./tools.js";
|
|
9
10
|
const DEFAULTS = Object.freeze({
|
|
10
11
|
engine: null,
|
|
11
12
|
runtimes: ['node22'],
|
|
@@ -76,6 +77,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
76
77
|
' Update "sandbox.runtimes" in .agents/.airc.json (e.g. "node22"), or relax "engines.node".\n'));
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
const customTools = parseCustomTools(sandbox.customTools, { home });
|
|
79
81
|
return {
|
|
80
82
|
repoRoot,
|
|
81
83
|
configPath,
|
|
@@ -93,6 +95,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
|
|
|
93
95
|
tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
|
|
94
96
|
? [...sandbox.tools]
|
|
95
97
|
: defaults.tools,
|
|
98
|
+
customTools,
|
|
96
99
|
dockerfile,
|
|
97
100
|
vm: {
|
|
98
101
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
@@ -2,7 +2,8 @@ const USAGE = `Usage: ai sandbox <command> [options]
|
|
|
2
2
|
|
|
3
3
|
Commands:
|
|
4
4
|
create <branch> [base] Create a sandbox (VM + image + worktree + container)
|
|
5
|
-
exec <branch> [cmd...]
|
|
5
|
+
exec <branch | '#N'> [cmd...]
|
|
6
|
+
Enter sandbox or run a command (use leftmost '#' column from 'ls')
|
|
6
7
|
ls List sandboxes for the current project
|
|
7
8
|
prune [--dry-run] Remove orphaned per-branch state dirs
|
|
8
9
|
rebuild [--quiet] [--refresh]
|
|
@@ -4,16 +4,20 @@ ENV OPENCODE_DISABLE_AUTOUPDATE=1
|
|
|
4
4
|
ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
|
|
5
5
|
ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
|
|
6
6
|
|
|
7
|
-
ARG AI_TOOL_PACKAGES
|
|
8
|
-
RUN
|
|
9
|
-
echo "AI_TOOL_PACKAGES build arg is required"; \
|
|
10
|
-
exit 1; \
|
|
11
|
-
fi && \
|
|
12
|
-
set -e && \
|
|
7
|
+
ARG AI_TOOL_PACKAGES=
|
|
8
|
+
RUN set -e && \
|
|
13
9
|
for pkg in ${AI_TOOL_PACKAGES}; do \
|
|
14
10
|
npm install -g "$pkg"; \
|
|
15
11
|
done
|
|
16
12
|
|
|
13
|
+
ARG AI_TOOLS_SHELL_INSTALL_B64=
|
|
14
|
+
RUN if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
|
|
15
|
+
set -e && \
|
|
16
|
+
echo "${AI_TOOLS_SHELL_INSTALL_B64}" | base64 -d > /tmp/ai-tools-install.sh && \
|
|
17
|
+
bash /tmp/ai-tools-install.sh && \
|
|
18
|
+
rm /tmp/ai-tools-install.sh; \
|
|
19
|
+
fi
|
|
20
|
+
|
|
17
21
|
RUN npm install -g pyright
|
|
18
22
|
|
|
19
23
|
RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
|
|
2
2
|
import { hostJoin } from "./engines/wsl2-paths.js";
|
|
3
|
+
const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
3
4
|
function createBuiltinTools(home, project) {
|
|
4
5
|
return {
|
|
5
6
|
'claude-code': {
|
|
6
7
|
id: 'claude-code',
|
|
7
8
|
name: 'Claude Code',
|
|
8
|
-
|
|
9
|
+
install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
|
|
9
10
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
10
11
|
containerMount: '/home/devuser/.claude',
|
|
11
12
|
versionCmd: 'claude --version',
|
|
@@ -35,7 +36,7 @@ function createBuiltinTools(home, project) {
|
|
|
35
36
|
codex: {
|
|
36
37
|
id: 'codex',
|
|
37
38
|
name: 'Codex',
|
|
38
|
-
|
|
39
|
+
install: { type: 'npm', cmd: '@openai/codex' },
|
|
39
40
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
|
|
40
41
|
containerMount: '/home/devuser/.codex',
|
|
41
42
|
versionCmd: 'codex --version',
|
|
@@ -50,7 +51,7 @@ function createBuiltinTools(home, project) {
|
|
|
50
51
|
opencode: {
|
|
51
52
|
id: 'opencode',
|
|
52
53
|
name: 'OpenCode',
|
|
53
|
-
|
|
54
|
+
install: { type: 'npm', cmd: 'opencode-ai' },
|
|
54
55
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
|
|
55
56
|
containerMount: '/home/devuser/.local/share/opencode',
|
|
56
57
|
versionCmd: 'opencode version',
|
|
@@ -69,7 +70,7 @@ function createBuiltinTools(home, project) {
|
|
|
69
70
|
'gemini-cli': {
|
|
70
71
|
id: 'gemini-cli',
|
|
71
72
|
name: 'Gemini CLI',
|
|
72
|
-
|
|
73
|
+
install: { type: 'npm', cmd: '@google/gemini-cli' },
|
|
73
74
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
|
|
74
75
|
containerMount: '/home/devuser/.gemini',
|
|
75
76
|
versionCmd: 'gemini --version',
|
|
@@ -84,15 +85,200 @@ function createBuiltinTools(home, project) {
|
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
}
|
|
88
|
+
export function builtinToolIds() {
|
|
89
|
+
return Object.keys(createBuiltinTools('', ''));
|
|
90
|
+
}
|
|
87
91
|
function validateTool(tool) {
|
|
88
|
-
if (!tool.
|
|
89
|
-
throw new Error(`Invalid sandbox tool
|
|
92
|
+
if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
|
|
93
|
+
throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
|
|
94
|
+
}
|
|
95
|
+
if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
|
|
96
|
+
throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
|
|
97
|
+
}
|
|
98
|
+
if (!tool.install.cmd) {
|
|
99
|
+
throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
|
|
100
|
+
}
|
|
101
|
+
if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
|
|
102
|
+
throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isPlainObject(value) {
|
|
106
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
function asString(value, field, context) {
|
|
109
|
+
if (typeof value !== 'string') {
|
|
110
|
+
throw new Error(`${context}: field "${field}" must be a string`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function asOptionalNonEmptyString(value, field, context) {
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value !== 'string') {
|
|
119
|
+
throw new Error(`${context}: field "${field}" must be a string when provided`);
|
|
120
|
+
}
|
|
121
|
+
if (value.length === 0) {
|
|
122
|
+
throw new Error(`${context}: field "${field}" must be non-empty when provided`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function asStringRecord(value, field, context) {
|
|
127
|
+
if (value === undefined) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (!isPlainObject(value)) {
|
|
131
|
+
throw new Error(`${context}: field "${field}" must be an object when provided`);
|
|
132
|
+
}
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [key, val] of Object.entries(value)) {
|
|
135
|
+
if (typeof val !== 'string') {
|
|
136
|
+
throw new Error(`${context}: field "${field}.${key}" must be a string`);
|
|
137
|
+
}
|
|
138
|
+
out[key] = val;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function asStringArray(value, field, context) {
|
|
143
|
+
if (value === undefined) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(value)) {
|
|
147
|
+
throw new Error(`${context}: field "${field}" must be an array when provided`);
|
|
148
|
+
}
|
|
149
|
+
return value.map((item, index) => {
|
|
150
|
+
if (typeof item !== 'string') {
|
|
151
|
+
throw new Error(`${context}: field "${field}[${index}]" must be a string`);
|
|
152
|
+
}
|
|
153
|
+
return item;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function parseInstall(value, context) {
|
|
157
|
+
if (!isPlainObject(value)) {
|
|
158
|
+
throw new Error(`${context}: field "install" must be an object`);
|
|
159
|
+
}
|
|
160
|
+
const type = value.type;
|
|
161
|
+
if (type !== 'npm' && type !== 'shell') {
|
|
162
|
+
throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
|
|
163
|
+
}
|
|
164
|
+
const cmd = asString(value.cmd, 'install.cmd', context);
|
|
165
|
+
if (!cmd) {
|
|
166
|
+
throw new Error(`${context}: field "install.cmd" must be non-empty`);
|
|
167
|
+
}
|
|
168
|
+
return { type, cmd };
|
|
169
|
+
}
|
|
170
|
+
function parseHostPreSeedFiles(value, context) {
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(value)) {
|
|
175
|
+
throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
|
|
90
176
|
}
|
|
177
|
+
return value.map((item, index) => {
|
|
178
|
+
if (!isPlainObject(item)) {
|
|
179
|
+
throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
|
|
183
|
+
sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function parseHostPreSeedDirs(value, context) {
|
|
188
|
+
if (value === undefined) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if (!Array.isArray(value)) {
|
|
192
|
+
throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
|
|
193
|
+
}
|
|
194
|
+
return value.map((item, index) => {
|
|
195
|
+
if (!isPlainObject(item)) {
|
|
196
|
+
throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
|
|
200
|
+
sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function parseHostLiveMounts(value, context) {
|
|
205
|
+
if (value === undefined) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
|
|
210
|
+
}
|
|
211
|
+
return value.map((item, index) => {
|
|
212
|
+
if (!isPlainObject(item)) {
|
|
213
|
+
throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
|
|
217
|
+
containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function parseCustomTool(entry, index, options) {
|
|
222
|
+
const context = `customTools[${index}]`;
|
|
223
|
+
if (!isPlainObject(entry)) {
|
|
224
|
+
throw new Error(`${context} must be an object`);
|
|
225
|
+
}
|
|
226
|
+
const id = asString(entry.id, 'id', context);
|
|
227
|
+
if (!TOOL_ID_PATTERN.test(id)) {
|
|
228
|
+
throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
|
|
229
|
+
}
|
|
230
|
+
const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
|
|
231
|
+
?? `/home/devuser/.${id}`;
|
|
232
|
+
if (!containerMount.startsWith('/')) {
|
|
233
|
+
throw new Error(`${context}: field "containerMount" must be an absolute path`);
|
|
234
|
+
}
|
|
235
|
+
const tool = {
|
|
236
|
+
id,
|
|
237
|
+
name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
|
|
238
|
+
install: parseInstall(entry.install, context),
|
|
239
|
+
sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
|
|
240
|
+
containerMount,
|
|
241
|
+
versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
|
|
242
|
+
setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
|
|
243
|
+
?? `Run \`${id}\` inside the container to set up.`,
|
|
244
|
+
envVars: asStringRecord(entry.envVars, 'envVars', context),
|
|
245
|
+
hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
|
|
246
|
+
hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
|
|
247
|
+
pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
|
|
248
|
+
hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
|
|
249
|
+
postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
|
|
250
|
+
};
|
|
251
|
+
validateTool(tool);
|
|
252
|
+
return tool;
|
|
253
|
+
}
|
|
254
|
+
export function parseCustomTools(value, options) {
|
|
255
|
+
if (value === undefined || value === null) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
if (!Array.isArray(value)) {
|
|
259
|
+
throw new Error('sandbox: "customTools" must be an array');
|
|
260
|
+
}
|
|
261
|
+
return value.map((entry, index) => parseCustomTool(entry, index, options));
|
|
91
262
|
}
|
|
92
263
|
export function resolveTools(config) {
|
|
93
264
|
const builtins = createBuiltinTools(config.home, config.project);
|
|
265
|
+
const customs = config.customTools ?? [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
for (const tool of customs) {
|
|
268
|
+
if (builtins[tool.id]) {
|
|
269
|
+
throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
|
|
270
|
+
}
|
|
271
|
+
if (seen.has(tool.id)) {
|
|
272
|
+
throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
|
|
273
|
+
}
|
|
274
|
+
seen.add(tool.id);
|
|
275
|
+
}
|
|
276
|
+
const merged = { ...builtins };
|
|
277
|
+
for (const tool of customs) {
|
|
278
|
+
merged[tool.id] = tool;
|
|
279
|
+
}
|
|
94
280
|
return config.tools.map((id) => {
|
|
95
|
-
const tool =
|
|
281
|
+
const tool = merged[id];
|
|
96
282
|
if (!tool) {
|
|
97
283
|
throw new Error(`Unknown sandbox tool: ${id}`);
|
|
98
284
|
}
|
|
@@ -110,6 +296,25 @@ export function toolProjectDirCandidates(tool, project) {
|
|
|
110
296
|
return [hostJoin(tool.sandboxBase, project)];
|
|
111
297
|
}
|
|
112
298
|
export function toolNpmPackagesArg(tools) {
|
|
113
|
-
return tools
|
|
299
|
+
return tools
|
|
300
|
+
.filter((tool) => tool.install.type === 'npm')
|
|
301
|
+
.map((tool) => tool.install.cmd)
|
|
302
|
+
.join(' ');
|
|
303
|
+
}
|
|
304
|
+
export function toolShellInstallScript(tools) {
|
|
305
|
+
const blocks = tools
|
|
306
|
+
.filter((tool) => tool.install.type === 'shell')
|
|
307
|
+
.map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
|
|
308
|
+
if (blocks.length === 0) {
|
|
309
|
+
return '';
|
|
310
|
+
}
|
|
311
|
+
return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
|
|
312
|
+
}
|
|
313
|
+
export function toolShellInstallScriptBase64(tools) {
|
|
314
|
+
const script = toolShellInstallScript(tools);
|
|
315
|
+
return script ? Buffer.from(script, 'utf8').toString('base64') : '';
|
|
316
|
+
}
|
|
317
|
+
export function imageSignatureFields(tools) {
|
|
318
|
+
return tools.map((tool) => ({ id: tool.id, install: tool.install }));
|
|
114
319
|
}
|
|
115
320
|
//# sourceMappingURL=tools.js.map
|
package/dist/lib/update.js
CHANGED
|
@@ -121,6 +121,7 @@ async function cmdUpdate() {
|
|
|
121
121
|
const platformAdded = !config.platform;
|
|
122
122
|
const sandboxAdded = !config.sandbox;
|
|
123
123
|
const labelsAdded = !config.labels;
|
|
124
|
+
const requiresPullRequestAdded = config.requiresPullRequest === undefined;
|
|
124
125
|
let configChanged = changed;
|
|
125
126
|
if (platformAdded) {
|
|
126
127
|
config.platform = structuredClone(defaults.platform);
|
|
@@ -134,6 +135,10 @@ async function cmdUpdate() {
|
|
|
134
135
|
config.labels = structuredClone(defaults.labels);
|
|
135
136
|
configChanged = true;
|
|
136
137
|
}
|
|
138
|
+
if (requiresPullRequestAdded) {
|
|
139
|
+
config.requiresPullRequest = defaults.requiresPullRequest;
|
|
140
|
+
configChanged = true;
|
|
141
|
+
}
|
|
137
142
|
if (configChanged) {
|
|
138
143
|
console.log('');
|
|
139
144
|
if (hasNewEntries) {
|
|
@@ -145,7 +150,7 @@ async function cmdUpdate() {
|
|
|
145
150
|
ok(` merged: ${entry}`);
|
|
146
151
|
}
|
|
147
152
|
}
|
|
148
|
-
else if (platformAdded || sandboxAdded || labelsAdded) {
|
|
153
|
+
else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
|
|
149
154
|
if (platformAdded) {
|
|
150
155
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
151
156
|
}
|
|
@@ -155,6 +160,9 @@ async function cmdUpdate() {
|
|
|
155
160
|
if (labelsAdded) {
|
|
156
161
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
157
162
|
}
|
|
163
|
+
if (requiresPullRequestAdded) {
|
|
164
|
+
info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
|
|
165
|
+
}
|
|
158
166
|
}
|
|
159
167
|
else {
|
|
160
168
|
info(`File registry changed in ${CONFIG_PATH}.`);
|
|
@@ -168,6 +176,9 @@ async function cmdUpdate() {
|
|
|
168
176
|
if (hasNewEntries && platformAdded) {
|
|
169
177
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
170
178
|
}
|
|
179
|
+
if (hasNewEntries && requiresPullRequestAdded) {
|
|
180
|
+
info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
|
|
181
|
+
}
|
|
171
182
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
172
183
|
ok(`Updated ${CONFIG_PATH}`);
|
|
173
184
|
}
|
package/lib/defaults.json
CHANGED
package/lib/init.ts
CHANGED
|
@@ -24,6 +24,7 @@ type Defaults = {
|
|
|
24
24
|
files: FileRegistry;
|
|
25
25
|
sandbox: Record<string, unknown>;
|
|
26
26
|
labels: Record<string, unknown>;
|
|
27
|
+
requiresPullRequest: boolean;
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
type AgentConfig = {
|
|
@@ -31,6 +32,7 @@ type AgentConfig = {
|
|
|
31
32
|
org: string;
|
|
32
33
|
language: string;
|
|
33
34
|
platform: { type: string };
|
|
35
|
+
requiresPullRequest: boolean;
|
|
34
36
|
templateVersion: string;
|
|
35
37
|
sandbox: Record<string, unknown>;
|
|
36
38
|
labels: Record<string, unknown>;
|
|
@@ -207,6 +209,13 @@ async function cmdInit(): Promise<void> {
|
|
|
207
209
|
);
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
const requiresPRChoice = await select(
|
|
213
|
+
'Require Pull Request flow?',
|
|
214
|
+
['yes', 'no'],
|
|
215
|
+
'yes'
|
|
216
|
+
);
|
|
217
|
+
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
218
|
+
|
|
210
219
|
const templateSources = parseLocalSources(await prompt(
|
|
211
220
|
'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
|
|
212
221
|
''
|
|
@@ -280,6 +289,7 @@ async function cmdInit(): Promise<void> {
|
|
|
280
289
|
org: orgName,
|
|
281
290
|
language,
|
|
282
291
|
platform: { type: platformType },
|
|
292
|
+
requiresPullRequest,
|
|
283
293
|
templateVersion: VERSION,
|
|
284
294
|
sandbox: structuredClone(defaults.sandbox),
|
|
285
295
|
labels: structuredClone(defaults.labels),
|
|
@@ -36,7 +36,13 @@ import {
|
|
|
36
36
|
runVerboseEngine
|
|
37
37
|
} from '../shell.ts';
|
|
38
38
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
imageSignatureFields,
|
|
41
|
+
resolveTools,
|
|
42
|
+
toolConfigDirCandidates,
|
|
43
|
+
toolNpmPackagesArg,
|
|
44
|
+
toolShellInstallScriptBase64
|
|
45
|
+
} from '../tools.ts';
|
|
40
46
|
import type { SandboxTool } from '../tools.ts';
|
|
41
47
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
42
48
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
@@ -113,7 +119,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
|
|
|
113
119
|
return createHash('sha256')
|
|
114
120
|
.update(JSON.stringify({
|
|
115
121
|
dockerfile: preparedDockerfile.signature,
|
|
116
|
-
tools: tools
|
|
122
|
+
tools: imageSignatureFields(tools)
|
|
117
123
|
}))
|
|
118
124
|
.digest('hex')
|
|
119
125
|
.slice(0, 12);
|
|
@@ -1063,6 +1069,8 @@ export function buildImage(
|
|
|
1063
1069
|
`HOST_GID=${hostGid}`,
|
|
1064
1070
|
'--build-arg',
|
|
1065
1071
|
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
1072
|
+
'--build-arg',
|
|
1073
|
+
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
1066
1074
|
'--label',
|
|
1067
1075
|
sandboxLabel(config),
|
|
1068
1076
|
'--label',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from '../config.ts';
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
3
3
|
import { detectEngine } from '../engine.ts';
|
|
4
4
|
import {
|
|
5
5
|
formatCredentialWarnings,
|
|
@@ -13,8 +13,12 @@ import { resolveTaskBranch } from '../task-resolver.ts';
|
|
|
13
13
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
14
14
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
|
|
15
15
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
16
|
+
import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from './list-running.ts';
|
|
16
17
|
|
|
17
|
-
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]
|
|
18
|
+
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
|
|
19
|
+
|
|
20
|
+
'#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
|
|
21
|
+
Quote it as '#N' to avoid shell '#' comment handling.`;
|
|
18
22
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
19
23
|
|
|
20
24
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
@@ -115,8 +119,14 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
115
119
|
const config = loadConfig();
|
|
116
120
|
validateClaudeCredentialsEnvOverride();
|
|
117
121
|
const engine = detectEngine(config);
|
|
118
|
-
const [
|
|
119
|
-
|
|
122
|
+
const [firstArg = '', ...cmd] = args;
|
|
123
|
+
let branch: string;
|
|
124
|
+
if (isTaskShortRef(firstArg)) {
|
|
125
|
+
const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
126
|
+
branch = resolveTaskShortRef(firstArg, { running });
|
|
127
|
+
} else {
|
|
128
|
+
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
129
|
+
}
|
|
120
130
|
assertValidBranchName(branch);
|
|
121
131
|
const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
122
132
|
const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { runSafeEngine } from '../shell.ts';
|
|
2
|
+
|
|
3
|
+
export type SandboxRow = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
running: boolean;
|
|
8
|
+
index: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function containerListFormat(): string {
|
|
12
|
+
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseLabels(csv: string): Record<string, string> {
|
|
16
|
+
if (!csv) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const labels: Record<string, string> = {};
|
|
21
|
+
for (const pair of csv.split(',')) {
|
|
22
|
+
if (!pair) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const eq = pair.indexOf('=');
|
|
26
|
+
if (eq < 0) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
labels[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
30
|
+
}
|
|
31
|
+
return labels;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseSandboxRows(rawOutput: string, branchKey: string): SandboxRow[] {
|
|
35
|
+
if (!rawOutput) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return rawOutput.split('\n').map((line) => {
|
|
39
|
+
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
40
|
+
const branch = parseLabels(labelsCsv)[branchKey] ?? '';
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
status,
|
|
44
|
+
branch,
|
|
45
|
+
running: status.startsWith('Up '),
|
|
46
|
+
index: null
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
|
|
52
|
+
running: SandboxRow[];
|
|
53
|
+
nonRunning: SandboxRow[];
|
|
54
|
+
} {
|
|
55
|
+
const byName = (a: SandboxRow, b: SandboxRow): number => {
|
|
56
|
+
if (a.name < b.name) return -1;
|
|
57
|
+
if (a.name > b.name) return 1;
|
|
58
|
+
return 0;
|
|
59
|
+
};
|
|
60
|
+
const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
|
|
61
|
+
...row,
|
|
62
|
+
index: i + 1
|
|
63
|
+
}));
|
|
64
|
+
const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
|
|
65
|
+
...row,
|
|
66
|
+
index: null
|
|
67
|
+
}));
|
|
68
|
+
return { running, nonRunning };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function fetchSandboxRows(
|
|
72
|
+
engine: string,
|
|
73
|
+
label: string,
|
|
74
|
+
branchKey: string
|
|
75
|
+
): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
|
|
76
|
+
const raw = runSafeEngine(engine, 'docker', [
|
|
77
|
+
'ps',
|
|
78
|
+
'-a',
|
|
79
|
+
'--filter',
|
|
80
|
+
`label=${label}`,
|
|
81
|
+
'--format',
|
|
82
|
+
containerListFormat()
|
|
83
|
+
]);
|
|
84
|
+
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true iff `arg` is a syntactically valid task short reference ('#N').
|
|
89
|
+
* Zero IO. Callers MUST use this as the gate before constructing any context
|
|
90
|
+
* for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
|
|
91
|
+
* '#1.5', '#') never trigger sandbox list IO.
|
|
92
|
+
*/
|
|
93
|
+
export function isTaskShortRef(arg: string): boolean {
|
|
94
|
+
return /^#\d+$/.test(arg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a task short reference ('#N') to a branch name.
|
|
99
|
+
*
|
|
100
|
+
* Current implementation: treats the digits as a 1-based index into the
|
|
101
|
+
* supplied running-sandbox list (ls view order). This is the *only*
|
|
102
|
+
* resolution path until the global task-short-id registry lands in a
|
|
103
|
+
* follow-up task; do NOT read task.md or scan .agents/workspace/ from this
|
|
104
|
+
* helper here.
|
|
105
|
+
*
|
|
106
|
+
* Precondition: callers MUST gate on isTaskShortRef(arg) === true before
|
|
107
|
+
* constructing ctx and calling this function. Throws when arg is a valid
|
|
108
|
+
* short ref but cannot be resolved (out of range, no running sandboxes,
|
|
109
|
+
* etc.); the caller surfaces the error to the user.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveTaskShortRef(
|
|
112
|
+
arg: string,
|
|
113
|
+
ctx: { running: SandboxRow[] }
|
|
114
|
+
): string {
|
|
115
|
+
const n = Number(arg.slice(1));
|
|
116
|
+
if (n < 1) {
|
|
117
|
+
throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
|
|
118
|
+
}
|
|
119
|
+
const { running } = ctx;
|
|
120
|
+
if (running.length === 0) {
|
|
121
|
+
throw new Error(`No running sandbox to reference with '${arg}'`);
|
|
122
|
+
}
|
|
123
|
+
if (n > running.length) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`No running sandbox at index '${arg}' (only ${running.length} running)`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const row = running[n - 1]!;
|
|
129
|
+
if (!row.branch) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return row.branch;
|
|
135
|
+
}
|