@fitlab-ai/agent-infra 0.7.2 → 0.7.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 +7 -1
- package/README.zh-CN.md +9 -3
- package/dist/lib/defaults.json +0 -1
- package/dist/lib/init.js +0 -3
- package/dist/lib/sandbox/commands/enter.js +13 -15
- package/dist/lib/sandbox/commands/list-running.js +36 -1
- package/dist/lib/sandbox/commands/ls.js +9 -4
- package/dist/lib/sandbox/commands/start.js +36 -0
- package/dist/lib/sandbox/index.js +8 -0
- package/dist/lib/table.js +11 -2
- package/dist/lib/task/commands/ls.js +1 -1
- package/dist/lib/task/short-id.js +10 -0
- package/dist/lib/update.js +25 -8
- package/lib/defaults.json +0 -1
- package/lib/init.ts +0 -10
- package/lib/sandbox/commands/enter.ts +33 -14
- package/lib/sandbox/commands/list-running.ts +43 -1
- package/lib/sandbox/commands/ls.ts +12 -4
- package/lib/sandbox/commands/start.ts +61 -0
- package/lib/sandbox/index.ts +8 -0
- package/lib/table.ts +14 -2
- package/lib/task/commands/ls.ts +1 -1
- package/lib/task/short-id.ts +10 -0
- package/lib/update.ts +28 -10
- package/package.json +1 -1
- package/templates/.agents/hooks/auto-resume.sh +21 -4
- package/templates/.agents/rules/next-step-output.en.md +6 -3
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -3
- package/templates/.agents/rules/pr-checks-commands.en.md +5 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +62 -0
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +62 -0
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/pr-sync.github.en.md +7 -0
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +7 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +8 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +8 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +8 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +8 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +8 -1
- 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/commit/SKILL.en.md +2 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -3
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +31 -23
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +31 -23
- package/templates/.agents/skills/complete-task/SKILL.en.md +36 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +36 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +16 -7
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +16 -7
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -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 +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-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-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- 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/update-agent-infra/scripts/sync-templates.js +0 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +131 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +131 -0
- package/templates/.agents/skills/watch-pr/config/verify.json +22 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +43 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +43 -0
- package/templates/.agents/templates/task.en.md +1 -0
- package/templates/.agents/templates/task.zh-CN.md +1 -0
- package/templates/.agents/workflows/bug-fix.en.yaml +6 -4
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +5 -4
- package/templates/.agents/workflows/feature-development.en.yaml +6 -4
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +5 -4
- package/templates/.agents/workflows/refactoring.en.yaml +6 -4
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +5 -4
- package/templates/.claude/commands/watch-pr.en.md +8 -0
- package/templates/.claude/commands/watch-pr.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/watch-pr.en.toml +8 -0
- package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +8 -0
- package/templates/.opencode/commands/watch-pr.en.md +11 -0
- package/templates/.opencode/commands/watch-pr.zh-CN.md +11 -0
package/README.md
CHANGED
|
@@ -165,6 +165,9 @@ curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.
|
|
|
165
165
|
**Option C - Homebrew (macOS)**
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
+
# Newer Homebrew refuses to load formulae from third-party taps until trusted,
|
|
169
|
+
# which silently blocks upgrades. Trust the tap once before installing.
|
|
170
|
+
brew trust fitlab-ai/tap
|
|
168
171
|
brew install fitlab-ai/tap/agent-infra
|
|
169
172
|
```
|
|
170
173
|
|
|
@@ -218,6 +221,8 @@ The sandbox image also preinstalls `gh`. When `gh auth token` succeeds on the ho
|
|
|
218
221
|
|
|
219
222
|
`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.
|
|
220
223
|
|
|
224
|
+
`ai sandbox start <branch | TASK-id | N | '#N'>` recovers a sandbox container that has stopped — for example after the host Docker daemon was restarted or replaced (a common case is installing OrbStack over an existing Docker), which leaves the container `Exited`. It only starts a container that already exists and is stopped; if none exists it points you to `ai sandbox create`, and a container that is already running is left untouched. `ai sandbox exec <branch>` performs the same recovery automatically: when the target container exists but is stopped, it starts the container first and then enters it. Because each worktree and per-AI state directory is persisted on the host, restarting a stopped container is safe and loses no data.
|
|
225
|
+
|
|
221
226
|
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. Set `AI_SANDBOX_NO_CLIPBOARD_BRIDGE=1` to skip the bridge and enter the normal interactive path directly when diagnosing mouse, scrolling, or other input issues.
|
|
222
227
|
|
|
223
228
|
When you run the sandbox from a remote Mac over SSH, use `ai cp <ssh-alias>` on the Mac in front of you to push the local clipboard image to that remote Mac first. Copy an image with Cmd+C, run `ai cp mini`, then return to the existing SSH session and press `Ctrl+V`; the sandbox bridge reads the remote Mac's NSPasteboard and injects the image as usual. This command handles PNG images only and uses non-interactive ssh/scp with key-based authentication. For now both the sender and the remote must be macOS—the remote NSPasteboard is written via `osascript`—but the remote-write step is the natural extension point for other platforms later.
|
|
@@ -484,7 +489,7 @@ These configurations are not actively tested in this release:
|
|
|
484
489
|
|
|
485
490
|
- **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).
|
|
486
491
|
- **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.
|
|
487
|
-
- `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.
|
|
492
|
+
- `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 start`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm`, and `ai sandbox prune` directly.
|
|
488
493
|
|
|
489
494
|
### Windows
|
|
490
495
|
|
|
@@ -560,6 +565,7 @@ agent-infra ships with **a rich set of built-in AI skills**. They are organized
|
|
|
560
565
|
| Skill | Description | Parameters | Recommended use case |
|
|
561
566
|
|-------|-------------|------------|----------------------|
|
|
562
567
|
| `create-pr` | Open a Pull Request to an inferred or explicit target branch. | `task-id` (optional), `target-branch` (optional) | Publish reviewed changes for merge, with optional explicit task linkage after a fresh session. |
|
|
568
|
+
| `watch-pr` | Watch a PR's required checks and self-heal failures until green. | `task-id` or `--pr <number>` (optional; defaults to the current branch's PR) | Monitor CI after create-pr and auto-fix simple failures before merging. |
|
|
563
569
|
|
|
564
570
|
<a id="code-quality"></a>
|
|
565
571
|
|
package/README.zh-CN.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
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
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>
|
|
22
|
-
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
|
22
|
+
<a href="CONTRIBUTING.zh-CN.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
<p align="center">
|
|
@@ -165,6 +165,9 @@ curl -fsSL https://raw.githubusercontent.com/fitlab-ai/agent-infra/main/install.
|
|
|
165
165
|
**方式 C - Homebrew (macOS)**
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
+
# 新版 Homebrew 默认拒绝加载第三方 tap 的 formula,
|
|
169
|
+
# 会导致升级被静默跳过。首次安装前先信任本 tap。
|
|
170
|
+
brew trust fitlab-ai/tap
|
|
168
171
|
brew install fitlab-ai/tap/agent-infra
|
|
169
172
|
```
|
|
170
173
|
|
|
@@ -218,6 +221,8 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
|
|
|
218
221
|
|
|
219
222
|
`ai sandbox exec` 也会向容器透传一小组终端检测白名单变量(`TERM_PROGRAM`、`TERM_PROGRAM_VERSION`、`LC_TERMINAL`、`LC_TERMINAL_VERSION`)。这样可以让交互式 TUI 保持与宿主终端一致的行为,例如 Claude Code 的 `Shift+Enter` 换行支持,同时避免把整个宿主环境灌入容器。
|
|
220
223
|
|
|
224
|
+
`ai sandbox start <branch | TASK-id | N | '#N'>` 用于恢复已停止的沙箱容器——典型场景是宿主机 Docker daemon 被重启或替换(例如在已有 Docker 上安装 OrbStack 接管),导致容器变成 `Exited`。它只启动「已存在且已停止」的容器;容器不存在时会提示改用 `ai sandbox create`,已在运行的容器则保持不动。`ai sandbox exec <branch>` 会自动执行同样的恢复:当目标容器存在但已停止时,先启动容器再进入。由于每个 worktree 和各 AI 的 state 目录都持久化在宿主机,重启已停止的容器是安全的,不会丢失数据。
|
|
225
|
+
|
|
221
226
|
在 macOS 上,交互式 `ai sandbox exec <branch>` 会尽力桥接宿主图片粘贴。当你按下 `Ctrl+V` 且宿主剪贴板当前是图片时,agent-infra 会从宿主剪贴板读取图片,将 PNG 写到 `~/.agent-infra/clipboard/`,再以 bracketed paste 注入容器内路径,让 Claude Code、Codex、Gemini CLI 和 OpenCode 按图片附件处理。宿主剪贴板只读,不会被改写。该能力会自动降级:已有沙箱需要重建后才有 `/clipboard` 挂载;如果可选 pty 依赖或剪贴板探测不可用,会回退到原本的交互进入方式。排查鼠标、滚动或其他输入异常时,可以设置 `AI_SANDBOX_NO_CLIPBOARD_BRIDGE=1` 跳过桥接,直接进入原本的交互路径。
|
|
222
227
|
|
|
223
228
|
当你通过 SSH 在远端 Mac 上运行沙箱时,可先在手边这台 Mac 上执行 `ai cp <ssh-alias>`,把本机剪贴板图片推送到远端 Mac。典型流程是:Cmd+C 复制图片,运行 `ai cp mini`,回到已有 SSH session 后按 `Ctrl+V`;沙箱桥会读取远端 Mac 的 NSPasteboard,并按原路径注入图片。该命令只处理 PNG 图片,并使用基于 ssh key 的非交互 ssh/scp。目前发送端与远端都需为 macOS(远端通过 `osascript` 写入 NSPasteboard),后续可扩展支持其他远端平台。
|
|
@@ -457,7 +462,7 @@ Rootless 模式的已知差异:
|
|
|
457
462
|
|
|
458
463
|
- 用 **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 在每条命令前打印的提示)。
|
|
459
464
|
- **SELinux enforcing** 宿主机(Fedora / RHEL):`ai sandbox create` 会自动给 bind mount 加 Docker 共享 `:z` 标签,无需手动准备。如需排障可设 `AGENT_INFRA_SELINUX_DISABLE=1` 关闭。
|
|
460
|
-
- `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`。
|
|
465
|
+
- `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox start`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`、`ai sandbox prune`。
|
|
461
466
|
|
|
462
467
|
### Windows
|
|
463
468
|
|
|
@@ -533,6 +538,7 @@ agent-infra 提供 **丰富的内置 AI skills**。它们按使用场景分组
|
|
|
533
538
|
| Skill | 描述 | 参数 | 推荐场景 |
|
|
534
539
|
|-------|------|------|---------|
|
|
535
540
|
| `create-pr` | 向推断出的目标分支或显式指定分支创建 Pull Request。 | `task-id`(可选)、`target-branch`(可选) | 变更准备合入时创建 PR;清空上下文后也可显式传入任务关联。 |
|
|
541
|
+
| `watch-pr` | 监控 PR 的 required checks,失败时自愈直到全绿。 | `task-id` 或 `--pr <number>`(可选;默认取当前分支的 PR) | create-pr 后监控 CI,合入前自动修复简单失败。 |
|
|
536
542
|
|
|
537
543
|
<a id="code-quality"></a>
|
|
538
544
|
|
|
@@ -923,7 +929,7 @@ agent-infra 通过 Git tag 和 GitHub release 使用语义化版本号。当前
|
|
|
923
929
|
|
|
924
930
|
## 参与贡献
|
|
925
931
|
|
|
926
|
-
开发规范请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
932
|
+
开发规范请参阅 [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md)。
|
|
927
933
|
|
|
928
934
|
<a id="license"></a>
|
|
929
935
|
|
package/dist/lib/defaults.json
CHANGED
package/dist/lib/init.js
CHANGED
|
@@ -158,8 +158,6 @@ async function cmdInit() {
|
|
|
158
158
|
info(`Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
|
|
159
159
|
+ ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`);
|
|
160
160
|
}
|
|
161
|
-
const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
|
|
162
|
-
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
163
161
|
let enabledTUIs;
|
|
164
162
|
try {
|
|
165
163
|
enabledTUIs = await multiSelect('Built-in TUI command files to install/manage', BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] })));
|
|
@@ -220,7 +218,6 @@ async function cmdInit() {
|
|
|
220
218
|
org: orgName,
|
|
221
219
|
language,
|
|
222
220
|
platform: { type: platformType },
|
|
223
|
-
requiresPullRequest,
|
|
224
221
|
templateVersion: VERSION,
|
|
225
222
|
sandbox: structuredClone(defaults.sandbox),
|
|
226
223
|
task: structuredClone(defaults.task),
|
|
@@ -1,13 +1,12 @@
|
|
|
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
|
-
import { runInteractiveEngine
|
|
6
|
-
import { resolveTaskBranch } from "../task-resolver.js";
|
|
5
|
+
import { runInteractiveEngine } from "../shell.js";
|
|
7
6
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
8
7
|
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
9
8
|
import { detectHostTimezone } from "../host-timezone.js";
|
|
10
|
-
import {
|
|
9
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
|
|
11
10
|
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
12
11
|
|
|
13
12
|
N (bare) and '#N' both reference the same active task short id from
|
|
@@ -86,19 +85,18 @@ export async function enter(args) {
|
|
|
86
85
|
validateClaudeCredentialsEnvOverride();
|
|
87
86
|
const engine = detectEngine(config);
|
|
88
87
|
const [firstArg = '', ...cmd] = args;
|
|
89
|
-
|
|
90
|
-
if (isTaskShortRef(firstArg)) {
|
|
91
|
-
branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
95
|
-
}
|
|
88
|
+
const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
|
|
96
89
|
assertValidBranchName(branch);
|
|
97
|
-
const running =
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
throw new Error(`No
|
|
90
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
91
|
+
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
|
|
94
|
+
}
|
|
95
|
+
if (!found.running) {
|
|
96
|
+
process.stderr.write(`Sandbox '${found.name}' is stopped; starting it...\n`);
|
|
97
|
+
startSandboxContainer(engine, found.name);
|
|
101
98
|
}
|
|
99
|
+
const container = found.name;
|
|
102
100
|
if (config.tools.includes('claude-code')) {
|
|
103
101
|
try {
|
|
104
102
|
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { runSafeEngine } from "../shell.js";
|
|
4
|
+
import { runSafeEngine, runVerboseEngine } from "../shell.js";
|
|
5
|
+
import { resolveTaskBranch } from "../task-resolver.js";
|
|
5
6
|
export function containerListFormat() {
|
|
6
7
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
7
8
|
}
|
|
@@ -139,4 +140,38 @@ export function resolveTaskShortRef(arg, ctx) {
|
|
|
139
140
|
`'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
|
|
140
141
|
`use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`);
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a sandbox command argument (`<branch | TASK-id | N | '#N'>`) to a
|
|
145
|
+
* branch name, mirroring `ai sandbox exec` so that `start` and `exec` share one
|
|
146
|
+
* input contract. Short refs go through the registry-only resolver (which throws
|
|
147
|
+
* an actionable error on a miss); everything else flows through resolveTaskBranch
|
|
148
|
+
* (plain branch names pass through unchanged, TASK-ids resolve via task.md).
|
|
149
|
+
* Callers still run assertValidBranchName on the result.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveBranchArg(arg, ctx) {
|
|
152
|
+
return isTaskShortRef(arg)
|
|
153
|
+
? resolveTaskShortRef(arg, ctx)
|
|
154
|
+
: resolveTaskBranch(arg, ctx.repoRoot);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Pick the sandbox container row whose name matches one of the candidate
|
|
158
|
+
* container names (covers both the '..' and legacy '-' branch sanitizations).
|
|
159
|
+
* Pure: no IO. Returns null when no row matches.
|
|
160
|
+
*/
|
|
161
|
+
export function selectSandboxContainer(rows, candidates) {
|
|
162
|
+
return rows.find((row) => candidates.includes(row.name)) ?? null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Start an existing (stopped) sandbox container by name. Throws a distinct,
|
|
166
|
+
* actionable error when `docker start` fails, so callers can tell "start failed"
|
|
167
|
+
* apart from "container not found".
|
|
168
|
+
*/
|
|
169
|
+
export function startSandboxContainer(engine, name) {
|
|
170
|
+
try {
|
|
171
|
+
runVerboseEngine(engine, 'docker', ['start', name]);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new Error(`Failed to start sandbox container '${name}': ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
142
177
|
//# sourceMappingURL=list-running.js.map
|
|
@@ -16,10 +16,12 @@ Lists all containers for the current project. The '#' column is a
|
|
|
16
16
|
display-only row number; the 'SHORT' column shows the active task short
|
|
17
17
|
id bound to each container's branch (via
|
|
18
18
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
19
|
-
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11')
|
|
19
|
+
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
20
|
+
A '-' means no active task is bound to that branch, so the sandbox is free
|
|
21
|
+
to remove with "ai sandbox rm <branch>".`;
|
|
20
22
|
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
|
|
21
|
-
export function formatContainerTable(rows) {
|
|
22
|
-
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]));
|
|
23
|
+
export function formatContainerTable(rows, zebra = false) {
|
|
24
|
+
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
|
|
23
25
|
}
|
|
24
26
|
function listChildren(dir) {
|
|
25
27
|
if (!fs.existsSync(dir)) {
|
|
@@ -54,10 +56,13 @@ export function ls(args = []) {
|
|
|
54
56
|
branch: container.branch
|
|
55
57
|
};
|
|
56
58
|
});
|
|
57
|
-
for (const line of formatContainerTable(tableRows)) {
|
|
59
|
+
for (const line of formatContainerTable(tableRows, Boolean(process.stdout.isTTY))) {
|
|
58
60
|
process.stdout.write(` ${line}\n`);
|
|
59
61
|
}
|
|
60
62
|
process.stdout.write(` Total: ${ordered.length} containers\n`);
|
|
63
|
+
if (tableRows.some((r) => r.shortId === '-')) {
|
|
64
|
+
process.stdout.write(` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`);
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
p.log.step('Worktrees');
|
|
63
68
|
const worktrees = listChildren(config.worktreeBase);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
3
|
+
import { detectEngine } from "../engine.js";
|
|
4
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
|
|
5
|
+
const USAGE = `Usage: ai sandbox start <branch | TASK-id | N | '#N'>
|
|
6
|
+
|
|
7
|
+
Start an existing sandbox container that has stopped (for example after the
|
|
8
|
+
Docker daemon was restarted or replaced). The container must already exist:
|
|
9
|
+
if none is found, run 'ai sandbox create <branch>' first. A container that is
|
|
10
|
+
already running is left untouched.`;
|
|
11
|
+
export async function start(args) {
|
|
12
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
13
|
+
process.stdout.write(`${USAGE}\n`);
|
|
14
|
+
if (args.length === 0) {
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const [firstArg = ''] = args;
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const engine = detectEngine(config);
|
|
22
|
+
const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
|
|
23
|
+
assertValidBranchName(branch);
|
|
24
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
25
|
+
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
26
|
+
if (!found) {
|
|
27
|
+
throw new Error(`No sandbox container for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
|
|
28
|
+
}
|
|
29
|
+
if (found.running) {
|
|
30
|
+
process.stdout.write(`Sandbox '${found.name}' is already running.\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
startSandboxContainer(engine, found.name);
|
|
34
|
+
process.stdout.write(`Started sandbox '${found.name}'.\n`);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -6,6 +6,9 @@ Commands:
|
|
|
6
6
|
Enter sandbox or run a command. N (bare) is the
|
|
7
7
|
recommended form for task short ids (e.g.
|
|
8
8
|
'ai sandbox exec 11'); '#N' is also accepted.
|
|
9
|
+
start <branch | TASK-id | N | '#N'>
|
|
10
|
+
Start an existing stopped sandbox container
|
|
11
|
+
(e.g. after the Docker daemon restarted)
|
|
9
12
|
ls List sandboxes for the current project (the '#'
|
|
10
13
|
column is a display-only row number; the 'SHORT'
|
|
11
14
|
column shows the active task short id, '-' if none)
|
|
@@ -50,6 +53,11 @@ export async function runSandbox(args) {
|
|
|
50
53
|
}
|
|
51
54
|
break;
|
|
52
55
|
}
|
|
56
|
+
case 'start': {
|
|
57
|
+
const { start } = await import("./commands/start.js");
|
|
58
|
+
await start(rest);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
53
61
|
case 'ls': {
|
|
54
62
|
const { ls } = await import("./commands/ls.js");
|
|
55
63
|
ls(rest);
|
package/dist/lib/table.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
function formatTable(headers, rows, options = {}) {
|
|
3
|
+
const { zebra = false } = options;
|
|
2
4
|
const columnCount = headers.length;
|
|
3
5
|
const widths = headers.map((header, i) => {
|
|
4
6
|
const headerLen = header.length;
|
|
@@ -23,7 +25,14 @@ function formatTable(headers, rows) {
|
|
|
23
25
|
}
|
|
24
26
|
return parts.join(' ').trimEnd();
|
|
25
27
|
};
|
|
26
|
-
|
|
28
|
+
const dataLines = rows.map((row, i) => {
|
|
29
|
+
const line = renderRow(row);
|
|
30
|
+
// Zebra stripes: dim even-numbered data rows (rows 2, 4, 6... -> 0-based
|
|
31
|
+
// odd index). The header and odd rows are left untouched. When zebra is
|
|
32
|
+
// off, pc.dim is never called, so the output is byte-identical to before.
|
|
33
|
+
return zebra && i % 2 === 1 ? pc.dim(line) : line;
|
|
34
|
+
});
|
|
35
|
+
return [renderRow(headers), ...dataLines];
|
|
27
36
|
}
|
|
28
37
|
export { formatTable };
|
|
29
38
|
//# sourceMappingURL=table.js.map
|
|
@@ -113,7 +113,7 @@ function ls(args = []) {
|
|
|
113
113
|
r.branch,
|
|
114
114
|
r.title
|
|
115
115
|
]);
|
|
116
|
-
for (const line of formatTable(TABLE_HEADERS, tableRows)) {
|
|
116
|
+
for (const line of formatTable(TABLE_HEADERS, tableRows, { zebra: Boolean(process.stdout.isTTY) })) {
|
|
117
117
|
process.stdout.write(`${line}\n`);
|
|
118
118
|
}
|
|
119
119
|
process.stdout.write(`Total: ${rows.length} tasks\n`);
|
|
@@ -58,6 +58,16 @@ function loadShortIdByTaskId(repoRoot) {
|
|
|
58
58
|
}
|
|
59
59
|
return map;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a branch to its active-task short id (`#NN`), or `null` when no
|
|
63
|
+
* active task is bound to that branch.
|
|
64
|
+
*
|
|
65
|
+
* Two-state semantics: this only consults the active registry
|
|
66
|
+
* (`active/.short-ids.json`) plus each `active/{taskId}/task.md`. Tasks moved
|
|
67
|
+
* to completed/blocked/cancelled/archive have already released their short id,
|
|
68
|
+
* so their branches return `null` — in `ai sandbox ls` that surfaces as `-`,
|
|
69
|
+
* meaning the sandbox is free to remove.
|
|
70
|
+
*/
|
|
61
71
|
function lookupShortIdByBranch(branch, repoRoot, _opts) {
|
|
62
72
|
const registry = readRegistry(repoRoot);
|
|
63
73
|
if (!registry)
|
package/dist/lib/update.js
CHANGED
|
@@ -7,6 +7,24 @@ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js"
|
|
|
7
7
|
const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
|
|
8
8
|
const CONFIG_DIR = '.agents';
|
|
9
9
|
const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
|
|
10
|
+
// One-time migration of the legacy project-level PR switch to the three-state
|
|
11
|
+
// `prFlow` preference. `true` (the old default / "PR flow on") maps to the
|
|
12
|
+
// strong constraint `required`; `false` maps to `disabled`. A missing or
|
|
13
|
+
// already-migrated config is left untouched (idempotent). Returns the new
|
|
14
|
+
// prFlow value when a migration happened, otherwise null.
|
|
15
|
+
function migratePrFlow(config) {
|
|
16
|
+
if (config.requiresPullRequest === true) {
|
|
17
|
+
delete config.requiresPullRequest;
|
|
18
|
+
config.prFlow = 'required';
|
|
19
|
+
return 'required';
|
|
20
|
+
}
|
|
21
|
+
if (config.requiresPullRequest === false) {
|
|
22
|
+
delete config.requiresPullRequest;
|
|
23
|
+
config.prFlow = 'disabled';
|
|
24
|
+
return 'disabled';
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
10
28
|
function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
11
29
|
const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
|
|
12
30
|
if (!top.startsWith('.'))
|
|
@@ -134,7 +152,7 @@ async function cmdUpdate() {
|
|
|
134
152
|
const sandboxAdded = !config.sandbox;
|
|
135
153
|
const taskAdded = !config.task;
|
|
136
154
|
const labelsAdded = !config.labels;
|
|
137
|
-
const
|
|
155
|
+
const prFlowMigrated = migratePrFlow(config);
|
|
138
156
|
let configChanged = changed;
|
|
139
157
|
if (platformAdded) {
|
|
140
158
|
config.platform = structuredClone(defaults.platform);
|
|
@@ -152,8 +170,7 @@ async function cmdUpdate() {
|
|
|
152
170
|
config.labels = structuredClone(defaults.labels);
|
|
153
171
|
configChanged = true;
|
|
154
172
|
}
|
|
155
|
-
if (
|
|
156
|
-
config.requiresPullRequest = defaults.requiresPullRequest;
|
|
173
|
+
if (prFlowMigrated) {
|
|
157
174
|
configChanged = true;
|
|
158
175
|
}
|
|
159
176
|
if (configChanged) {
|
|
@@ -167,7 +184,7 @@ async function cmdUpdate() {
|
|
|
167
184
|
ok(` merged: ${entry}`);
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
|
-
else if (platformAdded || sandboxAdded || taskAdded || labelsAdded ||
|
|
187
|
+
else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || prFlowMigrated) {
|
|
171
188
|
if (platformAdded) {
|
|
172
189
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
173
190
|
}
|
|
@@ -180,8 +197,8 @@ async function cmdUpdate() {
|
|
|
180
197
|
if (labelsAdded) {
|
|
181
198
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
182
199
|
}
|
|
183
|
-
if (
|
|
184
|
-
info(`
|
|
200
|
+
if (prFlowMigrated) {
|
|
201
|
+
info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
else {
|
|
@@ -199,8 +216,8 @@ async function cmdUpdate() {
|
|
|
199
216
|
if (hasNewEntries && platformAdded) {
|
|
200
217
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
201
218
|
}
|
|
202
|
-
if (hasNewEntries &&
|
|
203
|
-
info(`
|
|
219
|
+
if (hasNewEntries && prFlowMigrated) {
|
|
220
|
+
info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
|
|
204
221
|
}
|
|
205
222
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
206
223
|
ok(`Updated ${CONFIG_PATH}`);
|
package/lib/defaults.json
CHANGED
package/lib/init.ts
CHANGED
|
@@ -32,7 +32,6 @@ type Defaults = {
|
|
|
32
32
|
sandbox: Record<string, unknown>;
|
|
33
33
|
task: { shortIdLength: number };
|
|
34
34
|
labels: Record<string, unknown>;
|
|
35
|
-
requiresPullRequest: boolean;
|
|
36
35
|
};
|
|
37
36
|
|
|
38
37
|
type AgentConfig = {
|
|
@@ -40,7 +39,6 @@ type AgentConfig = {
|
|
|
40
39
|
org: string;
|
|
41
40
|
language: string;
|
|
42
41
|
platform: { type: string };
|
|
43
|
-
requiresPullRequest: boolean;
|
|
44
42
|
templateVersion: string;
|
|
45
43
|
sandbox: Record<string, unknown>;
|
|
46
44
|
task: { shortIdLength: number };
|
|
@@ -224,13 +222,6 @@ async function cmdInit(): Promise<void> {
|
|
|
224
222
|
);
|
|
225
223
|
}
|
|
226
224
|
|
|
227
|
-
const requiresPRChoice = await select(
|
|
228
|
-
'Require Pull Request flow?',
|
|
229
|
-
['yes', 'no'],
|
|
230
|
-
'yes'
|
|
231
|
-
);
|
|
232
|
-
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
233
|
-
|
|
234
225
|
let enabledTUIs: string[];
|
|
235
226
|
try {
|
|
236
227
|
enabledTUIs = await multiSelect(
|
|
@@ -324,7 +315,6 @@ async function cmdInit(): Promise<void> {
|
|
|
324
315
|
org: orgName,
|
|
325
316
|
language,
|
|
326
317
|
platform: { type: platformType },
|
|
327
|
-
requiresPullRequest,
|
|
328
318
|
templateVersion: VERSION,
|
|
329
319
|
sandbox: structuredClone(defaults.sandbox),
|
|
330
320
|
task: structuredClone(defaults.task),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from '../config.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertValidBranchName,
|
|
4
|
+
containerNameCandidates,
|
|
5
|
+
sandboxBranchLabel,
|
|
6
|
+
sandboxLabel
|
|
7
|
+
} from '../constants.ts';
|
|
3
8
|
import { detectEngine } from '../engine.ts';
|
|
4
9
|
import {
|
|
5
10
|
formatCredentialWarnings,
|
|
@@ -8,12 +13,16 @@ import {
|
|
|
8
13
|
redactCommandError,
|
|
9
14
|
validateClaudeCredentialsEnvOverride
|
|
10
15
|
} from '../credentials.ts';
|
|
11
|
-
import { runInteractiveEngine
|
|
12
|
-
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
16
|
+
import { runInteractiveEngine } from '../shell.ts';
|
|
13
17
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
14
18
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
|
|
15
19
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
16
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
fetchSandboxRows,
|
|
22
|
+
resolveBranchArg,
|
|
23
|
+
selectSandboxContainer,
|
|
24
|
+
startSandboxContainer
|
|
25
|
+
} from './list-running.ts';
|
|
17
26
|
|
|
18
27
|
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
19
28
|
|
|
@@ -122,19 +131,29 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
122
131
|
validateClaudeCredentialsEnvOverride();
|
|
123
132
|
const engine = detectEngine(config);
|
|
124
133
|
const [firstArg = '', ...cmd] = args;
|
|
125
|
-
|
|
126
|
-
if (isTaskShortRef(firstArg)) {
|
|
127
|
-
branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
|
|
128
|
-
} else {
|
|
129
|
-
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
130
|
-
}
|
|
134
|
+
const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
|
|
131
135
|
assertValidBranchName(branch);
|
|
132
|
-
const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
133
|
-
const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const { running, nonRunning } = fetchSandboxRows(
|
|
138
|
+
engine,
|
|
139
|
+
sandboxLabel(config),
|
|
140
|
+
sandboxBranchLabel(config)
|
|
141
|
+
);
|
|
142
|
+
const found = selectSandboxContainer(
|
|
143
|
+
[...running, ...nonRunning],
|
|
144
|
+
containerNameCandidates(config, branch)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (!found) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (!found.running) {
|
|
153
|
+
process.stderr.write(`Sandbox '${found.name}' is stopped; starting it...\n`);
|
|
154
|
+
startSandboxContainer(engine, found.name);
|
|
137
155
|
}
|
|
156
|
+
const container = found.name;
|
|
138
157
|
|
|
139
158
|
if (config.tools.includes('claude-code')) {
|
|
140
159
|
try {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { runSafeEngine } from '../shell.ts';
|
|
4
|
+
import { runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
5
|
+
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
5
6
|
|
|
6
7
|
export type SandboxRow = {
|
|
7
8
|
name: string;
|
|
@@ -172,3 +173,44 @@ export function resolveTaskShortRef(
|
|
|
172
173
|
`use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`
|
|
173
174
|
);
|
|
174
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a sandbox command argument (`<branch | TASK-id | N | '#N'>`) to a
|
|
179
|
+
* branch name, mirroring `ai sandbox exec` so that `start` and `exec` share one
|
|
180
|
+
* input contract. Short refs go through the registry-only resolver (which throws
|
|
181
|
+
* an actionable error on a miss); everything else flows through resolveTaskBranch
|
|
182
|
+
* (plain branch names pass through unchanged, TASK-ids resolve via task.md).
|
|
183
|
+
* Callers still run assertValidBranchName on the result.
|
|
184
|
+
*/
|
|
185
|
+
export function resolveBranchArg(arg: string, ctx: { repoRoot: string }): string {
|
|
186
|
+
return isTaskShortRef(arg)
|
|
187
|
+
? resolveTaskShortRef(arg, ctx)
|
|
188
|
+
: resolveTaskBranch(arg, ctx.repoRoot);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pick the sandbox container row whose name matches one of the candidate
|
|
193
|
+
* container names (covers both the '..' and legacy '-' branch sanitizations).
|
|
194
|
+
* Pure: no IO. Returns null when no row matches.
|
|
195
|
+
*/
|
|
196
|
+
export function selectSandboxContainer(
|
|
197
|
+
rows: SandboxRow[],
|
|
198
|
+
candidates: string[]
|
|
199
|
+
): SandboxRow | null {
|
|
200
|
+
return rows.find((row) => candidates.includes(row.name)) ?? null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Start an existing (stopped) sandbox container by name. Throws a distinct,
|
|
205
|
+
* actionable error when `docker start` fails, so callers can tell "start failed"
|
|
206
|
+
* apart from "container not found".
|
|
207
|
+
*/
|
|
208
|
+
export function startSandboxContainer(engine: string, name: string): void {
|
|
209
|
+
try {
|
|
210
|
+
runVerboseEngine(engine, 'docker', ['start', name]);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to start sandbox container '${name}': ${error instanceof Error ? error.message : 'unknown error'}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -18,7 +18,9 @@ Lists all containers for the current project. The '#' column is a
|
|
|
18
18
|
display-only row number; the 'SHORT' column shows the active task short
|
|
19
19
|
id bound to each container's branch (via
|
|
20
20
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
21
|
-
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11')
|
|
21
|
+
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
22
|
+
A '-' means no active task is bound to that branch, so the sandbox is free
|
|
23
|
+
to remove with "ai sandbox rm <branch>".`;
|
|
22
24
|
|
|
23
25
|
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
24
26
|
|
|
@@ -30,10 +32,11 @@ type ContainerTableRow = {
|
|
|
30
32
|
branch: string;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
|
-
export function formatContainerTable(rows: ContainerTableRow[]): string[] {
|
|
35
|
+
export function formatContainerTable(rows: ContainerTableRow[], zebra = false): string[] {
|
|
34
36
|
return formatTable(
|
|
35
37
|
CONTAINER_TABLE_HEADERS,
|
|
36
|
-
rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch])
|
|
38
|
+
rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]),
|
|
39
|
+
{ zebra }
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -74,10 +77,15 @@ export function ls(args: string[] = []): void {
|
|
|
74
77
|
branch: container.branch
|
|
75
78
|
};
|
|
76
79
|
});
|
|
77
|
-
for (const line of formatContainerTable(tableRows)) {
|
|
80
|
+
for (const line of formatContainerTable(tableRows, Boolean(process.stdout.isTTY))) {
|
|
78
81
|
process.stdout.write(` ${line}\n`);
|
|
79
82
|
}
|
|
80
83
|
process.stdout.write(` Total: ${ordered.length} containers\n`);
|
|
84
|
+
if (tableRows.some((r) => r.shortId === '-')) {
|
|
85
|
+
process.stdout.write(
|
|
86
|
+
` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
p.log.step('Worktrees');
|