@fitlab-ai/agent-infra 0.8.3 → 0.8.5
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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +9 -0
- package/dist/lib/init.js +5 -0
- package/dist/lib/sandbox/capture.js +5 -3
- package/dist/lib/sandbox/clipboard/bridge.js +12 -9
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/commands/create.js +68 -26
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/daemon.js +9 -0
- package/dist/lib/server/process-control.js +66 -73
- package/dist/lib/server/process-state.js +168 -0
- package/dist/lib/update.js +2 -0
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +9 -0
- package/lib/init.ts +9 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +14 -12
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/commands/create.ts +80 -27
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/daemon.ts +9 -0
- package/lib/server/process-control.ts +70 -71
- package/lib/server/process-state.ts +183 -0
- package/lib/update.ts +6 -0
- package/package.json +2 -1
- package/runtime/platform-adapters/platform-sync.github.js +1501 -0
- package/templates/.agents/README.en.md +5 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- 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/review-handshake.en.md +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- package/templates/.agents/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
- package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
- package/templates/.agents/scripts/validate-artifact.js +144 -38
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +6 -4
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +5 -4
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
package/README.zh-CN.md
CHANGED
|
@@ -207,3 +207,13 @@ my-project/
|
|
|
207
207
|
## 许可协议
|
|
208
208
|
|
|
209
209
|
[MIT](License.txt)
|
|
210
|
+
|
|
211
|
+
## 社区
|
|
212
|
+
|
|
213
|
+
- 微信公众号:`FitFramework`
|
|
214
|
+
- QQ 技术交流群:`1029802553`
|
|
215
|
+
|
|
216
|
+
<p align="center">
|
|
217
|
+
<img src="./assets/qrcode_for_wechat_gh.png" alt="FitFramework 微信公众号二维码" width="250">
|
|
218
|
+
<img src="./assets/qrcode_for_qq_01.png" alt="FitFramework QQ 技术交流群二维码" width="250">
|
|
219
|
+
</p>
|
package/bin/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
15
15
|
Usage: ai <command> [options]
|
|
16
16
|
|
|
17
17
|
Commands:
|
|
18
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
18
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
19
19
|
decide Record a human decision for a task HD item
|
|
20
20
|
help Show this help message
|
|
21
21
|
init Initialize a new project with update-agent-infra seed command
|
package/dist/bin/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructur
|
|
|
19
19
|
Usage: ai <command> [options]
|
|
20
20
|
|
|
21
21
|
Commands:
|
|
22
|
-
cp <ssh-alias> Copy local clipboard image to a remote macOS
|
|
22
|
+
cp <ssh-alias> Copy local clipboard image to a remote macOS clipboard or Linux sandbox over SSH
|
|
23
23
|
decide Record a human decision for a task HD item
|
|
24
24
|
help Show this help message
|
|
25
25
|
init Initialize a new project with update-agent-infra seed command
|
package/dist/lib/cp.js
CHANGED
|
@@ -5,7 +5,8 @@ import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { createClipboardAdapter } from './sandbox/clipboard/index.js';
|
|
8
|
-
|
|
8
|
+
import { RECEIVER_CAPABILITY, receiveRemoteClipboardPng } from './sandbox/clipboard/inbox.js';
|
|
9
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS clipboard or Linux sandbox over SSH.\n';
|
|
9
10
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
11
|
export function runCommand(cmd, args, input) {
|
|
11
12
|
const result = spawnSync(cmd, args, {
|
|
@@ -23,6 +24,22 @@ export function runCommand(cmd, args, input) {
|
|
|
23
24
|
}
|
|
24
25
|
export async function cmdCp(args, deps = {}) {
|
|
25
26
|
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;
|
|
27
|
+
if (args.length === 1 && args[0] === '--remote-receive-capability') {
|
|
28
|
+
if (platform === 'linux')
|
|
29
|
+
writeStdout(`${RECEIVER_CAPABILITY}\n`);
|
|
30
|
+
return platform === 'linux' ? 0 : 1;
|
|
31
|
+
}
|
|
32
|
+
if (args.length === 2 && args[0] === '--remote-receive-v1') {
|
|
33
|
+
try {
|
|
34
|
+
const result = receiveRemoteClipboardPng(process.env.HOME || process.env.USERPROFILE || '', args[1], platform);
|
|
35
|
+
writeStdout(`${result}\n`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
writeStderr(`${error instanceof Error ? error.message : 'clipboard receive failed'}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
27
44
|
writeStdout(USAGE);
|
|
28
45
|
return 0;
|
|
@@ -62,6 +79,34 @@ export async function cmdCp(args, deps = {}) {
|
|
|
62
79
|
const localPng = path.join(localTmpDir, 'clipboard.png');
|
|
63
80
|
writeFileFn(localPng, png);
|
|
64
81
|
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
82
|
+
const remoteSystem = spawnFn('ssh', [
|
|
83
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'uname', '-s'
|
|
84
|
+
]);
|
|
85
|
+
if (remoteSystem.status !== 0) {
|
|
86
|
+
writeStderr(`failed to detect remote platform on ${alias}:\n${commandDetail(remoteSystem)}\n`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const remotePlatform = remoteSystem.stdout.trim();
|
|
90
|
+
if (remotePlatform !== 'Darwin' && remotePlatform !== 'Linux') {
|
|
91
|
+
writeStderr(`unsupported remote platform on ${alias}: ${remotePlatform || 'unknown'}\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (remotePlatform === 'Linux') {
|
|
95
|
+
const homeCheck = spawnFn('ssh', [
|
|
96
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'test', '-d', '$HOME/.agent-infra'
|
|
97
|
+
]);
|
|
98
|
+
if (homeCheck.status !== 0) {
|
|
99
|
+
writeStderr(`remote agent-infra installation is missing on ${alias}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const capability = spawnFn('ssh', [
|
|
103
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-capability'
|
|
104
|
+
]);
|
|
105
|
+
if (capability.status !== 0 || capability.stderr !== '' || capability.stdout.replace(/\n$/u, '') !== RECEIVER_CAPABILITY) {
|
|
106
|
+
writeStderr(`remote agent-infra clipboard receiver is unavailable or incompatible on ${alias}; update agent-infra\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
65
110
|
const upload = spawnFn('scp', [
|
|
66
111
|
'-o',
|
|
67
112
|
'BatchMode=yes',
|
|
@@ -75,24 +120,20 @@ export async function cmdCp(args, deps = {}) {
|
|
|
75
120
|
return 1;
|
|
76
121
|
}
|
|
77
122
|
uploaded = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'-o',
|
|
86
|
-
'ConnectTimeout=10',
|
|
87
|
-
alias,
|
|
88
|
-
'osascript',
|
|
89
|
-
'-'
|
|
90
|
-
], remoteSetScript(remotePath));
|
|
123
|
+
const setRemote = remotePlatform === 'Darwin'
|
|
124
|
+
? spawnFn('ssh', [
|
|
125
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'osascript', '-'
|
|
126
|
+
], remoteSetScript(remotePath))
|
|
127
|
+
: spawnFn('ssh', [
|
|
128
|
+
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', alias, 'ai', 'cp', '--remote-receive-v1', remotePath
|
|
129
|
+
]);
|
|
91
130
|
if (setRemote.status !== 0) {
|
|
92
131
|
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
93
132
|
return 1;
|
|
94
133
|
}
|
|
95
|
-
writeStdout(
|
|
134
|
+
writeStdout(remotePlatform === 'Linux'
|
|
135
|
+
? `uploaded clipboard image to ${alias}; return to the SSH session and press Ctrl+V\n`
|
|
136
|
+
: `copied clipboard image to ${alias}\n`);
|
|
96
137
|
return 0;
|
|
97
138
|
}
|
|
98
139
|
finally {
|
package/dist/lib/defaults.json
CHANGED
|
@@ -44,14 +44,23 @@
|
|
|
44
44
|
".gemini/commands/",
|
|
45
45
|
".git-hooks/check-version-format.sh",
|
|
46
46
|
".github/scripts/",
|
|
47
|
+
".github/workflows/metadata-sync.yml",
|
|
48
|
+
".github/workflows/pr-label.yml",
|
|
49
|
+
".github/workflows/status-label.yml",
|
|
47
50
|
".opencode/commands/"
|
|
48
51
|
],
|
|
52
|
+
"guardedManaged": [
|
|
53
|
+
".github/workflows/metadata-sync.yml",
|
|
54
|
+
".github/workflows/pr-label.yml",
|
|
55
|
+
".github/workflows/status-label.yml"
|
|
56
|
+
],
|
|
49
57
|
"merged": [
|
|
50
58
|
"**/post-release.*",
|
|
51
59
|
"**/release.*",
|
|
52
60
|
"**/test-integration.*",
|
|
53
61
|
"**/test.*",
|
|
54
62
|
"**/upgrade-dependency.*",
|
|
63
|
+
".agents/rules/testing-discipline.*",
|
|
55
64
|
".agents/skills/post-release/SKILL.*",
|
|
56
65
|
".agents/skills/release/SKILL.*",
|
|
57
66
|
".agents/skills/test-integration/SKILL.*",
|
package/dist/lib/init.js
CHANGED
|
@@ -197,6 +197,8 @@ async function cmdInit() {
|
|
|
197
197
|
// install skill
|
|
198
198
|
copySkillDir(path.join(templateDir, '.agents', 'skills', 'update-agent-infra'), path.join('.agents', 'skills', 'update-agent-infra'), replacements, language, platformType);
|
|
199
199
|
ok('Installed .agents/skills/update-agent-infra/');
|
|
200
|
+
renderFile(path.join(templateDir, '.agents', 'scripts', 'lib', 'agent-infra-package.js'), path.join('.agents', 'scripts', 'lib', 'agent-infra-package.js'), replacements);
|
|
201
|
+
ok('Installed .agents/scripts/lib/agent-infra-package.js');
|
|
200
202
|
// install Claude command (only if enabled)
|
|
201
203
|
if (enabledTUISet.has('claude-code')) {
|
|
202
204
|
renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
|
|
@@ -245,6 +247,9 @@ async function cmdInit() {
|
|
|
245
247
|
console.log('');
|
|
246
248
|
ok('Project initialized successfully!');
|
|
247
249
|
console.log('');
|
|
250
|
+
console.log(' If this init used npx, install agent-infra persistently before update or validation:');
|
|
251
|
+
console.log(' npm install -g @fitlab-ai/agent-infra');
|
|
252
|
+
console.log('');
|
|
248
253
|
if (enabledTUISet.size === 0) {
|
|
249
254
|
console.log(' No built-in TUI selected.');
|
|
250
255
|
console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { loadConfig } from './config.js';
|
|
3
3
|
import { containerNameCandidates, sandboxBranchLabel, sandboxLabel } from './constants.js';
|
|
4
4
|
import { detectEngine } from './engine.js';
|
|
5
|
+
import { commandForEngine } from './shell.js';
|
|
5
6
|
import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.js';
|
|
6
7
|
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
7
8
|
async function spawnCapture(file, args) {
|
|
@@ -49,12 +50,12 @@ function buildRunScript(params) {
|
|
|
49
50
|
return `#!/bin/sh
|
|
50
51
|
set -u
|
|
51
52
|
cd /workspace
|
|
52
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
53
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/started_at
|
|
53
54
|
printf '%s\\n' running > ${runDir}/status
|
|
54
55
|
${command}
|
|
55
56
|
code=$?
|
|
56
57
|
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
57
|
-
date "+%Y-%m-%d %H:%M:%S
|
|
58
|
+
date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\\([+-][0-9][0-9]\\)\\([0-9][0-9]\\)$/\\1:\\2/' > ${runDir}/finished_at
|
|
58
59
|
if [ "$code" -eq 0 ]; then
|
|
59
60
|
printf '%s\\n' completed > ${runDir}/status
|
|
60
61
|
else
|
|
@@ -144,7 +145,8 @@ export async function runInSandbox(request, options = {}) {
|
|
|
144
145
|
'-lc',
|
|
145
146
|
buildTmuxLauncher({ request, runId })
|
|
146
147
|
];
|
|
147
|
-
const
|
|
148
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
149
|
+
const result = await (options.spawn ?? spawnCapture)(command.cmd, command.args);
|
|
148
150
|
return {
|
|
149
151
|
...result,
|
|
150
152
|
run: {
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { createClipboardAdapter } from './index.js';
|
|
4
4
|
import { buildBracketedPaste, CtrlVDetector } from './keys.js';
|
|
5
5
|
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
|
|
6
|
+
import { isClipboardInboxReadable, readPendingClipboardPath } from './inbox.js';
|
|
6
7
|
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from '../shell.js';
|
|
7
8
|
import { loadNodePty } from './node-pty.js';
|
|
8
9
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
@@ -37,13 +38,10 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
37
38
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
38
39
|
return fallback('host stdin/stdout is not a TTY');
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const available = adapter.available();
|
|
44
|
-
if (!available.ok) {
|
|
41
|
+
const available = adapter?.available() ?? { ok: false, reason: 'no clipboard adapter available on this platform' };
|
|
42
|
+
const inboxReady = isClipboardInboxReadable(home);
|
|
43
|
+
if (!available.ok && !inboxReady)
|
|
45
44
|
return fallback(available.reason);
|
|
46
|
-
}
|
|
47
45
|
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
48
46
|
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
49
47
|
}
|
|
@@ -69,7 +67,7 @@ export async function runInteractiveWithClipboardBridge(options) {
|
|
|
69
67
|
return runBridge({
|
|
70
68
|
child,
|
|
71
69
|
home,
|
|
72
|
-
adapter,
|
|
70
|
+
adapter: available.ok ? adapter : null,
|
|
73
71
|
writeStderr,
|
|
74
72
|
stdin,
|
|
75
73
|
stdout,
|
|
@@ -116,7 +114,7 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
116
114
|
function handleText(raw, target) {
|
|
117
115
|
const textAdapter = adapter;
|
|
118
116
|
const pastedText = extractSinglePathPaste(raw);
|
|
119
|
-
if (!textAdapter.readImageFromText || pastedText === null) {
|
|
117
|
+
if (!textAdapter || !textAdapter.readImageFromText || pastedText === null) {
|
|
120
118
|
target.write(raw);
|
|
121
119
|
return;
|
|
122
120
|
}
|
|
@@ -138,12 +136,17 @@ async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, det
|
|
|
138
136
|
}
|
|
139
137
|
function handleCtrlV(match, target) {
|
|
140
138
|
try {
|
|
139
|
+
const pendingPath = readPendingClipboardPath(home);
|
|
140
|
+
if (pendingPath) {
|
|
141
|
+
target.write(buildBracketedPaste(pendingPath));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
141
144
|
// readImagePng returns null both for "no image on clipboard" and for
|
|
142
145
|
// unexpected read failures; both cases forward the original Ctrl+V so
|
|
143
146
|
// the container app handles it as a regular keystroke. The throw branch
|
|
144
147
|
// below only fires on truly unexpected exceptions (e.g. fs write
|
|
145
148
|
// errors writing to the host clipboard dir).
|
|
146
|
-
const png = adapter
|
|
149
|
+
const png = adapter?.readImagePng() ?? null;
|
|
147
150
|
if (!png) {
|
|
148
151
|
target.write(match.raw);
|
|
149
152
|
return;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from './paths.js';
|
|
4
|
+
export const RECEIVER_CAPABILITY = 'AGENT_INFRA_CLIPBOARD_RECEIVER:v1';
|
|
5
|
+
const MARKER = '.pending.json';
|
|
6
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
7
|
+
const HASHED_PNG = /^[a-f0-9]{16}\.png$/u;
|
|
8
|
+
const REMOTE_TEMP = /^agent-infra-cp-[0-9a-f-]{36}\.png$/u;
|
|
9
|
+
export function isClipboardInboxReadable(home) {
|
|
10
|
+
const dir = clipboardHostDir(home);
|
|
11
|
+
try {
|
|
12
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
13
|
+
return fs.statSync(dir).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function receiveRemoteClipboardPng(home, tempPath, platformName = process.platform) {
|
|
20
|
+
if (platformName !== 'linux')
|
|
21
|
+
throw new Error('remote clipboard receiver requires Linux');
|
|
22
|
+
if (path.dirname(tempPath) !== '/tmp' || !REMOTE_TEMP.test(path.basename(tempPath))) {
|
|
23
|
+
throw new Error('invalid clipboard upload path');
|
|
24
|
+
}
|
|
25
|
+
const signature = Buffer.alloc(PNG_SIGNATURE.length);
|
|
26
|
+
const fd = fs.openSync(tempPath, 'r');
|
|
27
|
+
let signatureBytes;
|
|
28
|
+
try {
|
|
29
|
+
signatureBytes = fs.readSync(fd, signature, 0, signature.length, 0);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
if (signatureBytes !== PNG_SIGNATURE.length || !signature.equals(PNG_SIGNATURE)) {
|
|
35
|
+
throw new Error('uploaded clipboard file is not a PNG');
|
|
36
|
+
}
|
|
37
|
+
const png = fs.readFileSync(tempPath);
|
|
38
|
+
const dir = clipboardHostDir(home);
|
|
39
|
+
if (!fs.existsSync(path.dirname(dir)))
|
|
40
|
+
throw new Error('agent-infra is not installed on this host');
|
|
41
|
+
const filename = pngClipboardFilename(png);
|
|
42
|
+
writeClipboardPngAtomic(dir, filename, png);
|
|
43
|
+
pruneClipboardDir(dir);
|
|
44
|
+
const markerTmp = path.join(dir, `.${MARKER}.${process.pid}.tmp`);
|
|
45
|
+
fs.writeFileSync(markerTmp, JSON.stringify({ version: 1, filename, createdAt: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
46
|
+
fs.renameSync(markerTmp, path.join(dir, MARKER));
|
|
47
|
+
return containerClipboardPath(filename);
|
|
48
|
+
}
|
|
49
|
+
export function readPendingClipboardPath(home) {
|
|
50
|
+
const dir = clipboardHostDir(home);
|
|
51
|
+
const markerPath = path.join(dir, MARKER);
|
|
52
|
+
let markerText;
|
|
53
|
+
try {
|
|
54
|
+
markerText = fs.readFileSync(markerPath, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT')
|
|
58
|
+
return null;
|
|
59
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
60
|
+
}
|
|
61
|
+
let marker;
|
|
62
|
+
try {
|
|
63
|
+
marker = JSON.parse(markerText);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
67
|
+
}
|
|
68
|
+
if (marker.version !== 1 || typeof marker.filename !== 'string' || !HASHED_PNG.test(marker.filename)) {
|
|
69
|
+
throw new Error('invalid clipboard pending marker');
|
|
70
|
+
}
|
|
71
|
+
const filePath = path.join(dir, marker.filename);
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.lstatSync(filePath);
|
|
74
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
75
|
+
throw new Error('pending clipboard image is not a regular file');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new Error('invalid clipboard pending marker', { cause: error });
|
|
79
|
+
}
|
|
80
|
+
return containerClipboardPath(marker.filename);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -47,13 +47,27 @@ 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>] [--no-refresh]
|
|
50
|
+
const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P]
|
|
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
|
-
symlinked into $HOME)
|
|
56
|
+
symlinked into $HOME).
|
|
57
|
+
|
|
58
|
+
Proxy:
|
|
59
|
+
--inherit-proxy, -P Copy non-empty standard host proxy variables into the
|
|
60
|
+
container environment through the private env file.`;
|
|
61
|
+
const HOST_PROXY_ENV_KEYS = [
|
|
62
|
+
'http_proxy',
|
|
63
|
+
'HTTP_PROXY',
|
|
64
|
+
'https_proxy',
|
|
65
|
+
'HTTPS_PROXY',
|
|
66
|
+
'all_proxy',
|
|
67
|
+
'ALL_PROXY',
|
|
68
|
+
'no_proxy',
|
|
69
|
+
'NO_PROXY'
|
|
70
|
+
];
|
|
57
71
|
function resolveToolDirs(config, tools, branch) {
|
|
58
72
|
return tools.map((tool) => {
|
|
59
73
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -209,12 +223,14 @@ export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoo
|
|
|
209
223
|
// Keep in sync with the symlink block in lib/sandbox/runtimes/ai-tools.dockerfile.
|
|
210
224
|
const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
|
|
211
225
|
export function ensureShellConfigSymlinks(engine, container, execFn = execEngine) {
|
|
212
|
-
// Idempotent symlink setup.
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
// Idempotent symlink setup. Avoid a shell command here because Windows .cmd
|
|
227
|
+
// engine shims would interpret metacharacters in a `bash -lc` script before
|
|
228
|
+
// forwarding it to Docker.
|
|
229
|
+
for (const file of SHELL_CONFIG_SYMLINKS) {
|
|
230
|
+
execFn(engine, 'docker', [
|
|
231
|
+
'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
|
|
232
|
+
], { stdio: 'ignore' });
|
|
233
|
+
}
|
|
218
234
|
}
|
|
219
235
|
export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
|
|
220
236
|
const hostDir = hostShellConfigDir(home, project, branch);
|
|
@@ -423,10 +439,21 @@ function formatEnvFileEntry(key, value) {
|
|
|
423
439
|
}
|
|
424
440
|
return `${key}=${value}`;
|
|
425
441
|
}
|
|
442
|
+
export function collectHostProxyEntries(env) {
|
|
443
|
+
const entriesByExactKey = new Map(Object.entries(env));
|
|
444
|
+
return HOST_PROXY_ENV_KEYS.flatMap((key) => {
|
|
445
|
+
const value = entriesByExactKey.get(key);
|
|
446
|
+
return typeof value === 'string' && value !== '' ? [[key, value]] : [];
|
|
447
|
+
});
|
|
448
|
+
}
|
|
426
449
|
export function buildContainerEnvFile(resolvedTools, engine, runSafeEngineFn = runSafeEngine, options = {}) {
|
|
427
|
-
const { mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir() } = options;
|
|
450
|
+
const { additionalEntries = [], mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir(), runSafeFn = runSafe } = options;
|
|
428
451
|
const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
|
|
429
|
-
|
|
452
|
+
entries.push(...additionalEntries);
|
|
453
|
+
let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
|
|
454
|
+
if (!ghToken && engine === 'wsl2') {
|
|
455
|
+
ghToken = runSafeFn('gh', ['auth', 'token']);
|
|
456
|
+
}
|
|
430
457
|
if (ghToken) {
|
|
431
458
|
entries.push(['GH_TOKEN', ghToken]);
|
|
432
459
|
}
|
|
@@ -928,6 +955,7 @@ export async function create(args) {
|
|
|
928
955
|
cpu: { type: 'string' },
|
|
929
956
|
memory: { type: 'string' },
|
|
930
957
|
'no-refresh': { type: 'boolean' },
|
|
958
|
+
'inherit-proxy': { type: 'boolean', short: 'P' },
|
|
931
959
|
help: { type: 'boolean', short: 'h' }
|
|
932
960
|
}
|
|
933
961
|
});
|
|
@@ -1143,8 +1171,12 @@ export async function create(args) {
|
|
|
1143
1171
|
const cachedGpg = needsGpg
|
|
1144
1172
|
? readGpgCache(effectiveConfig.home, effectiveConfig.project, undefined, signingKey)
|
|
1145
1173
|
: null;
|
|
1146
|
-
const
|
|
1174
|
+
const proxyEntries = values['inherit-proxy'] ? collectHostProxyEntries(process.env) : [];
|
|
1175
|
+
const envFile = buildContainerEnvFile(effectiveResolvedTools, engine, undefined, {
|
|
1176
|
+
additionalEntries: proxyEntries
|
|
1177
|
+
});
|
|
1147
1178
|
let hostShellConfig;
|
|
1179
|
+
let tmpfsSeedPlan = [];
|
|
1148
1180
|
try {
|
|
1149
1181
|
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1150
1182
|
if (claudeCodeEntry) {
|
|
@@ -1181,21 +1213,23 @@ export async function create(args) {
|
|
|
1181
1213
|
'-v',
|
|
1182
1214
|
volumeArg(engine, hostPath, containerPath, ':ro')
|
|
1183
1215
|
]);
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
// the
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
// runtime files left in the host dir (e.g. a stale logs_2.sqlite or
|
|
1192
|
-
// sessions/ from a previous bind-mount era) must NOT be re-mounted,
|
|
1193
|
-
// or the high-churn writes would land on the host SSD again.
|
|
1194
|
-
const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
|
|
1216
|
+
// Mount each declared seed read-only at an isolated staging path.
|
|
1217
|
+
// After docker run mounts the empty tmpfs, the container's default
|
|
1218
|
+
// user copies these entries into it so the runtime targets are normal,
|
|
1219
|
+
// writable files rather than nested mount points. The allowlist keeps
|
|
1220
|
+
// stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
|
|
1221
|
+
// being exposed or written through.
|
|
1222
|
+
tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
|
|
1195
1223
|
const hostPath = path.join(dir, entry);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1224
|
+
if (!fs.existsSync(hostPath)) {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
|
|
1228
|
+
return [{
|
|
1229
|
+
stagingPath,
|
|
1230
|
+
targetPath: path.posix.join(tool.containerMount, entry),
|
|
1231
|
+
volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
|
|
1232
|
+
}];
|
|
1199
1233
|
}));
|
|
1200
1234
|
const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
|
|
1201
1235
|
.filter(({ hostPath }) => fs.existsSync(hostPath))
|
|
@@ -1238,7 +1272,7 @@ export async function create(args) {
|
|
|
1238
1272
|
...dotfilesMount,
|
|
1239
1273
|
...toolVolumes,
|
|
1240
1274
|
...tmpfsArgs,
|
|
1241
|
-
...
|
|
1275
|
+
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1242
1276
|
...liveMountVolumes,
|
|
1243
1277
|
...shellConfigVolumes,
|
|
1244
1278
|
...envFile.dockerArgs,
|
|
@@ -1251,6 +1285,14 @@ export async function create(args) {
|
|
|
1251
1285
|
finally {
|
|
1252
1286
|
envFile.cleanup();
|
|
1253
1287
|
}
|
|
1288
|
+
for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
|
|
1289
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1290
|
+
'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
|
|
1291
|
+
]);
|
|
1292
|
+
runEngineTaskCommand(engine, 'docker', [
|
|
1293
|
+
'exec', container, 'cp', '-R', '--', stagingPath, targetPath
|
|
1294
|
+
]);
|
|
1295
|
+
}
|
|
1254
1296
|
// Belt-and-suspenders: re-create the four shell-config symlinks at
|
|
1255
1297
|
// runtime so users with a custom `sandbox.dockerfile` (which won't
|
|
1256
1298
|
// include the ai-tools.dockerfile symlink fragment) still get
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { platform } from 'node:os';
|
|
2
2
|
import { detectHostResources } from './constants.js';
|
|
3
3
|
import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.js';
|
|
4
|
-
import { run, runOk, runSafe, runVerbose } from './shell.js';
|
|
4
|
+
import { run, runOk, runOkEngine, runSafe, runVerbose } from './shell.js';
|
|
5
5
|
export const ENGINES = Object.freeze({
|
|
6
6
|
COLIMA: 'colima',
|
|
7
7
|
ORBSTACK: 'orbstack',
|
|
@@ -27,6 +27,34 @@ function applyDockerContext(adapter) {
|
|
|
27
27
|
process.env.DOCKER_CONTEXT = adapter.dockerContext;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
function buildKitError(engine) {
|
|
31
|
+
let repair;
|
|
32
|
+
switch (engine) {
|
|
33
|
+
case ENGINES.COLIMA:
|
|
34
|
+
repair = 'Install the Docker Buildx plugin with: brew install docker-buildx';
|
|
35
|
+
break;
|
|
36
|
+
case ENGINES.DOCKER_DESKTOP:
|
|
37
|
+
case ENGINES.WSL2:
|
|
38
|
+
repair = 'Upgrade or repair Docker Desktop so Docker Buildx and BuildKit are available.';
|
|
39
|
+
break;
|
|
40
|
+
case ENGINES.NATIVE:
|
|
41
|
+
repair = 'Install the Docker Buildx plugin for your Docker Engine installation.';
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
repair = `Upgrade or repair ${engineDisplayName(engine)} so Docker Buildx and BuildKit are available.`;
|
|
45
|
+
}
|
|
46
|
+
return new Error([
|
|
47
|
+
`Docker BuildKit is not available for ${engineDisplayName(engine)}.`,
|
|
48
|
+
repair,
|
|
49
|
+
'Verify the builder with: docker buildx inspect --bootstrap',
|
|
50
|
+
'Then retry the sandbox create or rebuild command.'
|
|
51
|
+
].join('\n'));
|
|
52
|
+
}
|
|
53
|
+
function ensureBuildKit(engine, runOkEngineFn) {
|
|
54
|
+
if (!runOkEngineFn(engine, 'docker', ['buildx', 'inspect', '--bootstrap'])) {
|
|
55
|
+
throw buildKitError(engine);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
30
58
|
export function validateSandboxEngine(engine, { platformFn = platform } = {}) {
|
|
31
59
|
if (engine === null || engine === undefined) {
|
|
32
60
|
return null;
|
|
@@ -90,9 +118,11 @@ export async function ensureDocker(config, onMessage, dependencies = {}) {
|
|
|
90
118
|
const engine = detectEngine(config, dependencies);
|
|
91
119
|
const adapter = getAdapter(engine);
|
|
92
120
|
const effectiveConfig = effectiveConfigFor(adapter, config);
|
|
121
|
+
const fns = runFns(dependencies);
|
|
93
122
|
applyDockerContext(adapter);
|
|
94
|
-
const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage,
|
|
95
|
-
|
|
123
|
+
const vmJustStarted = await adapter.ensure(effectiveConfig, onMessage, fns);
|
|
124
|
+
ensureBuildKit(engine, dependencies.runOkEngineFn ?? runOkEngine);
|
|
125
|
+
adapter.syncResources(effectiveConfig, onMessage, fns, { vmJustStarted });
|
|
96
126
|
}
|
|
97
127
|
export function isVmManaged(config = {}, dependencies = {}) {
|
|
98
128
|
try {
|
|
@@ -28,8 +28,8 @@ export const colimaAdapter = {
|
|
|
28
28
|
async ensure(config, onMessage, { runOk, runSafe, runVerbose }) {
|
|
29
29
|
let started = false;
|
|
30
30
|
if (!runOk('which', ['colima'])) {
|
|
31
|
-
onMessage?.('Installing colima + docker via Homebrew...');
|
|
32
|
-
runVerbose('brew', ['install', 'colima', 'docker']);
|
|
31
|
+
onMessage?.('Installing colima + docker + docker-buildx via Homebrew...');
|
|
32
|
+
runVerbose('brew', ['install', 'colima', 'docker', 'docker-buildx']);
|
|
33
33
|
}
|
|
34
34
|
if (!runOk('colima', ['status'])) {
|
|
35
35
|
onMessage?.('Starting Colima VM...');
|
|
@@ -12,6 +12,9 @@ RUN if [ "${HOST_UID}" = "0" ]; then \
|
|
|
12
12
|
(groupadd -o -g ${HOST_GID} devuser || true) && \
|
|
13
13
|
useradd -o -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
14
14
|
else \
|
|
15
|
+
if ubuntu_uid="$(id -u ubuntu 2>/dev/null)" && [ "${ubuntu_uid}" = "${HOST_UID}" ]; then \
|
|
16
|
+
userdel -r ubuntu || exit 1; \
|
|
17
|
+
fi; \
|
|
15
18
|
(groupadd -g ${HOST_GID} devuser || true) && \
|
|
16
19
|
useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
17
20
|
fi
|
|
@@ -188,6 +191,10 @@ fi
|
|
|
188
191
|
exec tmux new-session -s "$SESSION"
|
|
189
192
|
SCRIPT
|
|
190
193
|
|
|
194
|
+
RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status && \
|
|
195
|
+
test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
|
|
196
|
+
test -s /usr/local/bin/sandbox-tmux-entry && test -x /usr/local/bin/sandbox-tmux-entry
|
|
197
|
+
|
|
191
198
|
ENV LANG=en_US.UTF-8
|
|
192
199
|
ENV LC_ALL=en_US.UTF-8
|
|
193
200
|
ENV TERM=xterm-256color
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { getAdapter } from './engines/index.js';
|
|
4
5
|
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
5
6
|
function normalizeOptions(opts = {}, stdio) {
|
|
6
7
|
return {
|
|
@@ -114,6 +115,12 @@ export function commandForEngine(engine, cmd, args = []) {
|
|
|
114
115
|
const resolvedWrapper = resolveCommand('wsl.exe');
|
|
115
116
|
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
116
117
|
}
|
|
118
|
+
if (cmd === 'docker') {
|
|
119
|
+
const context = getAdapter(engine).dockerContext;
|
|
120
|
+
if (context) {
|
|
121
|
+
return { cmd, args: ['--context', context, ...args] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
return { cmd, args };
|
|
118
125
|
}
|
|
119
126
|
export function runEngine(engine, cmd, args, opts = {}) {
|
|
@@ -122,7 +129,8 @@ export function runEngine(engine, cmd, args, opts = {}) {
|
|
|
122
129
|
}
|
|
123
130
|
export function execEngine(engine, cmd, args, opts = {}) {
|
|
124
131
|
const command = commandForEngine(engine, cmd, args);
|
|
125
|
-
|
|
132
|
+
const resolved = resolveCommand(command.cmd);
|
|
133
|
+
return execFileSync(resolved, command.args, commandOptions(resolved, opts));
|
|
126
134
|
}
|
|
127
135
|
export function runOkEngine(engine, cmd, args, opts = {}) {
|
|
128
136
|
const command = commandForEngine(engine, cmd, args);
|