@fitlab-ai/agent-infra 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -8
- package/README.zh-CN.md +176 -8
- package/bin/{cli.js → cli.ts} +23 -19
- package/dist/bin/cli.js +116 -0
- package/dist/lib/defaults.json +61 -0
- package/dist/lib/init.js +238 -0
- package/dist/lib/log.js +18 -0
- package/dist/lib/merge.js +747 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/prompt.js +85 -0
- package/dist/lib/render.js +139 -0
- package/dist/lib/sandbox/commands/create.js +1173 -0
- package/dist/lib/sandbox/commands/enter.js +98 -0
- package/dist/lib/sandbox/commands/ls.js +93 -0
- package/dist/lib/sandbox/commands/rebuild.js +101 -0
- package/dist/lib/sandbox/commands/refresh.js +85 -0
- package/dist/lib/sandbox/commands/rm.js +226 -0
- package/dist/lib/sandbox/commands/vm.js +144 -0
- package/dist/lib/sandbox/config.js +85 -0
- package/dist/lib/sandbox/constants.js +104 -0
- package/dist/lib/sandbox/credentials.js +437 -0
- package/dist/lib/sandbox/dockerfile.js +76 -0
- package/dist/lib/sandbox/dotfiles.js +170 -0
- package/dist/lib/sandbox/engine.js +155 -0
- package/dist/lib/sandbox/engines/colima.js +64 -0
- package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
- package/dist/lib/sandbox/engines/index.js +25 -0
- package/dist/lib/sandbox/engines/native.js +96 -0
- package/dist/lib/sandbox/engines/orbstack.js +63 -0
- package/dist/lib/sandbox/engines/selinux.js +48 -0
- package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
- package/dist/lib/sandbox/engines/wsl2.js +57 -0
- package/dist/lib/sandbox/index.js +70 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
- package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +148 -0
- package/dist/lib/sandbox/task-resolver.js +35 -0
- package/dist/lib/sandbox/tools.js +115 -0
- package/dist/lib/update.js +186 -0
- package/dist/lib/version.js +5 -0
- package/dist/package.json +5 -0
- package/lib/{init.js → init.ts} +64 -20
- package/lib/{log.js → log.ts} +4 -4
- package/lib/{merge.js → merge.ts} +129 -63
- package/lib/paths.ts +18 -0
- package/lib/{prompt.js → prompt.ts} +12 -12
- package/lib/{render.js → render.ts} +30 -17
- package/lib/sandbox/commands/create.ts +1507 -0
- package/lib/sandbox/commands/enter.ts +115 -0
- package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
- package/lib/sandbox/commands/rebuild.ts +135 -0
- package/lib/sandbox/commands/refresh.ts +128 -0
- package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
- package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
- package/lib/sandbox/config.ts +133 -0
- package/lib/sandbox/{constants.js → constants.ts} +41 -17
- package/lib/sandbox/credentials.ts +634 -0
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/dotfiles.ts +236 -0
- package/lib/sandbox/engine.ts +231 -0
- package/lib/sandbox/engines/colima.ts +81 -0
- package/lib/sandbox/engines/docker-desktop.ts +36 -0
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/native.ts +131 -0
- package/lib/sandbox/engines/orbstack.ts +78 -0
- package/lib/sandbox/engines/selinux.ts +66 -0
- package/lib/sandbox/engines/wsl2-paths.ts +65 -0
- package/lib/sandbox/engines/wsl2.ts +74 -0
- package/lib/sandbox/{index.js → index.ts} +17 -8
- package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
- package/lib/sandbox/runtimes/base.dockerfile +116 -1
- package/lib/sandbox/shell.ts +186 -0
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +33 -29
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +22 -12
- package/templates/.agents/rules/create-issue.github.en.md +2 -4
- package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
- package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
- package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
- 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-issue/SKILL.en.md +6 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
- package/lib/paths.js +0 -9
- package/lib/sandbox/commands/create.js +0 -1174
- package/lib/sandbox/commands/enter.js +0 -79
- package/lib/sandbox/commands/rebuild.js +0 -102
- package/lib/sandbox/config.js +0 -84
- package/lib/sandbox/engine.js +0 -256
- package/lib/sandbox/shell.js +0 -122
- package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
- /package/lib/{version.js → version.ts} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
USER devuser
|
|
2
|
+
ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
|
|
3
|
+
ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
|
|
4
|
+
|
|
5
|
+
ARG AI_TOOL_PACKAGES
|
|
6
|
+
RUN if [ -z "${AI_TOOL_PACKAGES}" ]; then \
|
|
7
|
+
echo "AI_TOOL_PACKAGES build arg is required"; \
|
|
8
|
+
exit 1; \
|
|
9
|
+
fi && \
|
|
10
|
+
set -e && \
|
|
11
|
+
for pkg in ${AI_TOOL_PACKAGES}; do \
|
|
12
|
+
npm install -g "$pkg"; \
|
|
13
|
+
done
|
|
14
|
+
|
|
15
|
+
RUN npm install -g pyright
|
|
16
|
+
|
|
17
|
+
RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
|
|
18
|
+
|
|
19
|
+
RUN git config --global --add safe.directory /workspace
|
|
20
|
+
|
|
21
|
+
# Host shell-config is bind-mounted as a directory at this path; the four files
|
|
22
|
+
# inside (.gitconfig, .gitignore_global, .stCommitMsg, .bash_aliases) are exposed
|
|
23
|
+
# via symlinks in $HOME. Directory binds avoid the //deleted invalidation that
|
|
24
|
+
# single-file binds suffer when their source is rewritten on macOS/virtiofs.
|
|
25
|
+
RUN mkdir -p /home/devuser/.host-shell-config && \
|
|
26
|
+
ln -sf .host-shell-config/.gitconfig /home/devuser/.gitconfig && \
|
|
27
|
+
ln -sf .host-shell-config/.gitignore_global /home/devuser/.gitignore_global && \
|
|
28
|
+
ln -sf .host-shell-config/.stCommitMsg /home/devuser/.stCommitMsg && \
|
|
29
|
+
ln -sf .host-shell-config/.bash_aliases /home/devuser/.bash_aliases
|
|
30
|
+
|
|
31
|
+
RUN echo 'export NPM_CONFIG_PREFIX=/home/devuser/.npm-global' >> /home/devuser/.bashrc && \
|
|
32
|
+
echo 'export PATH="/home/devuser/.npm-global/bin:${PATH}"' >> /home/devuser/.bashrc && \
|
|
33
|
+
echo 'export GIT_CONFIG_GLOBAL=/home/devuser/.gitconfig' >> /home/devuser/.bashrc && \
|
|
34
|
+
echo 'export GPG_TTY=$(tty)' >> /home/devuser/.bashrc && \
|
|
35
|
+
echo '[ -f ~/.bash_aliases ] && . ~/.bash_aliases' >> /home/devuser/.bashrc
|
|
36
|
+
|
|
37
|
+
WORKDIR /workspace
|
|
38
|
+
|
|
39
|
+
CMD ["tail", "-f", "/dev/null"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
FROM ubuntu:22.04
|
|
2
|
+
|
|
3
|
+
LABEL description="AI coding sandbox"
|
|
4
|
+
|
|
5
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
6
|
+
ENV TZ=Asia/Shanghai
|
|
7
|
+
|
|
8
|
+
ARG HOST_UID=1000
|
|
9
|
+
ARG HOST_GID=1000
|
|
10
|
+
# Root host uid 0 collides with container root; -o lets devuser share uid 0
|
|
11
|
+
# while keeping a real passwd entry that USER devuser can resolve.
|
|
12
|
+
RUN if [ "${HOST_UID}" = "0" ]; then \
|
|
13
|
+
(groupadd -o -g ${HOST_GID} devuser || true) && \
|
|
14
|
+
useradd -o -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
15
|
+
else \
|
|
16
|
+
(groupadd -g ${HOST_GID} devuser || true) && \
|
|
17
|
+
useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
RUN apt-get update && apt-get install -y \
|
|
21
|
+
curl wget git vim file jq \
|
|
22
|
+
build-essential ca-certificates gnupg lsb-release \
|
|
23
|
+
libevent-core-2.1-7 libncursesw6 libtinfo6 \
|
|
24
|
+
pkg-config bison libevent-dev libncurses-dev \
|
|
25
|
+
locales \
|
|
26
|
+
&& locale-gen en_US.UTF-8 \
|
|
27
|
+
&& (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
28
|
+
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg) \
|
|
29
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
30
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
31
|
+
&& apt-get update && apt-get install -y gh \
|
|
32
|
+
&& TMUX_VERSION=3.6a \
|
|
33
|
+
&& wget -qO /tmp/tmux.tar.gz \
|
|
34
|
+
"https://github.com/tmux/tmux/releases/download/${TMUX_VERSION}/tmux-${TMUX_VERSION}.tar.gz" \
|
|
35
|
+
&& tar xzf /tmp/tmux.tar.gz -C /tmp \
|
|
36
|
+
&& cd /tmp/tmux-${TMUX_VERSION} \
|
|
37
|
+
&& ./configure --prefix=/usr/local \
|
|
38
|
+
&& make -j"$(nproc)" \
|
|
39
|
+
&& make install \
|
|
40
|
+
&& cd / \
|
|
41
|
+
&& rm -rf /tmp/tmux.tar.gz /tmp/tmux-${TMUX_VERSION} \
|
|
42
|
+
&& apt-get purge -y pkg-config bison libevent-dev libncurses-dev \
|
|
43
|
+
&& apt-get autoremove -y \
|
|
44
|
+
&& apt-get clean \
|
|
45
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
46
|
+
|
|
47
|
+
# Enable extended keys in CSI u format so Shift+Enter and other modified
|
|
48
|
+
# keys are forwarded through tmux. Preserve terminal-detection variables
|
|
49
|
+
# injected at `docker exec` time when new tmux sessions are created.
|
|
50
|
+
RUN printf '%s\n' \
|
|
51
|
+
'set -g extended-keys always' \
|
|
52
|
+
'set -g extended-keys-format csi-u' \
|
|
53
|
+
"set -as terminal-features 'xterm*:extkeys'" \
|
|
54
|
+
"set -ga update-environment 'TERM_PROGRAM TERM_PROGRAM_VERSION LC_TERMINAL LC_TERMINAL_VERSION'" \
|
|
55
|
+
'set -g mouse on' \
|
|
56
|
+
'set -g status-interval 1' \
|
|
57
|
+
'set -g status-right-length 80' \
|
|
58
|
+
"set -g status-right '#(/usr/local/bin/cc-token-status) | %H:%M'" \
|
|
59
|
+
> /etc/tmux.conf
|
|
60
|
+
|
|
61
|
+
RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
|
|
62
|
+
#!/bin/sh
|
|
63
|
+
set -eu
|
|
64
|
+
|
|
65
|
+
CRED_FILE="/home/devuser/.claude/.credentials.json"
|
|
66
|
+
[ -r "$CRED_FILE" ] || exit 0
|
|
67
|
+
|
|
68
|
+
EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
|
|
69
|
+
case "$EXPIRES_MS" in
|
|
70
|
+
''|*[!0-9]*) exit 0 ;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
NOW_MS=$(($(date +%s) * 1000))
|
|
74
|
+
DIFF_MS=$((EXPIRES_MS - NOW_MS))
|
|
75
|
+
DIFF_S=$((DIFF_MS / 1000))
|
|
76
|
+
|
|
77
|
+
DIM='#[fg=colour245]'
|
|
78
|
+
YELLOW='#[fg=yellow]'
|
|
79
|
+
YELLOW_BOLD='#[fg=yellow,bold]'
|
|
80
|
+
RED_BOLD='#[fg=red,bold]'
|
|
81
|
+
RED_REV='#[fg=red,reverse]'
|
|
82
|
+
RESET='#[default]'
|
|
83
|
+
|
|
84
|
+
if [ "$DIFF_S" -le 0 ]; then
|
|
85
|
+
ELAPSED=$(( -DIFF_S ))
|
|
86
|
+
M=$((ELAPSED / 60))
|
|
87
|
+
printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
|
|
88
|
+
elif [ "$DIFF_S" -lt 60 ]; then
|
|
89
|
+
printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
|
|
90
|
+
elif [ "$DIFF_S" -lt 300 ]; then
|
|
91
|
+
M=$((DIFF_S / 60))
|
|
92
|
+
S=$((DIFF_S % 60))
|
|
93
|
+
printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
|
|
94
|
+
elif [ "$DIFF_S" -lt 1800 ]; then
|
|
95
|
+
M=$((DIFF_S / 60))
|
|
96
|
+
printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
|
|
97
|
+
elif [ "$DIFF_S" -lt 3600 ]; then
|
|
98
|
+
M=$((DIFF_S / 60))
|
|
99
|
+
printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
|
|
100
|
+
else
|
|
101
|
+
TOTAL_M=$((DIFF_S / 60))
|
|
102
|
+
H=$((TOTAL_M / 60))
|
|
103
|
+
M=$((TOTAL_M % 60))
|
|
104
|
+
printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
|
|
105
|
+
fi
|
|
106
|
+
SCRIPT
|
|
107
|
+
|
|
108
|
+
RUN cat > /usr/local/bin/sandbox-dotfiles-link <<'SCRIPT' && chmod +x /usr/local/bin/sandbox-dotfiles-link
|
|
109
|
+
#!/bin/sh
|
|
110
|
+
# Mirror /dotfiles/ tree as symlinks under $HOME/, overwriting any image-baked
|
|
111
|
+
# defaults. Future preferences only need to land in the host directory.
|
|
112
|
+
set -eu
|
|
113
|
+
|
|
114
|
+
DOTFILES_SRC=/dotfiles
|
|
115
|
+
[ -d "$DOTFILES_SRC" ] || exit 0
|
|
116
|
+
|
|
117
|
+
cd "$DOTFILES_SRC"
|
|
118
|
+
find . -type f -print | while IFS= read -r rel; do
|
|
119
|
+
rel=${rel#./}
|
|
120
|
+
target="$HOME/$rel"
|
|
121
|
+
case "$rel" in
|
|
122
|
+
.ssh|.ssh/*|\
|
|
123
|
+
.gnupg|.gnupg/*|\
|
|
124
|
+
.claude|.claude/*|\
|
|
125
|
+
.codex|.codex/*|\
|
|
126
|
+
.gemini|.gemini/*|\
|
|
127
|
+
.config/opencode|.config/opencode/*|\
|
|
128
|
+
.local/share/opencode|.local/share/opencode/*|\
|
|
129
|
+
.host-shell-config|.host-shell-config/*|\
|
|
130
|
+
.gitconfig|.gitignore_global|.stCommitMsg|.bash_aliases)
|
|
131
|
+
continue ;;
|
|
132
|
+
esac
|
|
133
|
+
|
|
134
|
+
mkdir -p "$(dirname "$target")"
|
|
135
|
+
if [ -d "$target" ] && [ ! -L "$target" ]; then
|
|
136
|
+
printf 'sandbox-dotfiles-link: skipping %s (existing directory; use nested path like %s/<file> instead)\n' "$target" "$rel" >&2
|
|
137
|
+
continue
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
ln -sfn "$DOTFILES_SRC/$rel" "$target" 2>/dev/null \
|
|
141
|
+
|| printf 'sandbox-dotfiles-link: failed to link %s\n' "$target" >&2
|
|
142
|
+
done
|
|
143
|
+
SCRIPT
|
|
144
|
+
|
|
145
|
+
RUN cat > /usr/local/bin/sandbox-tmux-entry <<'SCRIPT' && chmod +x /usr/local/bin/sandbox-tmux-entry
|
|
146
|
+
#!/bin/sh
|
|
147
|
+
set -eu
|
|
148
|
+
|
|
149
|
+
sandbox-dotfiles-link >/dev/null || true
|
|
150
|
+
|
|
151
|
+
SESSION=work
|
|
152
|
+
|
|
153
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
154
|
+
exec bash
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
158
|
+
exec tmux new-session -s "$SESSION"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | \
|
|
162
|
+
while read -r name attached; do
|
|
163
|
+
[ "$name" = "$SESSION" ] && continue
|
|
164
|
+
case "$name" in
|
|
165
|
+
''|*[!0-9]*) continue ;;
|
|
166
|
+
esac
|
|
167
|
+
[ "$attached" = "0" ] && tmux kill-session -t "$name" 2>/dev/null || true
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
exec tmux new-session -t "$SESSION"
|
|
171
|
+
SCRIPT
|
|
172
|
+
|
|
173
|
+
ENV LANG=en_US.UTF-8
|
|
174
|
+
ENV LC_ALL=en_US.UTF-8
|
|
175
|
+
ENV TERM=xterm-256color
|
|
176
|
+
ENV COLORTERM=truecolor
|
|
177
|
+
|
|
178
|
+
RUN ln -s /workspace /home/devuser/workspace
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
5
|
+
function normalizeOptions(opts = {}, stdio) {
|
|
6
|
+
return {
|
|
7
|
+
cwd: opts.cwd,
|
|
8
|
+
encoding: opts.encoding,
|
|
9
|
+
stdio,
|
|
10
|
+
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function resolveCommand(cmd) {
|
|
14
|
+
if (process.platform !== 'win32' || path.extname(cmd)) {
|
|
15
|
+
return cmd;
|
|
16
|
+
}
|
|
17
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
18
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
19
|
+
.split(';')
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
22
|
+
for (const extension of extensions) {
|
|
23
|
+
const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
|
|
24
|
+
if (fs.existsSync(candidate)) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
|
|
28
|
+
if (fs.existsSync(upperCandidate)) {
|
|
29
|
+
return upperCandidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return cmd;
|
|
34
|
+
}
|
|
35
|
+
function commandOptions(cmd, opts) {
|
|
36
|
+
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
37
|
+
return { ...opts, shell: true };
|
|
38
|
+
}
|
|
39
|
+
return opts;
|
|
40
|
+
}
|
|
41
|
+
export function run(cmd, args, opts = {}) {
|
|
42
|
+
const resolved = resolveCommand(cmd);
|
|
43
|
+
return execFileSync(resolved, args, commandOptions(resolved, {
|
|
44
|
+
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
45
|
+
encoding: 'utf8'
|
|
46
|
+
})).trim();
|
|
47
|
+
}
|
|
48
|
+
export function runOk(cmd, args, opts = {}) {
|
|
49
|
+
const resolved = resolveCommand(cmd);
|
|
50
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
|
|
51
|
+
return result.status === 0;
|
|
52
|
+
}
|
|
53
|
+
export function restoreTerminal() {
|
|
54
|
+
if (!process.stdout.isTTY) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
process.stdout.write([
|
|
59
|
+
'\x1b[?1049l',
|
|
60
|
+
'\x1b[?25h',
|
|
61
|
+
'\x1b>',
|
|
62
|
+
'\x1b[?1000l',
|
|
63
|
+
'\x1b[?1002l',
|
|
64
|
+
'\x1b[?1003l',
|
|
65
|
+
'\x1b[?1006l'
|
|
66
|
+
].join(''));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best-effort cleanup only; preserve the original command result.
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === 'win32') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
execFileSync('stty', ['sane'], { stdio: 'inherit' });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Some environments do not provide stty or reject sane; ANSI reset still helps.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function runInteractive(cmd, args, opts = {}) {
|
|
82
|
+
const resolved = resolveCommand(cmd);
|
|
83
|
+
try {
|
|
84
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
85
|
+
return result.status ?? 1;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
restoreTerminal();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function runVerbose(cmd, args, opts = {}) {
|
|
92
|
+
const resolved = resolveCommand(cmd);
|
|
93
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
94
|
+
if (result.status !== 0) {
|
|
95
|
+
if (result.signal === 'SIGTERM') {
|
|
96
|
+
throw new Error(`Command timed out after ${opts.timeout ?? DEFAULT_TIMEOUT_MS}ms: ${cmd}`);
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Command failed with exit code ${result.status}: ${cmd}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function runSafe(cmd, args, opts = {}) {
|
|
102
|
+
const resolved = resolveCommand(cmd);
|
|
103
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, {
|
|
104
|
+
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
}));
|
|
107
|
+
if (result.status !== 0 && result.stderr) {
|
|
108
|
+
process.stderr.write(result.stderr);
|
|
109
|
+
}
|
|
110
|
+
return (result.stdout ?? '').trim();
|
|
111
|
+
}
|
|
112
|
+
export function commandForEngine(engine, cmd, args = []) {
|
|
113
|
+
if (engine === 'wsl2') {
|
|
114
|
+
const resolvedWrapper = resolveCommand('wsl.exe');
|
|
115
|
+
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
116
|
+
}
|
|
117
|
+
return { cmd, args };
|
|
118
|
+
}
|
|
119
|
+
export function runEngine(engine, cmd, args, opts = {}) {
|
|
120
|
+
const command = commandForEngine(engine, cmd, args);
|
|
121
|
+
return run(command.cmd, command.args, opts);
|
|
122
|
+
}
|
|
123
|
+
export function execEngine(engine, cmd, args, opts = {}) {
|
|
124
|
+
const command = commandForEngine(engine, cmd, args);
|
|
125
|
+
return execFileSync(command.cmd, command.args, opts);
|
|
126
|
+
}
|
|
127
|
+
export function runOkEngine(engine, cmd, args, opts = {}) {
|
|
128
|
+
const command = commandForEngine(engine, cmd, args);
|
|
129
|
+
return runOk(command.cmd, command.args, opts);
|
|
130
|
+
}
|
|
131
|
+
export function runSafeEngine(engine, cmd, args, opts = {}) {
|
|
132
|
+
const command = commandForEngine(engine, cmd, args);
|
|
133
|
+
return runSafe(command.cmd, command.args, opts);
|
|
134
|
+
}
|
|
135
|
+
export function runVerboseEngine(engine, cmd, args, opts = {}) {
|
|
136
|
+
const command = commandForEngine(engine, cmd, args);
|
|
137
|
+
return runVerbose(command.cmd, command.args, opts);
|
|
138
|
+
}
|
|
139
|
+
export function runInteractiveEngine(engine, cmd, args, opts = {}) {
|
|
140
|
+
const command = commandForEngine(engine, cmd, args);
|
|
141
|
+
return runInteractive(command.cmd, command.args, opts);
|
|
142
|
+
}
|
|
143
|
+
export function runProbe(cmd, args, opts = {}) {
|
|
144
|
+
const { spawnFn = spawnSync, ...commandOpts } = opts;
|
|
145
|
+
const resolved = resolveCommand(cmd);
|
|
146
|
+
return spawnFn(resolved, args, commandOptions(resolved, normalizeOptions({ encoding: 'utf8', ...commandOpts }, commandOpts.stdio ?? ['pipe', 'pipe', 'pipe'])));
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
4
|
+
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
5
|
+
function stripQuotes(value) {
|
|
6
|
+
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
7
|
+
}
|
|
8
|
+
function readTaskContent(repoRoot, taskId) {
|
|
9
|
+
for (const dir of WORKSPACE_DIRS) {
|
|
10
|
+
const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
|
|
11
|
+
if (fs.existsSync(taskPath)) {
|
|
12
|
+
return fs.readFileSync(taskPath, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
16
|
+
}
|
|
17
|
+
function resolveBranchFromTaskContent(content, taskId) {
|
|
18
|
+
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
19
|
+
if (frontmatterBranch?.[1]?.trim()) {
|
|
20
|
+
return stripQuotes(frontmatterBranch[1].trim());
|
|
21
|
+
}
|
|
22
|
+
const contextBranch = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
|
|
23
|
+
if (contextBranch?.[1]?.trim()) {
|
|
24
|
+
return stripQuotes(contextBranch[1].trim());
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Task ${taskId} has no branch field in task.md`);
|
|
27
|
+
}
|
|
28
|
+
export function resolveTaskBranch(arg, repoRoot) {
|
|
29
|
+
if (!TASK_ID_RE.test(arg)) {
|
|
30
|
+
return arg;
|
|
31
|
+
}
|
|
32
|
+
const content = readTaskContent(repoRoot, arg);
|
|
33
|
+
return resolveBranchFromTaskContent(content, arg);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=task-resolver.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
|
|
2
|
+
import { hostJoin } from "./engines/wsl2-paths.js";
|
|
3
|
+
function createBuiltinTools(home, project) {
|
|
4
|
+
return {
|
|
5
|
+
'claude-code': {
|
|
6
|
+
id: 'claude-code',
|
|
7
|
+
name: 'Claude Code',
|
|
8
|
+
npmPackage: '@anthropic-ai/claude-code',
|
|
9
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
10
|
+
containerMount: '/home/devuser/.claude',
|
|
11
|
+
versionCmd: 'claude --version',
|
|
12
|
+
setupHint: 'Authenticates via host credentials live-mounted at ~/.claude/.credentials.json',
|
|
13
|
+
// Claude Code stores user data (.claude.json — onboarding state, theme,
|
|
14
|
+
// workspace trust) at $HOME/.claude.json by default, which sits OUTSIDE
|
|
15
|
+
// the bind-mounted /home/devuser/.claude tree, so our preseeded
|
|
16
|
+
// .claude.json never gets read and the theme picker re-runs on every
|
|
17
|
+
// container start. Pinning CLAUDE_CONFIG_DIR to the tool mount relocates
|
|
18
|
+
// .claude.json into the same directory as .credentials.json/settings.json,
|
|
19
|
+
// letting ensureClaudeOnboarding actually take effect.
|
|
20
|
+
envVars: { CLAUDE_CONFIG_DIR: '/home/devuser/.claude' },
|
|
21
|
+
hostPreSeedDirs: [
|
|
22
|
+
{ hostDir: hostJoin(home, '.claude', 'plugins'), sandboxSubdir: 'plugins' }
|
|
23
|
+
],
|
|
24
|
+
pathRewriteFiles: [
|
|
25
|
+
'plugins/installed_plugins.json',
|
|
26
|
+
'plugins/known_marketplaces.json'
|
|
27
|
+
],
|
|
28
|
+
hostLiveMounts: [
|
|
29
|
+
{
|
|
30
|
+
hostPath: hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code', '.credentials.json'),
|
|
31
|
+
containerSubpath: '.credentials.json'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
codex: {
|
|
36
|
+
id: 'codex',
|
|
37
|
+
name: 'Codex',
|
|
38
|
+
npmPackage: '@openai/codex',
|
|
39
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
|
|
40
|
+
containerMount: '/home/devuser/.codex',
|
|
41
|
+
versionCmd: 'codex --version',
|
|
42
|
+
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
43
|
+
hostLiveMounts: [
|
|
44
|
+
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
45
|
+
],
|
|
46
|
+
postSetupCmds: [
|
|
47
|
+
'test -d /workspace/.codex/commands && ln -sfn /workspace/.codex/commands /home/devuser/.codex/prompts || true'
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
opencode: {
|
|
51
|
+
id: 'opencode',
|
|
52
|
+
name: 'OpenCode',
|
|
53
|
+
npmPackage: 'opencode-ai',
|
|
54
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
|
|
55
|
+
containerMount: '/home/devuser/.local/share/opencode',
|
|
56
|
+
versionCmd: 'opencode version',
|
|
57
|
+
setupHint: 'Configure OpenCode credentials inside the container before first use.',
|
|
58
|
+
// OpenCode reads opencode.json from $XDG_CONFIG_HOME/opencode by default,
|
|
59
|
+
// outside this tool mount. Pin the config file path so the inherited
|
|
60
|
+
// sandbox opencode.json is the one the TUI actually reads.
|
|
61
|
+
envVars: { OPENCODE_CONFIG: '/home/devuser/.local/share/opencode/opencode.json' },
|
|
62
|
+
hostLiveMounts: [
|
|
63
|
+
{
|
|
64
|
+
hostPath: hostJoin(home, '.local', 'share', 'opencode', 'auth.json'),
|
|
65
|
+
containerSubpath: 'auth.json'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
'gemini-cli': {
|
|
70
|
+
id: 'gemini-cli',
|
|
71
|
+
name: 'Gemini CLI',
|
|
72
|
+
npmPackage: '@google/gemini-cli',
|
|
73
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
|
|
74
|
+
containerMount: '/home/devuser/.gemini',
|
|
75
|
+
versionCmd: 'gemini --version',
|
|
76
|
+
setupHint: 'Run gemini inside the container to finish authentication.',
|
|
77
|
+
hostLiveMounts: [
|
|
78
|
+
{ hostPath: hostJoin(home, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' }
|
|
79
|
+
],
|
|
80
|
+
hostPreSeedFiles: [
|
|
81
|
+
{ hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
|
|
82
|
+
{ hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function validateTool(tool) {
|
|
88
|
+
if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
|
|
89
|
+
throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function resolveTools(config) {
|
|
93
|
+
const builtins = createBuiltinTools(config.home, config.project);
|
|
94
|
+
return config.tools.map((id) => {
|
|
95
|
+
const tool = builtins[id];
|
|
96
|
+
if (!tool) {
|
|
97
|
+
throw new Error(`Unknown sandbox tool: ${id}`);
|
|
98
|
+
}
|
|
99
|
+
validateTool(tool);
|
|
100
|
+
return tool;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function toolConfigDir(tool, project, branch) {
|
|
104
|
+
return hostJoin(tool.sandboxBase, project, sanitizeBranchName(branch));
|
|
105
|
+
}
|
|
106
|
+
export function toolConfigDirCandidates(tool, project, branch) {
|
|
107
|
+
return safeNameCandidates(branch).map((name) => hostJoin(tool.sandboxBase, project, name));
|
|
108
|
+
}
|
|
109
|
+
export function toolProjectDirCandidates(tool, project) {
|
|
110
|
+
return [hostJoin(tool.sandboxBase, project)];
|
|
111
|
+
}
|
|
112
|
+
export function toolNpmPackagesArg(tools) {
|
|
113
|
+
return tools.map((tool) => tool.npmPackage).join(' ');
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=tools.js.map
|