@fitlab-ai/agent-infra 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/README.zh-CN.md +10 -3
- package/bin/cli.ts +6 -1
- package/dist/bin/cli.js +6 -1
- package/dist/lib/sandbox/clipboard/bridge.js +216 -0
- package/dist/lib/sandbox/clipboard/darwin.js +73 -0
- package/dist/lib/sandbox/clipboard/index.js +9 -0
- package/dist/lib/sandbox/clipboard/keys.js +58 -0
- package/dist/lib/sandbox/clipboard/node-pty.js +13 -0
- package/dist/lib/sandbox/clipboard/paths.js +59 -0
- package/dist/lib/sandbox/commands/create.js +11 -2
- package/dist/lib/sandbox/commands/enter.js +8 -2
- package/dist/lib/sandbox/commands/ls.js +19 -4
- package/dist/lib/sandbox/commands/prune.js +176 -0
- package/dist/lib/sandbox/commands/rm.js +27 -33
- package/dist/lib/sandbox/config.js +1 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/index.js +7 -1
- package/dist/lib/sandbox/managed-fs.js +25 -0
- package/dist/lib/sandbox/tools.js +1 -1
- package/dist/lib/version.js +9 -2
- package/lib/sandbox/clipboard/bridge.ts +285 -0
- package/lib/sandbox/clipboard/darwin.ts +90 -0
- package/lib/sandbox/clipboard/index.ts +13 -0
- package/lib/sandbox/clipboard/keys.ts +78 -0
- package/lib/sandbox/clipboard/node-pty.d.ts +19 -0
- package/lib/sandbox/clipboard/node-pty.ts +34 -0
- package/lib/sandbox/clipboard/paths.ts +71 -0
- package/lib/sandbox/commands/create.ts +15 -2
- package/lib/sandbox/commands/enter.ts +8 -2
- package/lib/sandbox/commands/ls.ts +28 -4
- package/lib/sandbox/commands/prune.ts +211 -0
- package/lib/sandbox/commands/rm.ts +30 -32
- package/lib/sandbox/config.ts +2 -0
- package/lib/sandbox/constants.ts +9 -0
- package/lib/sandbox/index.ts +7 -1
- package/lib/sandbox/managed-fs.ts +27 -0
- package/lib/sandbox/tools.ts +1 -1
- package/lib/version.ts +11 -4
- package/package.json +5 -1
- package/templates/.agents/README.en.md +19 -0
- package/templates/.agents/README.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +26 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +26 -0
- package/templates/.agents/skills/analyze-task/config/verify.en.json +51 -0
- package/templates/.agents/skills/analyze-task/config/{verify.json → verify.zh-CN.json} +6 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +15 -0
- package/templates/.agents/skills/complete-task/config/{verify.json → verify.en.json} +10 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +48 -0
- package/templates/.agents/skills/implement-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/implement-task/config/verify.en.json +51 -0
- package/templates/.agents/skills/implement-task/config/{verify.json → verify.zh-CN.json} +7 -2
- package/templates/.agents/skills/implement-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/implement-task/reference/report-template.zh-CN.md +15 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +22 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +22 -0
- package/templates/.agents/skills/plan-task/config/verify.en.json +52 -0
- package/templates/.agents/skills/plan-task/config/{verify.json → verify.zh-CN.json} +6 -2
- package/templates/.agents/skills/post-release/SKILL.en.md +1 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/refine-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/refine-task/config/verify.en.json +47 -0
- package/templates/.agents/skills/refine-task/config/{verify.json → verify.zh-CN.json} +7 -2
- package/templates/.agents/skills/refine-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +15 -0
- package/templates/.agents/skills/review-task/SKILL.en.md +14 -0
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +14 -0
- package/templates/.agents/skills/review-task/config/verify.en.json +50 -0
- package/templates/.agents/skills/review-task/config/{verify.json → verify.zh-CN.json} +5 -2
- package/templates/.agents/skills/review-task/reference/report-template.en.md +15 -0
- package/templates/.agents/skills/review-task/reference/report-template.zh-CN.md +15 -0
- package/dist/package.json +0 -5
package/README.md
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
<a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
19
19
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
|
|
20
20
|
<a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
|
|
21
|
+
<a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
|
|
21
22
|
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
|
22
23
|
</p>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
<p align="center">
|
|
26
|
+
<strong>English</strong> · <a href="./README.zh-CN.md">中文</a>
|
|
27
|
+
</p>
|
|
25
28
|
|
|
26
29
|
<a id="why-agent-infra"></a>
|
|
27
30
|
|
|
@@ -201,6 +204,8 @@ The sandbox image also preinstalls `gh`. When `gh auth token` succeeds on the ho
|
|
|
201
204
|
|
|
202
205
|
`ai sandbox exec` also forwards a small terminal-detection whitelist (`TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `LC_TERMINAL`, `LC_TERMINAL_VERSION`) into the container. This keeps interactive TUIs aligned with the host terminal for behaviors such as Claude Code's Shift+Enter newline support, without passing through the full host environment.
|
|
203
206
|
|
|
207
|
+
On macOS, interactive `ai sandbox exec <branch>` sessions can bridge image paste into the sandbox. When you press `Ctrl+V` and the host clipboard currently holds an image, agent-infra reads the image from the host clipboard, writes a PNG under `~/.agent-infra/clipboard/`, and injects the container path as bracketed paste so Claude Code, Codex, Gemini CLI, and OpenCode can attach it. The host clipboard is only read, never rewritten. The bridge is best-effort: existing sandboxes must be rebuilt to receive the `/clipboard` mount, and if the optional pty dependency or clipboard probe is unavailable the session falls back to the normal interactive path.
|
|
208
|
+
|
|
204
209
|
`ai sandbox exec` and `ai sandbox refresh` reconcile Claude Code credentials in both directions across the host credential store and every sandbox project copy under `~/.agent-infra/credentials/*`. When a long-running sandbox refreshes OAuth tokens first, the next entry or refresh command writes the freshest valid copy back to the host Keychain or `~/.claude/.credentials.json`; when the host is fresher, it updates the project copies. If every copy is stale, `ai sandbox refresh` probes `claude /status` and asks you to log in only when the probe cannot recover credentials.
|
|
205
210
|
|
|
206
211
|
### Host-sandbox file exchange
|
|
@@ -212,11 +217,16 @@ the host and the sandbox without polluting the git worktree:
|
|
|
212
217
|
sandbox of the same project, regardless of branch.
|
|
213
218
|
- `/share/branch` <- `~/.agent-infra/share/<project>/branches/<branch>/` -
|
|
214
219
|
exclusive to the current branch sandbox.
|
|
220
|
+
- `/clipboard` <- `~/.agent-infra/clipboard/` - read-only image paste bridge
|
|
221
|
+
storage on macOS.
|
|
215
222
|
|
|
216
223
|
These paths are intentionally hardcoded; there is no `.airc.json` knob. Both
|
|
217
224
|
host directories are created automatically on first `create`. When you
|
|
218
225
|
`ai sandbox rm <branch>` or `ai sandbox rm --all`, you will be prompted (default
|
|
219
226
|
yes) to clean up the corresponding share dirs alongside the worktrees.
|
|
227
|
+
Use `ai sandbox prune --dry-run` to inspect orphaned per-branch state dirs left
|
|
228
|
+
behind by older versions or interrupted cleanup, then `ai sandbox prune` to
|
|
229
|
+
remove only dirs without an active sandbox container.
|
|
220
230
|
Existing sandboxes pick up these mounts after `ai sandbox rm <branch>` and
|
|
221
231
|
`ai sandbox create <branch>`.
|
|
222
232
|
|
|
@@ -452,7 +462,7 @@ These configurations are not actively tested in this release:
|
|
|
452
462
|
|
|
453
463
|
- **Podman** instead of Docker: Works on Fedora 40+ and other `dnf`-based RHEL family distros (RHEL, CentOS Stream, Rocky, Alma) via the `podman-docker` shim (`sudo dnf install podman podman-docker`; optionally `sudo touch /etc/containers/nodocker` to silence its per-command notice).
|
|
454
464
|
- **SELinux-enforcing** hosts (Fedora / RHEL): `ai sandbox create` automatically labels bind mounts with Docker's shared `:z` flag — no setup required. Set `AGENT_INFRA_SELINUX_DISABLE=1` to opt out for debugging.
|
|
455
|
-
- `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm` directly.
|
|
465
|
+
- `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm`, and `ai sandbox prune` directly.
|
|
456
466
|
|
|
457
467
|
### Windows
|
|
458
468
|
|
|
@@ -777,7 +787,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
|
|
|
777
787
|
"project": "my-project",
|
|
778
788
|
"org": "my-org",
|
|
779
789
|
"language": "en",
|
|
780
|
-
"templateVersion": "v0.6.
|
|
790
|
+
"templateVersion": "v0.6.3",
|
|
781
791
|
"templates": {
|
|
782
792
|
"sources": [
|
|
783
793
|
{ "type": "local", "path": "~/private-templates" }
|
package/README.zh-CN.md
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
<a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
19
19
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
|
|
20
20
|
<a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
|
|
21
|
+
<a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
|
|
21
22
|
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
|
22
23
|
</p>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="./README.md">English</a> · <strong>中文</strong>
|
|
27
|
+
</p>
|
|
25
28
|
|
|
26
29
|
<a id="why-agent-infra"></a>
|
|
27
30
|
|
|
@@ -201,6 +204,8 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
|
|
|
201
204
|
|
|
202
205
|
`ai sandbox exec` 也会向容器透传一小组终端检测白名单变量(`TERM_PROGRAM`、`TERM_PROGRAM_VERSION`、`LC_TERMINAL`、`LC_TERMINAL_VERSION`)。这样可以让交互式 TUI 保持与宿主终端一致的行为,例如 Claude Code 的 `Shift+Enter` 换行支持,同时避免把整个宿主环境灌入容器。
|
|
203
206
|
|
|
207
|
+
在 macOS 上,交互式 `ai sandbox exec <branch>` 会尽力桥接宿主图片粘贴。当你按下 `Ctrl+V` 且宿主剪贴板当前是图片时,agent-infra 会从宿主剪贴板读取图片,将 PNG 写到 `~/.agent-infra/clipboard/`,再以 bracketed paste 注入容器内路径,让 Claude Code、Codex、Gemini CLI 和 OpenCode 按图片附件处理。宿主剪贴板只读,不会被改写。该能力会自动降级:已有沙箱需要重建后才有 `/clipboard` 挂载;如果可选 pty 依赖或剪贴板探测不可用,会回退到原本的交互进入方式。
|
|
208
|
+
|
|
204
209
|
`ai sandbox exec` 和 `ai sandbox refresh` 会在宿主机凭证存储与 `~/.agent-infra/credentials/*` 下的所有沙箱项目副本之间做双向 reconcile。长时间运行的沙箱如果先刷新了 OAuth token,下一次进入或刷新命令会把最新有效副本回写到宿主 Keychain 或 `~/.claude/.credentials.json`;宿主机更新时也会继续覆盖项目副本。如果所有副本都已失效,`ai sandbox refresh` 会尝试 `claude /status` 探活,只有探活无法恢复时才提示重新登录。
|
|
205
210
|
|
|
206
211
|
### 宿主-沙箱文件交换
|
|
@@ -209,8 +214,10 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
|
|
|
209
214
|
|
|
210
215
|
- `/share/common` <- `~/.agent-infra/share/<project>/common/`:项目级共享,跨分支可见。
|
|
211
216
|
- `/share/branch` <- `~/.agent-infra/share/<project>/branches/<branch>/`:分支独占。
|
|
217
|
+
- `/clipboard` <- `~/.agent-infra/clipboard/`:macOS 图片粘贴桥接使用的只读存储。
|
|
212
218
|
|
|
213
219
|
这两条路径硬编码,不暴露 `.airc.json` 配置项。首次 `create` 时会自动创建宿主目录;`ai sandbox rm <branch>` 与 `ai sandbox rm --all` 删除时会附带询问是否清理(默认 yes)。
|
|
220
|
+
可先用 `ai sandbox prune --dry-run` 查看旧版本或异常中断遗留的孤儿 per-branch 状态目录,再用 `ai sandbox prune` 只删除没有活跃 sandbox 容器对应的目录。
|
|
214
221
|
已有沙箱需要执行 `ai sandbox rm <branch>` 后再执行 `ai sandbox create <branch>`,才能加载新的挂载点。
|
|
215
222
|
|
|
216
223
|
#### 用户级 dotfiles 通道
|
|
@@ -428,7 +435,7 @@ Rootless 模式的已知差异:
|
|
|
428
435
|
|
|
429
436
|
- 用 **Podman** 替代 Docker:Fedora 40+ 及其他 `dnf` 系 RHEL 发行版(RHEL、CentOS Stream、Rocky、Alma)上通过 `podman-docker` shim 已可使用(`sudo dnf install podman podman-docker`;可选 `sudo touch /etc/containers/nodocker` 抑制 podman 在每条命令前打印的提示)。
|
|
430
437
|
- **SELinux enforcing** 宿主机(Fedora / RHEL):`ai sandbox create` 会自动给 bind mount 加 Docker 共享 `:z` 标签,无需手动准备。如需排障可设 `AGENT_INFRA_SELINUX_DISABLE=1` 关闭。
|
|
431
|
-
- `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`。
|
|
438
|
+
- `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`、`ai sandbox prune`。
|
|
432
439
|
|
|
433
440
|
### Windows
|
|
434
441
|
|
|
@@ -753,7 +760,7 @@ import-issue #42 从 GitHub Issue 导入任务
|
|
|
753
760
|
"project": "my-project",
|
|
754
761
|
"org": "my-org",
|
|
755
762
|
"language": "en",
|
|
756
|
-
"templateVersion": "v0.6.
|
|
763
|
+
"templateVersion": "v0.6.3",
|
|
757
764
|
"templates": {
|
|
758
765
|
"sources": [
|
|
759
766
|
{ "type": "local", "path": "~/private-templates" }
|
package/bin/cli.ts
CHANGED
|
@@ -10,7 +10,7 @@ if (major < 22) {
|
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const USAGE = `agent-infra - bootstrap AI collaboration infrastructure
|
|
13
|
+
const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
agent-infra init Initialize a new project with update-agent-infra seed command
|
|
@@ -104,6 +104,11 @@ switch (command) {
|
|
|
104
104
|
}
|
|
105
105
|
break;
|
|
106
106
|
}
|
|
107
|
+
case '--version':
|
|
108
|
+
case '-v': {
|
|
109
|
+
console.log(`agent-infra ${VERSION}`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
107
112
|
case 'help':
|
|
108
113
|
case '':
|
|
109
114
|
process.stdout.write(USAGE);
|
package/dist/bin/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ if (major < 22) {
|
|
|
14
14
|
process.stderr.write(`agent-infra requires Node.js >= 22 (current: ${process.version})\n`);
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
|
-
const USAGE = `agent-infra - bootstrap AI collaboration infrastructure
|
|
17
|
+
const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
20
|
agent-infra init Initialize a new project with update-agent-infra seed command
|
|
@@ -108,6 +108,11 @@ switch (command) {
|
|
|
108
108
|
}
|
|
109
109
|
break;
|
|
110
110
|
}
|
|
111
|
+
case '--version':
|
|
112
|
+
case '-v': {
|
|
113
|
+
console.log(`agent-infra ${VERSION}`);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
111
116
|
case 'help':
|
|
112
117
|
case '':
|
|
113
118
|
process.stdout.write(USAGE);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { createClipboardAdapter } from "./index.js";
|
|
3
|
+
import { buildBracketedPaste, CtrlVDetector } from "./keys.js";
|
|
4
|
+
import { clipboardHostDir, containerClipboardPath, pngClipboardFilename, pruneClipboardDir, writeClipboardPngAtomic } from "./paths.js";
|
|
5
|
+
import { commandForEngine, restoreTerminal, runInteractiveEngine, runOkEngine } from "../shell.js";
|
|
6
|
+
import { loadNodePty } from "./node-pty.js";
|
|
7
|
+
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
8
|
+
const PARTIAL_ESCAPE_FLUSH_MS = 30;
|
|
9
|
+
export async function runInteractiveWithClipboardBridge(options) {
|
|
10
|
+
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
|
+
function fallback(reason) {
|
|
12
|
+
writeStderr(`${FALLBACK_PREFIX}: ${reason}\n`);
|
|
13
|
+
return runInteractive(engine, 'docker', dockerArgs);
|
|
14
|
+
}
|
|
15
|
+
if (platformName !== 'darwin') {
|
|
16
|
+
return runInteractive(engine, 'docker', dockerArgs);
|
|
17
|
+
}
|
|
18
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
19
|
+
return fallback('host stdin/stdout is not a TTY');
|
|
20
|
+
}
|
|
21
|
+
if (!adapter) {
|
|
22
|
+
return fallback('macOS clipboard adapter is unavailable');
|
|
23
|
+
}
|
|
24
|
+
const available = adapter.available();
|
|
25
|
+
if (!available.ok) {
|
|
26
|
+
return fallback(available.reason);
|
|
27
|
+
}
|
|
28
|
+
if (!runOk(engine, 'docker', ['exec', container, 'sh', '-c', '[ -d /clipboard ] && [ -r /clipboard ]'])) {
|
|
29
|
+
return fallback('container /clipboard mount is missing; rebuild the sandbox to enable image paste');
|
|
30
|
+
}
|
|
31
|
+
const pty = await loadPty();
|
|
32
|
+
if (!pty) {
|
|
33
|
+
return fallback('node-pty optional dependency is unavailable');
|
|
34
|
+
}
|
|
35
|
+
const command = commandForEngine(engine, 'docker', dockerArgs);
|
|
36
|
+
let child;
|
|
37
|
+
try {
|
|
38
|
+
child = pty.spawn(command.cmd, command.args, {
|
|
39
|
+
name: env.TERM || 'xterm-256color',
|
|
40
|
+
cols: stdout.columns || 120,
|
|
41
|
+
rows: stdout.rows || 40,
|
|
42
|
+
cwd,
|
|
43
|
+
env
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : 'unknown error';
|
|
48
|
+
return fallback(`node-pty spawn failed: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
return runBridge({
|
|
51
|
+
child,
|
|
52
|
+
home,
|
|
53
|
+
adapter,
|
|
54
|
+
writeStderr,
|
|
55
|
+
stdin,
|
|
56
|
+
stdout,
|
|
57
|
+
detector: createDetector()
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function runBridge({ child, home, adapter, writeStderr, stdin, stdout, detector }) {
|
|
61
|
+
let warnedPasteFailure = false;
|
|
62
|
+
let flushTimer = null;
|
|
63
|
+
const inputDecoder = new StringDecoder('utf8');
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
clearFlushTimer();
|
|
66
|
+
for (const token of detector.feed(inputDecoder.write(chunk))) {
|
|
67
|
+
if (token.kind === 'text') {
|
|
68
|
+
child.write(token.raw);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
handleCtrlV(token, child);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (detector.hasPending()) {
|
|
75
|
+
flushTimer = setTimeout(() => {
|
|
76
|
+
flushTimer = null;
|
|
77
|
+
for (const token of detector.flush()) {
|
|
78
|
+
if (token.kind === 'text') {
|
|
79
|
+
child.write(token.raw);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
handleCtrlV(token, child);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, PARTIAL_ESCAPE_FLUSH_MS);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const onResize = () => child.resize(stdout.columns || 120, stdout.rows || 40);
|
|
89
|
+
const onSigint = () => child.kill('SIGINT');
|
|
90
|
+
const onSigterm = () => child.kill('SIGTERM');
|
|
91
|
+
function handleCtrlV(match, target) {
|
|
92
|
+
try {
|
|
93
|
+
if (!adapter.hasImage()) {
|
|
94
|
+
target.write(match.raw);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const png = adapter.readImagePng();
|
|
98
|
+
if (!png) {
|
|
99
|
+
throw new Error('clipboard image could not be read');
|
|
100
|
+
}
|
|
101
|
+
const filename = pngClipboardFilename(png);
|
|
102
|
+
writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
|
|
103
|
+
pruneClipboardDir(clipboardHostDir(home));
|
|
104
|
+
target.write(buildBracketedPaste(containerClipboardPath(filename)));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
target.write(match.raw);
|
|
108
|
+
if (!warnedPasteFailure) {
|
|
109
|
+
warnedPasteFailure = true;
|
|
110
|
+
writeStderr(`Warning: clipboard image paste failed; forwarded original Ctrl+V (${error instanceof Error ? error.message : 'unknown error'})\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function clearFlushTimer() {
|
|
115
|
+
if (flushTimer) {
|
|
116
|
+
clearTimeout(flushTimer);
|
|
117
|
+
flushTimer = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
stdin.setRawMode?.(true);
|
|
122
|
+
stdin.resume();
|
|
123
|
+
stdin.on('data', onData);
|
|
124
|
+
stdout.on('resize', onResize);
|
|
125
|
+
process.on('SIGINT', onSigint);
|
|
126
|
+
process.on('SIGTERM', onSigterm);
|
|
127
|
+
child.onData((data) => stdout.write(data));
|
|
128
|
+
return exitCode(await onceExit(child, stdin));
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearFlushTimer();
|
|
132
|
+
// The child pty is already exiting here; flushing buffered input is
|
|
133
|
+
// best-effort and must never block terminal/stdin cleanup below.
|
|
134
|
+
try {
|
|
135
|
+
for (const token of detector.feed(inputDecoder.end())) {
|
|
136
|
+
if (token.kind === 'text') {
|
|
137
|
+
child.write(token.raw);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
handleCtrlV(token, child);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const token of detector.flush()) {
|
|
144
|
+
if (token.kind === 'text') {
|
|
145
|
+
child.write(token.raw);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Writing to an already-closed pty can throw; ignore on teardown.
|
|
151
|
+
}
|
|
152
|
+
stdin.off('data', onData);
|
|
153
|
+
stdout.off('resize', onResize);
|
|
154
|
+
process.off('SIGINT', onSigint);
|
|
155
|
+
process.off('SIGTERM', onSigterm);
|
|
156
|
+
stdin.setRawMode?.(false);
|
|
157
|
+
// Release stdin so the resumed TTY handle stops keeping the event loop
|
|
158
|
+
// alive; without this the CLI hangs after the sandbox exits until Ctrl+C.
|
|
159
|
+
stdin.pause?.();
|
|
160
|
+
restoreTerminal();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function onceExit(child, stdin) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
let settled = false;
|
|
166
|
+
const finish = (event) => {
|
|
167
|
+
if (settled) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
settled = true;
|
|
171
|
+
stdin.off('end', onStdinEnd);
|
|
172
|
+
stdin.off('close', onStdinEnd);
|
|
173
|
+
resolve(event);
|
|
174
|
+
};
|
|
175
|
+
const onStdinEnd = () => {
|
|
176
|
+
child.kill('SIGHUP');
|
|
177
|
+
finish({ exitCode: 0, signal: 'SIGHUP' });
|
|
178
|
+
};
|
|
179
|
+
child.onExit(finish);
|
|
180
|
+
stdin.once('end', onStdinEnd);
|
|
181
|
+
stdin.once('close', onStdinEnd);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function exitCode(event) {
|
|
185
|
+
if (event.signal !== undefined && event.signal !== null) {
|
|
186
|
+
return signalExitCode(event.signal);
|
|
187
|
+
}
|
|
188
|
+
if (event.exitCode !== undefined && event.exitCode !== null) {
|
|
189
|
+
return event.exitCode;
|
|
190
|
+
}
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
function signalExitCode(signal) {
|
|
194
|
+
if (typeof signal === 'number') {
|
|
195
|
+
return 128 + signal;
|
|
196
|
+
}
|
|
197
|
+
const signals = {
|
|
198
|
+
SIGHUP: 1,
|
|
199
|
+
SIGINT: 2,
|
|
200
|
+
SIGQUIT: 3,
|
|
201
|
+
SIGILL: 4,
|
|
202
|
+
SIGTRAP: 5,
|
|
203
|
+
SIGABRT: 6,
|
|
204
|
+
SIGBUS: 7,
|
|
205
|
+
SIGFPE: 8,
|
|
206
|
+
SIGKILL: 9,
|
|
207
|
+
SIGUSR1: 10,
|
|
208
|
+
SIGSEGV: 11,
|
|
209
|
+
SIGUSR2: 12,
|
|
210
|
+
SIGPIPE: 13,
|
|
211
|
+
SIGALRM: 14,
|
|
212
|
+
SIGTERM: 15
|
|
213
|
+
};
|
|
214
|
+
return 128 + (signals[signal] ?? 0);
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const HAS_IMAGE_TIMEOUT_MS = 500;
|
|
6
|
+
const READ_IMAGE_TIMEOUT_MS = 5_000;
|
|
7
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
8
|
+
export function createDarwinClipboardAdapter({ execFn = execFileSync, mkdtempFn = fs.mkdtempSync, readFileFn = fs.readFileSync, rmFn = fs.rmSync } = {}) {
|
|
9
|
+
return {
|
|
10
|
+
available() {
|
|
11
|
+
try {
|
|
12
|
+
execFn('osascript', ['-e', 'return "ok"'], { encoding: 'utf8', timeout: HAS_IMAGE_TIMEOUT_MS });
|
|
13
|
+
return { ok: true };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, reason: 'macOS osascript is unavailable' };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
hasImage() {
|
|
20
|
+
try {
|
|
21
|
+
const output = String(execFn('osascript', ['-e', 'clipboard info'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
timeout: HAS_IMAGE_TIMEOUT_MS
|
|
24
|
+
}));
|
|
25
|
+
return /\b(PNGf|TIFF|JPEG|GIFf)\b/.test(output);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
readImagePng() {
|
|
32
|
+
const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
|
|
33
|
+
const outputPath = path.join(tmpDir, 'clipboard.png');
|
|
34
|
+
try {
|
|
35
|
+
try {
|
|
36
|
+
execFn('osascript', ['-e', pngWriteScript(outputPath)], {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
execFn('pngpaste', [outputPath], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: READ_IMAGE_TIMEOUT_MS
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const png = Buffer.from(readFileFn(outputPath));
|
|
48
|
+
return isPng(png) ? png : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
rmFn(tmpDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isPng(buffer) {
|
|
60
|
+
return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
|
|
61
|
+
}
|
|
62
|
+
function pngWriteScript(outputPath) {
|
|
63
|
+
const escapedPath = outputPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
64
|
+
return [
|
|
65
|
+
'set pngData to the clipboard as «class PNGf»',
|
|
66
|
+
`set outFile to POSIX file "${escapedPath}"`,
|
|
67
|
+
'set fileRef to open for access outFile with write permission',
|
|
68
|
+
'set eof fileRef to 0',
|
|
69
|
+
'write pngData to fileRef',
|
|
70
|
+
'close access fileRef'
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=darwin.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { platform } from 'node:os';
|
|
2
|
+
import { createDarwinClipboardAdapter } from "./darwin.js";
|
|
3
|
+
export function createClipboardAdapter({ platformName = platform() } = {}) {
|
|
4
|
+
if (platformName !== 'darwin') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return createDarwinClipboardAdapter();
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const CTRL_V_SEQUENCES = [
|
|
2
|
+
{ raw: '\x16', label: 'ctrl-v 0x16' },
|
|
3
|
+
{ raw: '\x1b[118;5u', label: 'ctrl-v csi-u ESC[118;5u' },
|
|
4
|
+
{ raw: '\x1b[27;5;118~', label: 'ctrl-v modifyOtherKeys ESC[27;5;118~' }
|
|
5
|
+
];
|
|
6
|
+
export class CtrlVDetector {
|
|
7
|
+
#pending = '';
|
|
8
|
+
hasPending() {
|
|
9
|
+
return this.#pending.length > 0;
|
|
10
|
+
}
|
|
11
|
+
feed(raw) {
|
|
12
|
+
let input = this.#pending + raw;
|
|
13
|
+
this.#pending = '';
|
|
14
|
+
const tokens = [];
|
|
15
|
+
while (input.length > 0) {
|
|
16
|
+
const match = CTRL_V_SEQUENCES.find((sequence) => input.startsWith(sequence.raw));
|
|
17
|
+
if (match) {
|
|
18
|
+
tokens.push({ kind: 'ctrl-v', raw: match.raw, label: match.label });
|
|
19
|
+
input = input.slice(match.raw.length);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const partial = CTRL_V_SEQUENCES.some((sequence) => sequence.raw.startsWith(input) && input.length < sequence.raw.length);
|
|
23
|
+
if (partial) {
|
|
24
|
+
this.#pending = input;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const first = input.slice(0, 1);
|
|
28
|
+
tokens.push({ kind: 'text', raw: first });
|
|
29
|
+
input = input.slice(first.length);
|
|
30
|
+
}
|
|
31
|
+
return coalesceText(tokens);
|
|
32
|
+
}
|
|
33
|
+
flush() {
|
|
34
|
+
if (!this.#pending) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const raw = this.#pending;
|
|
38
|
+
this.#pending = '';
|
|
39
|
+
return [{ kind: 'text', raw }];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function coalesceText(tokens) {
|
|
43
|
+
const result = [];
|
|
44
|
+
for (const token of tokens) {
|
|
45
|
+
const previous = result.at(-1);
|
|
46
|
+
if (token.kind === 'text' && previous?.kind === 'text') {
|
|
47
|
+
previous.raw += token.raw;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result.push(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export function buildBracketedPaste(text) {
|
|
56
|
+
return `\x1b[200~${text}\x1b[201~`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
export async function loadNodePty() {
|
|
3
|
+
try {
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const mod = require('@lydell/node-pty');
|
|
6
|
+
const maybeDefault = mod;
|
|
7
|
+
return maybeDefault.default ?? mod;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=node-pty.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hostJoin } from "../engines/wsl2-paths.js";
|
|
5
|
+
export const CONTAINER_CLIPBOARD_MOUNT = '/clipboard';
|
|
6
|
+
const DEFAULT_KEEP = 20;
|
|
7
|
+
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
export function clipboardHostDir(home) {
|
|
9
|
+
return hostJoin(home, '.agent-infra', 'clipboard');
|
|
10
|
+
}
|
|
11
|
+
export function containerClipboardPath(filename) {
|
|
12
|
+
return path.posix.join(CONTAINER_CLIPBOARD_MOUNT, filename);
|
|
13
|
+
}
|
|
14
|
+
export function pngClipboardFilename(buffer) {
|
|
15
|
+
return `${crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 16)}.png`;
|
|
16
|
+
}
|
|
17
|
+
export function writeClipboardPngAtomic(dir, filename, buffer) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
19
|
+
try {
|
|
20
|
+
fs.chmodSync(dir, 0o700);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Best effort: existing directories may live on filesystems that ignore chmod.
|
|
24
|
+
}
|
|
25
|
+
const target = path.join(dir, filename);
|
|
26
|
+
const tmp = path.join(dir, `.${filename}.${process.pid}.tmp`);
|
|
27
|
+
fs.writeFileSync(tmp, buffer);
|
|
28
|
+
fs.renameSync(tmp, target);
|
|
29
|
+
return target;
|
|
30
|
+
}
|
|
31
|
+
export function pruneClipboardDir(dir, { keep = DEFAULT_KEEP, maxAgeMs = DEFAULT_MAX_AGE_MS, now = Date.now() } = {}) {
|
|
32
|
+
if (!fs.existsSync(dir)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const entries = fs.readdirSync(dir)
|
|
36
|
+
.filter((name) => name.endsWith('.png'))
|
|
37
|
+
.map((name) => {
|
|
38
|
+
const fullPath = path.join(dir, name);
|
|
39
|
+
const stat = fs.statSync(fullPath);
|
|
40
|
+
return { fullPath, mtimeMs: stat.mtimeMs };
|
|
41
|
+
})
|
|
42
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
43
|
+
const keepSet = new Set(entries.slice(0, keep).map((entry) => entry.fullPath));
|
|
44
|
+
const removed = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (keepSet.has(entry.fullPath) && now - entry.mtimeMs <= maxAgeMs) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
fs.rmSync(entry.fullPath, { force: true });
|
|
51
|
+
removed.push(entry.fullPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Cleanup is opportunistic; a failed prune should not break paste.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return removed;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -8,13 +8,14 @@ import * as p from '@clack/prompts';
|
|
|
8
8
|
import pc from 'picocolors';
|
|
9
9
|
import * as toml from 'smol-toml';
|
|
10
10
|
import { loadConfig } from "../config.js";
|
|
11
|
-
import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxLabel,
|
|
11
|
+
import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from "../constants.js";
|
|
12
12
|
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
16
|
import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from "../tools.js";
|
|
17
17
|
import { hostJoin, toEnginePath, volumeArg } from "../engines/wsl2-paths.js";
|
|
18
|
+
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from "../clipboard/paths.js";
|
|
18
19
|
import { validateSelinuxDisableEnv } from "../engines/selinux.js";
|
|
19
20
|
import { resolveBuildUid } from "../engines/native.js";
|
|
20
21
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
@@ -70,7 +71,13 @@ function resolveToolDirs(config, tools, branch) {
|
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
export function hostShellConfigDir(home, project, branch) {
|
|
73
|
-
return hostJoin(home, '.agent-infra', 'config', project,
|
|
74
|
+
return shellConfigDir({ shellConfigBase: hostJoin(home, '.agent-infra', 'config', project) }, branch);
|
|
75
|
+
}
|
|
76
|
+
export function buildClipboardVolumeArgs(engine, home) {
|
|
77
|
+
return [
|
|
78
|
+
'-v',
|
|
79
|
+
volumeArg(engine, clipboardHostDir(home), CONTAINER_CLIPBOARD_MOUNT, ':ro')
|
|
80
|
+
];
|
|
74
81
|
}
|
|
75
82
|
function runtimeChecks(runtimes) {
|
|
76
83
|
const checks = [];
|
|
@@ -1034,6 +1041,7 @@ export async function create(args) {
|
|
|
1034
1041
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
1035
1042
|
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1036
1043
|
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1044
|
+
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
1037
1045
|
const dotfilesSnapshot = materializeDotfiles(effectiveConfig.dotfilesDir, dotfilesCacheDir(effectiveConfig.home, effectiveConfig.project));
|
|
1038
1046
|
const dotfilesMount = dotfilesSnapshot
|
|
1039
1047
|
? buildDotfilesVolumeArgs(engine, dotfilesSnapshot.cacheDir)
|
|
@@ -1057,6 +1065,7 @@ export async function create(args) {
|
|
|
1057
1065
|
volumeArg(engine, shareCommon, '/share/common'),
|
|
1058
1066
|
'-v',
|
|
1059
1067
|
volumeArg(engine, shareBranch, '/share/branch'),
|
|
1068
|
+
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
1060
1069
|
'-v',
|
|
1061
1070
|
volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
|
|
1062
1071
|
'-v',
|
|
@@ -5,6 +5,7 @@ import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials,
|
|
|
5
5
|
import { runInteractiveEngine, runSafeEngine } from "../shell.js";
|
|
6
6
|
import { resolveTaskBranch } from "../task-resolver.js";
|
|
7
7
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
8
|
+
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
8
9
|
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]`;
|
|
9
10
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
10
11
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
@@ -51,7 +52,7 @@ export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY)
|
|
|
51
52
|
}
|
|
52
53
|
return null;
|
|
53
54
|
}
|
|
54
|
-
export function enter(args) {
|
|
55
|
+
export async function enter(args) {
|
|
55
56
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
56
57
|
process.stdout.write(`${USAGE}\n`);
|
|
57
58
|
if (args.length === 0) {
|
|
@@ -91,7 +92,12 @@ export function enter(args) {
|
|
|
91
92
|
catch (error) {
|
|
92
93
|
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
93
94
|
}
|
|
94
|
-
return
|
|
95
|
+
return runInteractiveWithClipboardBridge({
|
|
96
|
+
engine,
|
|
97
|
+
dockerArgs: ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH],
|
|
98
|
+
container,
|
|
99
|
+
home: config.home
|
|
100
|
+
});
|
|
95
101
|
}
|
|
96
102
|
return runInteractiveEngine(engine, 'docker', ['exec', '-it', ...envFlags, container, ...cmd]);
|
|
97
103
|
}
|