@fitlab-ai/agent-infra 0.6.4 → 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 +63 -27
- package/README.zh-CN.md +61 -25
- package/bin/cli.ts +18 -6
- package/dist/bin/cli.js +20 -6
- package/dist/lib/cp.js +127 -0
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +3 -0
- package/dist/lib/sandbox/clipboard/bridge.js +23 -4
- package/dist/lib/sandbox/clipboard/index.js +12 -3
- package/dist/lib/sandbox/commands/create.js +11 -2
- package/dist/lib/sandbox/commands/enter.js +29 -6
- 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 +15 -7
- package/dist/lib/sandbox/config.js +3 -0
- package/dist/lib/sandbox/index.js +6 -4
- package/dist/lib/sandbox/readme-scaffold.js +148 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
- package/dist/lib/sandbox/tools.js +213 -8
- package/dist/lib/update.js +12 -1
- package/lib/cp.ts +177 -0
- package/lib/defaults.json +1 -0
- package/lib/init.ts +10 -0
- package/lib/sandbox/clipboard/bridge.ts +23 -4
- package/lib/sandbox/clipboard/index.ts +12 -3
- package/lib/sandbox/commands/create.ts +18 -2
- package/lib/sandbox/commands/enter.ts +48 -6
- package/lib/sandbox/commands/list-running.ts +135 -0
- package/lib/sandbox/commands/ls.ts +28 -49
- package/lib/sandbox/commands/rebuild.ts +24 -7
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/index.ts +6 -4
- package/lib/sandbox/readme-scaffold.ts +177 -0
- package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/lib/sandbox/runtimes/base.dockerfile +3 -3
- 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
package/dist/lib/cp.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { createClipboardAdapter } from "./sandbox/clipboard/index.js";
|
|
8
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS NSPasteboard over ssh/scp.\n';
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
|
+
export function runCommand(cmd, args, input) {
|
|
11
|
+
const result = spawnSync(cmd, args, {
|
|
12
|
+
input,
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
15
|
+
timeout: COMMAND_TIMEOUT_MS
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
status: result.status,
|
|
19
|
+
stdout: result.stdout ?? '',
|
|
20
|
+
stderr: result.stderr ?? '',
|
|
21
|
+
error: result.error
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function cmdCp(args, deps = {}) {
|
|
25
|
+
const { platform = currentPlatform(), createAdapter = createClipboardAdapter, spawnFn = runCommand, randomId = randomUUID, mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, rmFn = fs.rmSync, tmpdir = defaultTmpdir, writeStdout = (chunk) => process.stdout.write(chunk), writeStderr = (chunk) => process.stderr.write(chunk) } = deps;
|
|
26
|
+
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
27
|
+
writeStdout(USAGE);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let positionals;
|
|
31
|
+
try {
|
|
32
|
+
({ positionals } = parseArgs({ args, allowPositionals: true, strict: true }));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
writeStderr(USAGE);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const alias = positionals[0];
|
|
39
|
+
if (!alias || positionals.length !== 1) {
|
|
40
|
+
writeStderr(USAGE);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (alias.startsWith('-')) {
|
|
44
|
+
writeStderr(`invalid ssh alias '${alias}': must not start with '-'\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (platform !== 'darwin') {
|
|
48
|
+
writeStderr(`ai cp currently supports macOS senders only (got ${platform})\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const adapter = createAdapter({ platformName: platform });
|
|
52
|
+
const png = adapter?.readImagePng() ?? null;
|
|
53
|
+
if (png === null) {
|
|
54
|
+
writeStderr('no image on clipboard\n');
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
let uploaded = false;
|
|
58
|
+
let localTmpDir = null;
|
|
59
|
+
let remotePath = null;
|
|
60
|
+
try {
|
|
61
|
+
localTmpDir = mkdtempFn(path.join(tmpdir(), 'agent-infra-cp-'));
|
|
62
|
+
const localPng = path.join(localTmpDir, 'clipboard.png');
|
|
63
|
+
writeFileFn(localPng, png);
|
|
64
|
+
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
65
|
+
const upload = spawnFn('scp', [
|
|
66
|
+
'-o',
|
|
67
|
+
'BatchMode=yes',
|
|
68
|
+
'-o',
|
|
69
|
+
'ConnectTimeout=10',
|
|
70
|
+
localPng,
|
|
71
|
+
`${alias}:${remotePath}`
|
|
72
|
+
]);
|
|
73
|
+
if (upload.status !== 0) {
|
|
74
|
+
writeStderr(`failed to upload image to ${alias}:\n${commandDetail(upload)}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
uploaded = true;
|
|
78
|
+
// Remote write currently targets macOS only: it pipes an AppleScript to the
|
|
79
|
+
// remote `osascript` to set its NSPasteboard. This is the extension point for
|
|
80
|
+
// other remote platforms later (e.g. dispatch on remote OS to wl-copy/xclip
|
|
81
|
+
// on Linux); a non-macOS remote fails here with a clear non-zero error today.
|
|
82
|
+
const setRemote = spawnFn('ssh', [
|
|
83
|
+
'-o',
|
|
84
|
+
'BatchMode=yes',
|
|
85
|
+
'-o',
|
|
86
|
+
'ConnectTimeout=10',
|
|
87
|
+
alias,
|
|
88
|
+
'osascript',
|
|
89
|
+
'-'
|
|
90
|
+
], remoteSetScript(remotePath));
|
|
91
|
+
if (setRemote.status !== 0) {
|
|
92
|
+
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
writeStdout(`copied clipboard image to ${alias}\n`);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (uploaded && remotePath) {
|
|
100
|
+
spawnFn('ssh', [
|
|
101
|
+
'-o',
|
|
102
|
+
'BatchMode=yes',
|
|
103
|
+
'-o',
|
|
104
|
+
'ConnectTimeout=10',
|
|
105
|
+
alias,
|
|
106
|
+
'rm',
|
|
107
|
+
'-f',
|
|
108
|
+
remotePath
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
if (localTmpDir) {
|
|
112
|
+
rmFn(localTmpDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function commandDetail(result) {
|
|
117
|
+
const detail = result.stderr || result.error?.message || result.stdout || 'unknown error';
|
|
118
|
+
return detail.trimEnd();
|
|
119
|
+
}
|
|
120
|
+
function remoteSetScript(remotePath) {
|
|
121
|
+
const escapedPath = remotePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
122
|
+
return [
|
|
123
|
+
`set theFile to POSIX file "${escapedPath}"`,
|
|
124
|
+
'set the clipboard to (read theFile as «class PNGf»)'
|
|
125
|
+
].join('\n');
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=cp.js.map
|
package/dist/lib/defaults.json
CHANGED
package/dist/lib/init.js
CHANGED
|
@@ -156,6 +156,8 @@ async function cmdInit() {
|
|
|
156
156
|
info(`Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
|
|
157
157
|
+ ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`);
|
|
158
158
|
}
|
|
159
|
+
const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
|
|
160
|
+
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
159
161
|
const templateSources = parseLocalSources(await prompt('Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)', ''));
|
|
160
162
|
const skillSources = parseLocalSources(await prompt('Skill sources (optional, comma-separated local paths, e.g. ~/my-skills; Enter to skip)', ''));
|
|
161
163
|
closePrompt();
|
|
@@ -199,6 +201,7 @@ async function cmdInit() {
|
|
|
199
201
|
org: orgName,
|
|
200
202
|
language,
|
|
201
203
|
platform: { type: platformType },
|
|
204
|
+
requiresPullRequest,
|
|
202
205
|
templateVersion: VERSION,
|
|
203
206
|
sandbox: structuredClone(defaults.sandbox),
|
|
204
207
|
labels: structuredClone(defaults.labels),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { createClipboardAdapter } from "./index.js";
|
|
3
4
|
import { buildBracketedPaste, CtrlVDetector } from "./keys.js";
|
|
4
5
|
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from "./paths.js";
|
|
@@ -6,20 +7,37 @@ import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine }
|
|
|
6
7
|
import { loadNodePty } from "./node-pty.js";
|
|
7
8
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
8
9
|
const PARTIAL_ESCAPE_FLUSH_MS = 30;
|
|
10
|
+
// Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
|
|
11
|
+
// cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
|
|
12
|
+
// set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
|
|
13
|
+
// that tmux emits after homing the cursor inside the right pane into \r\n,
|
|
14
|
+
// snapping the cursor to column 1 so the following erase/redraw wipes the left
|
|
15
|
+
// pane. Clearing OPOST brings the host TTY in line with the non-bridge path.
|
|
16
|
+
// Best-effort: setRawMode(false) on teardown restores the original termios, and
|
|
17
|
+
// a missing/failed stty only reinstates the redraw glitch.
|
|
18
|
+
function disableOutputPostProcessing(stdin) {
|
|
19
|
+
const candidate = stdin.fd;
|
|
20
|
+
if (typeof candidate !== 'number') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
spawnSync('stty', ['-opost'], { stdio: [candidate, 'ignore', 'ignore'] });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// stty unavailable or fd is not a tty; leave the terminal as-is.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
9
30
|
export async function runInteractiveWithClipboardBridge(options) {
|
|
10
31
|
const { engine, dockerArgs, container, home, cwd = process.cwd(), env = process.env, platformName = process.platform, adapter = createClipboardAdapter({ platformName }), loadPty = loadNodePty, runInteractive = runInteractiveEngine, runOk = runOkEngine, writeStderr = (chunk) => process.stderr.write(chunk), stdin = process.stdin, stdout = process.stdout, createDetector = () => new CtrlVDetector() } = options;
|
|
11
32
|
function fallback(reason) {
|
|
12
33
|
writeStderr(`${FALLBACK_PREFIX}: ${reason}\n`);
|
|
13
34
|
return runInteractive(engine, 'docker', dockerArgs);
|
|
14
35
|
}
|
|
15
|
-
if (platformName !== 'darwin') {
|
|
16
|
-
return runInteractive(engine, 'docker', dockerArgs);
|
|
17
|
-
}
|
|
18
36
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
19
37
|
return fallback('host stdin/stdout is not a TTY');
|
|
20
38
|
}
|
|
21
39
|
if (!adapter) {
|
|
22
|
-
return fallback('
|
|
40
|
+
return fallback('no clipboard adapter available on this platform');
|
|
23
41
|
}
|
|
24
42
|
const available = adapter.available();
|
|
25
43
|
if (!available.ok) {
|
|
@@ -121,6 +139,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
121
139
|
}
|
|
122
140
|
try {
|
|
123
141
|
stdin.setRawMode?.(true);
|
|
142
|
+
disableOutputPostProcessing(stdin);
|
|
124
143
|
stdin.resume();
|
|
125
144
|
stdin.on('data', onData);
|
|
126
145
|
stdout.on('resize', onResize);
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { platform } from 'node:os';
|
|
2
2
|
import { createDarwinClipboardAdapter } from "./darwin.js";
|
|
3
3
|
export function createClipboardAdapter({ platformName = platform() } = {}) {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
switch (platformName) {
|
|
5
|
+
case 'darwin':
|
|
6
|
+
return createDarwinClipboardAdapter();
|
|
7
|
+
case 'linux':
|
|
8
|
+
// Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
|
|
9
|
+
// see Issue #386 follow-up. Returning null disables the bridge for now.
|
|
10
|
+
return null;
|
|
11
|
+
case 'win32':
|
|
12
|
+
// Future work: native Win32 clipboard reader. Returning null disables the bridge.
|
|
13
|
+
return null;
|
|
14
|
+
default:
|
|
15
|
+
return null;
|
|
6
16
|
}
|
|
7
|
-
return createDarwinClipboardAdapter();
|
|
8
17
|
}
|
|
9
18
|
//# sourceMappingURL=index.js.map
|
|
@@ -13,12 +13,13 @@ import { prepareDockerfile } from "../dockerfile.js";
|
|
|
13
13
|
import { detectEngine, ensureDocker } from "../engine.js";
|
|
14
14
|
import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from "../shell.js";
|
|
15
15
|
import { resolveTaskBranch } from "../task-resolver.js";
|
|
16
|
-
import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from "../tools.js";
|
|
16
|
+
import { imageSignatureFields, resolveTools, toolConfigDirCandidates, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
|
|
17
17
|
import { hostJoin, toEnginePath, volumeArg } from "../engines/wsl2-paths.js";
|
|
18
18
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from "../clipboard/paths.js";
|
|
19
19
|
import { validateSelinuxDisableEnv } from "../engines/selinux.js";
|
|
20
20
|
import { resolveBuildUid } from "../engines/native.js";
|
|
21
21
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
22
|
+
import { ensureSandboxDiscoveryReadmes } from "../readme-scaffold.js";
|
|
22
23
|
import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
23
24
|
import { detectHostTimezone } from "../host-timezone.js";
|
|
24
25
|
const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
|
|
@@ -57,7 +58,7 @@ function buildSignature(preparedDockerfile, tools) {
|
|
|
57
58
|
return createHash('sha256')
|
|
58
59
|
.update(JSON.stringify({
|
|
59
60
|
dockerfile: preparedDockerfile.signature,
|
|
60
|
-
tools: tools
|
|
61
|
+
tools: imageSignatureFields(tools)
|
|
61
62
|
}))
|
|
62
63
|
.digest('hex')
|
|
63
64
|
.slice(0, 12);
|
|
@@ -788,6 +789,8 @@ export function buildImage(config, tools, dockerfilePath, imageSignature, { engi
|
|
|
788
789
|
`HOST_GID=${hostGid}`,
|
|
789
790
|
'--build-arg',
|
|
790
791
|
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
792
|
+
'--build-arg',
|
|
793
|
+
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
791
794
|
'--label',
|
|
792
795
|
sandboxLabel(config),
|
|
793
796
|
'--label',
|
|
@@ -981,6 +984,12 @@ export async function create(args) {
|
|
|
981
984
|
if (aliasesFile.created) {
|
|
982
985
|
message(`Created default sandbox aliases at ${aliasesFile.path}`);
|
|
983
986
|
}
|
|
987
|
+
const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
|
|
988
|
+
for (const { created, path: readmePath } of readmeResults) {
|
|
989
|
+
if (created) {
|
|
990
|
+
message(`Created discovery README at ${readmePath}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
984
993
|
const gitconfigPath = path.join(effectiveConfig.home, '.gitconfig');
|
|
985
994
|
const gitconfigContent = fs.existsSync(gitconfigPath)
|
|
986
995
|
? fs.readFileSync(gitconfigPath, 'utf8')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from "../constants.js";
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
3
3
|
import { detectEngine } from "../engine.js";
|
|
4
4
|
import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
5
5
|
import { runInteractiveEngine, runSafeEngine } from "../shell.js";
|
|
@@ -7,7 +7,11 @@ import { resolveTaskBranch } from "../task-resolver.js";
|
|
|
7
7
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
8
8
|
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
9
9
|
import { detectHostTimezone } from "../host-timezone.js";
|
|
10
|
-
|
|
10
|
+
import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from "./list-running.js";
|
|
11
|
+
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
|
|
12
|
+
|
|
13
|
+
'#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
|
|
14
|
+
Quote it as '#N' to avoid shell '#' comment handling.`;
|
|
11
15
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
12
16
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
13
17
|
// inspect to enable progressive enhancements such as the kitty keyboard
|
|
@@ -34,6 +38,17 @@ export function hostTimezoneEnvFlags(detect = detectHostTimezone) {
|
|
|
34
38
|
const tz = detect();
|
|
35
39
|
return tz ? ['-e', `TZ=${tz}`] : [];
|
|
36
40
|
}
|
|
41
|
+
export function clipboardBridgeDisabled(env = process.env) {
|
|
42
|
+
const value = (env.AI_SANDBOX_NO_CLIPBOARD_BRIDGE ?? '').trim().toLowerCase();
|
|
43
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
44
|
+
}
|
|
45
|
+
export function runSandboxInteractive(params) {
|
|
46
|
+
const { engine, dockerArgs, container, home, env = process.env, runBridge = runInteractiveWithClipboardBridge, runInteractive = runInteractiveEngine } = params;
|
|
47
|
+
if (clipboardBridgeDisabled(env)) {
|
|
48
|
+
return runInteractive(engine, 'docker', dockerArgs);
|
|
49
|
+
}
|
|
50
|
+
return runBridge({ engine, dockerArgs, container, home });
|
|
51
|
+
}
|
|
37
52
|
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY) {
|
|
38
53
|
if (result.status === 'STALE_ACCESS') {
|
|
39
54
|
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
@@ -68,8 +83,15 @@ export async function enter(args) {
|
|
|
68
83
|
const config = loadConfig();
|
|
69
84
|
validateClaudeCredentialsEnvOverride();
|
|
70
85
|
const engine = detectEngine(config);
|
|
71
|
-
const [
|
|
72
|
-
|
|
86
|
+
const [firstArg = '', ...cmd] = args;
|
|
87
|
+
let branch;
|
|
88
|
+
if (isTaskShortRef(firstArg)) {
|
|
89
|
+
const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
90
|
+
branch = resolveTaskShortRef(firstArg, { running });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
94
|
+
}
|
|
73
95
|
assertValidBranchName(branch);
|
|
74
96
|
const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
75
97
|
const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
|
|
@@ -97,9 +119,10 @@ export async function enter(args) {
|
|
|
97
119
|
catch (error) {
|
|
98
120
|
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
99
121
|
}
|
|
100
|
-
|
|
122
|
+
const dockerArgs = ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH];
|
|
123
|
+
return runSandboxInteractive({
|
|
101
124
|
engine,
|
|
102
|
-
dockerArgs
|
|
125
|
+
dockerArgs,
|
|
103
126
|
container,
|
|
104
127
|
home: config.home
|
|
105
128
|
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { runSafeEngine } from "../shell.js";
|
|
2
|
+
export function containerListFormat() {
|
|
3
|
+
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
4
|
+
}
|
|
5
|
+
export function parseLabels(csv) {
|
|
6
|
+
if (!csv) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
const labels = {};
|
|
10
|
+
for (const pair of csv.split(',')) {
|
|
11
|
+
if (!pair) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const eq = pair.indexOf('=');
|
|
15
|
+
if (eq < 0) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
labels[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
19
|
+
}
|
|
20
|
+
return labels;
|
|
21
|
+
}
|
|
22
|
+
export function parseSandboxRows(rawOutput, branchKey) {
|
|
23
|
+
if (!rawOutput) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return rawOutput.split('\n').map((line) => {
|
|
27
|
+
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
28
|
+
const branch = parseLabels(labelsCsv)[branchKey] ?? '';
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
status,
|
|
32
|
+
branch,
|
|
33
|
+
running: status.startsWith('Up '),
|
|
34
|
+
index: null
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function sortAndIndexSandboxRows(rows) {
|
|
39
|
+
const byName = (a, b) => {
|
|
40
|
+
if (a.name < b.name)
|
|
41
|
+
return -1;
|
|
42
|
+
if (a.name > b.name)
|
|
43
|
+
return 1;
|
|
44
|
+
return 0;
|
|
45
|
+
};
|
|
46
|
+
const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
|
|
47
|
+
...row,
|
|
48
|
+
index: i + 1
|
|
49
|
+
}));
|
|
50
|
+
const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
|
|
51
|
+
...row,
|
|
52
|
+
index: null
|
|
53
|
+
}));
|
|
54
|
+
return { running, nonRunning };
|
|
55
|
+
}
|
|
56
|
+
export function fetchSandboxRows(engine, label, branchKey) {
|
|
57
|
+
const raw = runSafeEngine(engine, 'docker', [
|
|
58
|
+
'ps',
|
|
59
|
+
'-a',
|
|
60
|
+
'--filter',
|
|
61
|
+
`label=${label}`,
|
|
62
|
+
'--format',
|
|
63
|
+
containerListFormat()
|
|
64
|
+
]);
|
|
65
|
+
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns true iff `arg` is a syntactically valid task short reference ('#N').
|
|
69
|
+
* Zero IO. Callers MUST use this as the gate before constructing any context
|
|
70
|
+
* for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
|
|
71
|
+
* '#1.5', '#') never trigger sandbox list IO.
|
|
72
|
+
*/
|
|
73
|
+
export function isTaskShortRef(arg) {
|
|
74
|
+
return /^#\d+$/.test(arg);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a task short reference ('#N') to a branch name.
|
|
78
|
+
*
|
|
79
|
+
* Current implementation: treats the digits as a 1-based index into the
|
|
80
|
+
* supplied running-sandbox list (ls view order). This is the *only*
|
|
81
|
+
* resolution path until the global task-short-id registry lands in a
|
|
82
|
+
* follow-up task; do NOT read task.md or scan .agents/workspace/ from this
|
|
83
|
+
* helper here.
|
|
84
|
+
*
|
|
85
|
+
* Precondition: callers MUST gate on isTaskShortRef(arg) === true before
|
|
86
|
+
* constructing ctx and calling this function. Throws when arg is a valid
|
|
87
|
+
* short ref but cannot be resolved (out of range, no running sandboxes,
|
|
88
|
+
* etc.); the caller surfaces the error to the user.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveTaskShortRef(arg, ctx) {
|
|
91
|
+
const n = Number(arg.slice(1));
|
|
92
|
+
if (n < 1) {
|
|
93
|
+
throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
|
|
94
|
+
}
|
|
95
|
+
const { running } = ctx;
|
|
96
|
+
if (running.length === 0) {
|
|
97
|
+
throw new Error(`No running sandbox to reference with '${arg}'`);
|
|
98
|
+
}
|
|
99
|
+
if (n > running.length) {
|
|
100
|
+
throw new Error(`No running sandbox at index '${arg}' (only ${running.length} running)`);
|
|
101
|
+
}
|
|
102
|
+
const row = running[n - 1];
|
|
103
|
+
if (!row.branch) {
|
|
104
|
+
throw new Error(`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`);
|
|
105
|
+
}
|
|
106
|
+
return row.branch;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=list-running.js.map
|
|
@@ -5,39 +5,24 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
7
7
|
import { detectEngine } from "../engine.js";
|
|
8
|
-
import { runSafeEngine } from "../shell.js";
|
|
9
8
|
import { resolveTools, toolProjectDirCandidates } from "../tools.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
const labels = {};
|
|
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
|
-
}
|
|
9
|
+
import { fetchSandboxRows } from "./list-running.js";
|
|
10
|
+
export { containerListFormat, parseLabels } from "./list-running.js";
|
|
11
|
+
const USAGE = `Usage: ai sandbox ls
|
|
12
|
+
|
|
13
|
+
Lists all containers for the current project. The leftmost '#' column
|
|
14
|
+
numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
|
|
15
|
+
Quote '#N' to avoid shell '#' comment handling.`;
|
|
16
|
+
const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'];
|
|
33
17
|
export function formatContainerTable(rows) {
|
|
34
|
-
const columns = rows.map((row) => [row.name, row.status, row.branch]);
|
|
18
|
+
const columns = rows.map((row) => [row.index, row.name, row.status, row.branch]);
|
|
35
19
|
const widths = [
|
|
36
|
-
Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.
|
|
37
|
-
Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.
|
|
38
|
-
Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.
|
|
20
|
+
Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
|
|
21
|
+
Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
|
|
22
|
+
Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
|
|
23
|
+
Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
|
|
39
24
|
];
|
|
40
|
-
const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
|
|
25
|
+
const renderRow = (values) => `${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
|
|
41
26
|
return [
|
|
42
27
|
renderRow(CONTAINER_TABLE_HEADERS),
|
|
43
28
|
...columns.map((column) => renderRow(column))
|
|
@@ -58,27 +43,21 @@ export function ls(args = []) {
|
|
|
58
43
|
const engine = detectEngine(config);
|
|
59
44
|
const tools = resolveTools(config);
|
|
60
45
|
const label = sandboxLabel(config);
|
|
61
|
-
const
|
|
62
|
-
'ps',
|
|
63
|
-
'-a',
|
|
64
|
-
'--filter',
|
|
65
|
-
`label=${label}`,
|
|
66
|
-
'--format',
|
|
67
|
-
containerListFormat()
|
|
68
|
-
]);
|
|
46
|
+
const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
|
|
69
47
|
p.intro(pc.cyan(`Sandbox status for ${config.project}`));
|
|
70
48
|
p.log.step('Containers');
|
|
71
|
-
|
|
49
|
+
const ordered = [...running, ...nonRunning];
|
|
50
|
+
if (ordered.length === 0) {
|
|
72
51
|
p.log.warn(' No sandbox containers');
|
|
73
52
|
}
|
|
74
53
|
else {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
for (const line of formatContainerTable(
|
|
54
|
+
const tableRows = ordered.map((row) => ({
|
|
55
|
+
index: row.index === null ? '' : String(row.index),
|
|
56
|
+
name: row.name,
|
|
57
|
+
status: row.status,
|
|
58
|
+
branch: row.branch
|
|
59
|
+
}));
|
|
60
|
+
for (const line of formatContainerTable(tableRows)) {
|
|
82
61
|
process.stdout.write(` ${line}\n`);
|
|
83
62
|
}
|
|
84
63
|
}
|
|
@@ -7,20 +7,20 @@ 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
|
-
const USAGE = `Usage: ai sandbox rebuild [--quiet]`;
|
|
13
|
+
const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
|
|
14
14
|
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);
|
|
22
22
|
}
|
|
23
|
-
export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env } = {}) {
|
|
23
|
+
export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false } = {}) {
|
|
24
24
|
const selectedEngine = engine ?? detectEngine(config);
|
|
25
25
|
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
26
26
|
engine: selectedEngine,
|
|
@@ -28,7 +28,7 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
|
|
|
28
28
|
runSafeFn,
|
|
29
29
|
env
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
const args = [
|
|
32
32
|
'build',
|
|
33
33
|
'-t',
|
|
34
34
|
config.imageName,
|
|
@@ -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',
|
|
@@ -46,6 +48,10 @@ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engin
|
|
|
46
48
|
toEnginePath(selectedEngine, dockerfilePath),
|
|
47
49
|
toEnginePath(selectedEngine, config.repoRoot)
|
|
48
50
|
];
|
|
51
|
+
if (refresh) {
|
|
52
|
+
args.splice(1, 0, '--no-cache', '--pull');
|
|
53
|
+
}
|
|
54
|
+
return args;
|
|
49
55
|
}
|
|
50
56
|
function removeImageIfPresent(imageName, engine) {
|
|
51
57
|
if (runOkEngine(engine, 'docker', ['image', 'inspect', imageName])) {
|
|
@@ -58,6 +64,7 @@ export async function rebuild(args) {
|
|
|
58
64
|
allowPositionals: true,
|
|
59
65
|
strict: true,
|
|
60
66
|
options: {
|
|
67
|
+
refresh: { type: 'boolean' },
|
|
61
68
|
quiet: { type: 'boolean', short: 'q' },
|
|
62
69
|
help: { type: 'boolean', short: 'h' }
|
|
63
70
|
}
|
|
@@ -71,6 +78,7 @@ export async function rebuild(args) {
|
|
|
71
78
|
const preparedDockerfile = prepareDockerfile(config);
|
|
72
79
|
const imageSignature = buildSignature(preparedDockerfile, tools);
|
|
73
80
|
const quiet = values.quiet ?? false;
|
|
81
|
+
const refresh = values.refresh ?? false;
|
|
74
82
|
const engine = detectEngine(config);
|
|
75
83
|
await ensureDocker(config, undefined);
|
|
76
84
|
p.intro(pc.cyan('Rebuilding sandbox image'));
|
|
@@ -81,7 +89,7 @@ export async function rebuild(args) {
|
|
|
81
89
|
removeImageIfPresent(config.imageName, engine);
|
|
82
90
|
spinner.stop('Old image removed');
|
|
83
91
|
spinner.start('Building image...');
|
|
84
|
-
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine }), {
|
|
92
|
+
runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
|
|
85
93
|
cwd: config.repoRoot
|
|
86
94
|
});
|
|
87
95
|
spinner.stop(pc.green('Sandbox image rebuilt'));
|
|
@@ -90,7 +98,7 @@ export async function rebuild(args) {
|
|
|
90
98
|
p.log.step(`Removing old image ${config.imageName}`);
|
|
91
99
|
removeImageIfPresent(config.imageName, engine);
|
|
92
100
|
p.log.step('Building image');
|
|
93
|
-
runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine }), { cwd: config.repoRoot });
|
|
101
|
+
runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
|
|
94
102
|
p.log.success(pc.green('Sandbox image rebuilt'));
|
|
95
103
|
}
|
|
96
104
|
}
|
|
@@ -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,
|