@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.
Files changed (100) hide show
  1. package/README.md +200 -8
  2. package/README.zh-CN.md +176 -8
  3. package/bin/{cli.js → cli.ts} +23 -19
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +64 -20
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/create.ts +1507 -0
  54. package/lib/sandbox/commands/enter.ts +115 -0
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
  56. package/lib/sandbox/commands/rebuild.ts +135 -0
  57. package/lib/sandbox/commands/refresh.ts +128 -0
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
  60. package/lib/sandbox/config.ts +133 -0
  61. package/lib/sandbox/{constants.js → constants.ts} +41 -17
  62. package/lib/sandbox/credentials.ts +634 -0
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/dotfiles.ts +236 -0
  65. package/lib/sandbox/engine.ts +231 -0
  66. package/lib/sandbox/engines/colima.ts +81 -0
  67. package/lib/sandbox/engines/docker-desktop.ts +36 -0
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/native.ts +131 -0
  70. package/lib/sandbox/engines/orbstack.ts +78 -0
  71. package/lib/sandbox/engines/selinux.ts +66 -0
  72. package/lib/sandbox/engines/wsl2-paths.ts +65 -0
  73. package/lib/sandbox/engines/wsl2.ts +74 -0
  74. package/lib/sandbox/{index.js → index.ts} +17 -8
  75. package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
  76. package/lib/sandbox/runtimes/base.dockerfile +116 -1
  77. package/lib/sandbox/shell.ts +186 -0
  78. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  79. package/lib/sandbox/{tools.js → tools.ts} +33 -29
  80. package/lib/{update.js → update.ts} +33 -10
  81. package/package.json +22 -12
  82. package/templates/.agents/rules/create-issue.github.en.md +2 -4
  83. package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
  84. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  85. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  86. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  87. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
  88. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  89. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  90. package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
  91. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
  92. package/lib/paths.js +0 -9
  93. package/lib/sandbox/commands/create.js +0 -1174
  94. package/lib/sandbox/commands/enter.js +0 -79
  95. package/lib/sandbox/commands/rebuild.js +0 -102
  96. package/lib/sandbox/config.js +0 -84
  97. package/lib/sandbox/engine.js +0 -256
  98. package/lib/sandbox/shell.js +0 -122
  99. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
  100. /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,3 @@
1
+ RUN apt-get update && apt-get install -y \
2
+ openjdk-17-jdk maven \
3
+ && rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,3 @@
1
+ RUN apt-get update && apt-get install -y \
2
+ openjdk-21-jdk maven \
3
+ && rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,3 @@
1
+ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
2
+ apt-get install -y nodejs && \
3
+ rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,3 @@
1
+ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
2
+ apt-get install -y nodejs && \
3
+ rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,3 @@
1
+ RUN apt-get update && apt-get install -y \
2
+ python3 python3-pip python3-venv \
3
+ && rm -rf /var/lib/apt/lists/*
@@ -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