@fitlab-ai/agent-infra 0.5.5 → 0.5.7
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 +182 -1
- package/README.zh-CN.md +182 -1
- package/bin/cli.js +28 -4
- package/lib/defaults.json +1 -0
- package/lib/init.js +68 -4
- package/lib/prompt.js +28 -1
- package/lib/render.js +1 -1
- package/lib/sandbox/commands/create.js +7 -3
- package/lib/sandbox/commands/rm.js +6 -4
- package/lib/sandbox/commands/vm.js +43 -16
- package/lib/sandbox/config.js +5 -0
- package/lib/sandbox/engine.js +125 -16
- package/lib/sandbox/shell.js +47 -7
- package/lib/sandbox/task-resolver.js +13 -6
- package/lib/sandbox/tools.js +18 -14
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +17 -0
- package/templates/.agents/QUICKSTART.zh-CN.md +17 -0
- package/templates/.agents/README.en.md +121 -0
- package/templates/.agents/README.zh-CN.md +121 -0
- package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/issue-sync.en.md +5 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
- package/templates/.agents/rules/milestone-inference.en.md +5 -0
- package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
- package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
- package/templates/.agents/rules/pr-sync.en.md +5 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
- package/templates/.agents/rules/release-commands.en.md +5 -0
- package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/security-alerts.en.md +5 -0
- package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +47 -12
- package/templates/.agents/scripts/platform-adapters/platform-sync.js +6 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.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 +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +8 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/implement-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +12 -4
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +12 -4
- package/templates/.agents/skills/import-issue/config/verify.json +2 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
- package/templates/.agents/skills/init-milestones/SKILL.en.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
- package/templates/.agents/skills/refine-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +6 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
- package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/test/SKILL.en.md +1 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +10 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +598 -7
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.claude/commands/post-release.en.md +8 -0
- package/templates/.claude/commands/post-release.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
- package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
- package/templates/.github/workflows/metadata-sync.yml +1 -1
- package/templates/.github/workflows/pr-label.yml +1 -1
- package/templates/.github/workflows/status-label.yml +1 -1
- package/templates/.opencode/commands/post-release.en.md +9 -0
- package/templates/.opencode/commands/post-release.zh-CN.md +9 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
sandboxLabel,
|
|
12
12
|
worktreeDirCandidates
|
|
13
13
|
} from '../constants.js';
|
|
14
|
-
import {
|
|
14
|
+
import { detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
|
|
15
15
|
import { run, runOk, runSafe } from '../shell.js';
|
|
16
16
|
import { resolveTaskBranch } from '../task-resolver.js';
|
|
17
17
|
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
@@ -165,13 +165,15 @@ async function rmAll(config, tools) {
|
|
|
165
165
|
runSafe('docker', ['rmi', config.imageName]);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
const engine = detectEngine(config);
|
|
169
|
+
if (isManagedEngine(engine)) {
|
|
170
|
+
const name = engineDisplayName(engine);
|
|
169
171
|
const shouldStopVm = await p.confirm({
|
|
170
|
-
message:
|
|
172
|
+
message: `Stop ${name} VM?`,
|
|
171
173
|
initialValue: false
|
|
172
174
|
});
|
|
173
175
|
if (!p.isCancel(shouldStopVm) && shouldStopVm) {
|
|
174
|
-
|
|
176
|
+
stopManagedVm(config);
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
@@ -3,31 +3,49 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { parsePositiveIntegerOption } from '../constants.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
ENGINES,
|
|
8
|
+
detectEngine,
|
|
9
|
+
engineDisplayName,
|
|
10
|
+
ensureDocker,
|
|
11
|
+
isManagedEngine,
|
|
12
|
+
stopManagedVm
|
|
13
|
+
} from '../engine.js';
|
|
14
|
+
import { runOk, runSafe } from '../shell.js';
|
|
8
15
|
|
|
9
16
|
const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
|
|
10
17
|
|
|
11
|
-
function ensureManagedVm() {
|
|
12
|
-
if (!
|
|
13
|
-
throw new Error(`VM management is unavailable
|
|
18
|
+
function ensureManagedVm(engine) {
|
|
19
|
+
if (!isManagedEngine(engine)) {
|
|
20
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
function status() {
|
|
18
|
-
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const engine = detectEngine(config);
|
|
27
|
+
const name = engineDisplayName(engine);
|
|
28
|
+
ensureManagedVm(engine);
|
|
19
29
|
p.intro(pc.cyan('Sandbox VM status'));
|
|
20
30
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
if (engine === ENGINES.COLIMA) {
|
|
32
|
+
if (runOk('colima', ['status'])) {
|
|
33
|
+
process.stdout.write(`${runSafe('colima', ['status'])}\n`);
|
|
34
|
+
} else {
|
|
35
|
+
p.log.warn('Colima VM is not running');
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!runOk('orb', ['status'])) {
|
|
41
|
+
p.log.warn(`${name} VM is not running`);
|
|
42
|
+
return;
|
|
25
43
|
}
|
|
44
|
+
|
|
45
|
+
process.stdout.write(`${runSafe('orb', ['status'])}\n`);
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
async function start(args) {
|
|
29
|
-
ensureManagedVm();
|
|
30
|
-
|
|
31
49
|
const { values } = parseArgs({
|
|
32
50
|
args,
|
|
33
51
|
allowPositionals: true,
|
|
@@ -45,6 +63,8 @@ async function start(args) {
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
const config = loadConfig();
|
|
66
|
+
const engine = detectEngine(config);
|
|
67
|
+
ensureManagedVm(engine);
|
|
48
68
|
const effectiveConfig = {
|
|
49
69
|
...config,
|
|
50
70
|
vm: {
|
|
@@ -62,15 +82,22 @@ async function start(args) {
|
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
function stop() {
|
|
65
|
-
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
const engine = detectEngine(config);
|
|
87
|
+
const name = engineDisplayName(engine);
|
|
88
|
+
ensureManagedVm(engine);
|
|
66
89
|
p.intro(pc.cyan('Stopping sandbox VM'));
|
|
67
90
|
|
|
68
|
-
if (!runOk('colima', ['status'])) {
|
|
69
|
-
p.log.warn(
|
|
91
|
+
if (engine === ENGINES.COLIMA && !runOk('colima', ['status'])) {
|
|
92
|
+
p.log.warn(`${name} VM is not running`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (engine === ENGINES.ORBSTACK && !runOk('orb', ['status'])) {
|
|
96
|
+
p.log.warn(`${name} VM is not running`);
|
|
70
97
|
return;
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
|
|
100
|
+
stopManagedVm(config);
|
|
74
101
|
p.outro(pc.green('VM stopped'));
|
|
75
102
|
}
|
|
76
103
|
|
package/lib/sandbox/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { validateSandboxEngine } from './engine.js';
|
|
4
5
|
|
|
5
6
|
const DEFAULTS = Object.freeze({
|
|
7
|
+
engine: null,
|
|
6
8
|
runtimes: ['node20'],
|
|
7
9
|
tools: ['claude-code', 'codex', 'opencode', 'gemini-cli'],
|
|
8
10
|
dockerfile: null,
|
|
@@ -26,6 +28,7 @@ function detectRepoRoot() {
|
|
|
26
28
|
|
|
27
29
|
function cloneDefaults() {
|
|
28
30
|
return {
|
|
31
|
+
engine: DEFAULTS.engine,
|
|
29
32
|
runtimes: [...DEFAULTS.runtimes],
|
|
30
33
|
tools: [...DEFAULTS.tools],
|
|
31
34
|
dockerfile: DEFAULTS.dockerfile,
|
|
@@ -49,6 +52,7 @@ export function loadConfig() {
|
|
|
49
52
|
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
50
53
|
const defaults = cloneDefaults();
|
|
51
54
|
const sandbox = airc.sandbox ?? {};
|
|
55
|
+
const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine);
|
|
52
56
|
const project = airc.project;
|
|
53
57
|
|
|
54
58
|
if (!project || typeof project !== 'string') {
|
|
@@ -64,6 +68,7 @@ export function loadConfig() {
|
|
|
64
68
|
containerPrefix: `${project}-dev`,
|
|
65
69
|
imageName: `${project}-sandbox:latest`,
|
|
66
70
|
worktreeBase: path.join(home, '.agent-infra', 'worktrees', project),
|
|
71
|
+
engine,
|
|
67
72
|
runtimes: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
|
|
68
73
|
? [...sandbox.runtimes]
|
|
69
74
|
: defaults.runtimes,
|
package/lib/sandbox/engine.js
CHANGED
|
@@ -2,26 +2,56 @@ import { platform } from 'node:os';
|
|
|
2
2
|
import { detectHostResources } from './constants.js';
|
|
3
3
|
import { run, runOk, runSafe, runVerbose } from './shell.js';
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const ENGINES = Object.freeze({
|
|
6
|
+
COLIMA: 'colima',
|
|
7
|
+
ORBSTACK: 'orbstack',
|
|
8
|
+
DOCKER_DESKTOP: 'docker-desktop'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const VALID_CONFIG_ENGINES = new Set(Object.values(ENGINES));
|
|
12
|
+
|
|
13
|
+
export function validateSandboxEngine(engine) {
|
|
14
|
+
if (engine === null || engine === undefined) {
|
|
15
|
+
return null;
|
|
9
16
|
}
|
|
17
|
+
|
|
18
|
+
if (VALID_CONFIG_ENGINES.has(engine)) {
|
|
19
|
+
return engine;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
throw new Error(
|
|
23
|
+
`sandbox: invalid "sandbox.engine" value "${engine}". `
|
|
24
|
+
+ 'Expected one of: null, colima, orbstack, docker-desktop. '
|
|
25
|
+
+ 'This setting only affects macOS sandbox engine selection.'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function detectEngine(config = {}, { platformFn = platform } = {}) {
|
|
30
|
+
const configured = validateSandboxEngine(config.engine);
|
|
31
|
+
const os = platformFn();
|
|
10
32
|
if (os === 'linux') {
|
|
11
33
|
return 'native';
|
|
12
34
|
}
|
|
13
35
|
if (os === 'win32') {
|
|
14
36
|
return 'wsl2';
|
|
15
37
|
}
|
|
38
|
+
if (os === 'darwin') {
|
|
39
|
+
if (configured) {
|
|
40
|
+
return configured;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ENGINES.COLIMA;
|
|
44
|
+
}
|
|
16
45
|
return 'unsupported';
|
|
17
46
|
}
|
|
18
47
|
|
|
19
48
|
function colimaArgs(config, runSafeFn = runSafe) {
|
|
20
49
|
const arch = runSafeFn('uname', ['-m']);
|
|
21
50
|
const defaults = detectHostResources();
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
51
|
+
const vm = config.vm ?? {};
|
|
52
|
+
const cpu = vm.cpu ?? defaults.cpu;
|
|
53
|
+
const memory = vm.memory ?? defaults.memory;
|
|
54
|
+
const disk = vm.disk ?? 60;
|
|
25
55
|
const args = ['start', '--cpu', String(cpu), '--memory', String(memory), '--disk', String(disk)];
|
|
26
56
|
|
|
27
57
|
if (arch === 'arm64') {
|
|
@@ -53,14 +83,54 @@ export async function ensureColima(
|
|
|
53
83
|
}
|
|
54
84
|
}
|
|
55
85
|
|
|
86
|
+
export async function ensureOrbStack(
|
|
87
|
+
_config,
|
|
88
|
+
onMessage,
|
|
89
|
+
{ runOkFn = runOk, runVerboseFn = runVerbose } = {}
|
|
90
|
+
) {
|
|
91
|
+
if (!runOkFn('which', ['orb'])) {
|
|
92
|
+
onMessage?.('Installing OrbStack via Homebrew...');
|
|
93
|
+
runVerboseFn('brew', ['install', '--cask', 'orbstack']);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!runOkFn('docker', ['info'])) {
|
|
97
|
+
onMessage?.('Starting OrbStack...');
|
|
98
|
+
runVerboseFn('orb', ['start']);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!runOkFn('docker', ['info'])) {
|
|
102
|
+
throw new Error('Docker daemon is not available after starting OrbStack');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function ensureDockerDesktop(
|
|
107
|
+
_config,
|
|
108
|
+
onMessage,
|
|
109
|
+
{ runOkFn = runOk } = {}
|
|
110
|
+
) {
|
|
111
|
+
if (!runOkFn('docker', ['info'])) {
|
|
112
|
+
throw new Error('Docker Desktop is not running. Please start Docker Desktop manually.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
export async function ensureDocker(config, onMessage) {
|
|
57
|
-
const engine = detectEngine();
|
|
117
|
+
const engine = detectEngine(config);
|
|
58
118
|
|
|
59
|
-
if (engine ===
|
|
119
|
+
if (engine === ENGINES.COLIMA) {
|
|
60
120
|
await ensureColima(config, onMessage);
|
|
61
121
|
return;
|
|
62
122
|
}
|
|
63
123
|
|
|
124
|
+
if (engine === ENGINES.ORBSTACK) {
|
|
125
|
+
await ensureOrbStack(config, onMessage);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (engine === ENGINES.DOCKER_DESKTOP) {
|
|
130
|
+
await ensureDockerDesktop(config, onMessage);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
64
134
|
if (engine === 'native') {
|
|
65
135
|
if (!runOk('docker', ['info'])) {
|
|
66
136
|
throw new Error('Docker daemon is not running. Please start Docker first.');
|
|
@@ -75,19 +145,58 @@ export async function ensureDocker(config, onMessage) {
|
|
|
75
145
|
throw new Error(`Unsupported sandbox engine: ${engine}`);
|
|
76
146
|
}
|
|
77
147
|
|
|
78
|
-
export function isVmManaged() {
|
|
79
|
-
|
|
148
|
+
export function isVmManaged(config = {}, dependencies = {}) {
|
|
149
|
+
const engine = detectEngine(config, dependencies);
|
|
150
|
+
return isManagedEngine(engine);
|
|
80
151
|
}
|
|
81
152
|
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
|
|
153
|
+
export function isManagedEngine(engine) {
|
|
154
|
+
return engine === ENGINES.COLIMA || engine === ENGINES.ORBSTACK;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function engineDisplayName(engine) {
|
|
158
|
+
const names = {
|
|
159
|
+
[ENGINES.COLIMA]: 'Colima',
|
|
160
|
+
[ENGINES.ORBSTACK]: 'OrbStack',
|
|
161
|
+
[ENGINES.DOCKER_DESKTOP]: 'Docker Desktop',
|
|
162
|
+
native: 'native Docker',
|
|
163
|
+
wsl2: 'WSL2'
|
|
164
|
+
};
|
|
165
|
+
return names[engine] ?? engine;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function startManagedVm(
|
|
169
|
+
config,
|
|
170
|
+
{ platformFn = platform, runOkFn = runOk, runVerboseFn = runVerbose } = {}
|
|
171
|
+
) {
|
|
172
|
+
const engine = detectEngine(config, { platformFn });
|
|
173
|
+
if (!isManagedEngine(engine)) {
|
|
174
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
85
175
|
}
|
|
86
176
|
|
|
87
|
-
if (
|
|
177
|
+
if (engine === ENGINES.COLIMA && runOkFn('colima', ['status'])) {
|
|
178
|
+
return 'already-running';
|
|
179
|
+
}
|
|
180
|
+
if (engine === ENGINES.ORBSTACK && runOkFn('orb', ['status'])) {
|
|
88
181
|
return 'already-running';
|
|
89
182
|
}
|
|
90
183
|
|
|
91
|
-
|
|
184
|
+
if (engine === ENGINES.COLIMA) {
|
|
185
|
+
runVerboseFn('colima', colimaArgs(config));
|
|
186
|
+
} else if (engine === ENGINES.ORBSTACK) {
|
|
187
|
+
runVerboseFn('orb', ['start']);
|
|
188
|
+
}
|
|
92
189
|
return 'started';
|
|
93
190
|
}
|
|
191
|
+
|
|
192
|
+
export function stopManagedVm(config, { platformFn = platform, runFn = run } = {}) {
|
|
193
|
+
const engine = detectEngine(config, { platformFn });
|
|
194
|
+
if (engine === ENGINES.COLIMA) {
|
|
195
|
+
runFn('colima', ['stop']);
|
|
196
|
+
return 'stopped';
|
|
197
|
+
} else if (engine === ENGINES.ORBSTACK) {
|
|
198
|
+
runFn('orb', ['stop']);
|
|
199
|
+
return 'stopped';
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
|
|
202
|
+
}
|
package/lib/sandbox/shell.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
|
|
3
5
|
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
4
6
|
|
|
@@ -11,25 +13,62 @@ function normalizeOptions(opts = {}, stdio) {
|
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
function resolveCommand(cmd) {
|
|
17
|
+
if (process.platform !== 'win32' || path.extname(cmd)) {
|
|
18
|
+
return cmd;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
22
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
23
|
+
.split(';')
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
27
|
+
for (const extension of extensions) {
|
|
28
|
+
const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
|
|
29
|
+
if (fs.existsSync(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
|
|
33
|
+
if (fs.existsSync(upperCandidate)) {
|
|
34
|
+
return upperCandidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return cmd;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commandOptions(cmd, opts) {
|
|
43
|
+
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
44
|
+
return { ...opts, shell: true };
|
|
45
|
+
}
|
|
46
|
+
return opts;
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
export function run(cmd, args, opts = {}) {
|
|
15
|
-
|
|
50
|
+
const resolved = resolveCommand(cmd);
|
|
51
|
+
return execFileSync(resolved, args, commandOptions(resolved, {
|
|
16
52
|
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
17
53
|
encoding: 'utf8'
|
|
18
|
-
}).trim();
|
|
54
|
+
})).trim();
|
|
19
55
|
}
|
|
20
56
|
|
|
21
57
|
export function runOk(cmd, args, opts = {}) {
|
|
22
|
-
const
|
|
58
|
+
const resolved = resolveCommand(cmd);
|
|
59
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
|
|
23
60
|
return result.status === 0;
|
|
24
61
|
}
|
|
25
62
|
|
|
26
63
|
export function runInteractive(cmd, args, opts = {}) {
|
|
27
|
-
const
|
|
64
|
+
const resolved = resolveCommand(cmd);
|
|
65
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
28
66
|
return result.status ?? 1;
|
|
29
67
|
}
|
|
30
68
|
|
|
31
69
|
export function runVerbose(cmd, args, opts = {}) {
|
|
32
|
-
const
|
|
70
|
+
const resolved = resolveCommand(cmd);
|
|
71
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
33
72
|
|
|
34
73
|
if (result.status !== 0) {
|
|
35
74
|
if (result.signal === 'SIGTERM') {
|
|
@@ -40,9 +79,10 @@ export function runVerbose(cmd, args, opts = {}) {
|
|
|
40
79
|
}
|
|
41
80
|
|
|
42
81
|
export function runSafe(cmd, args, opts = {}) {
|
|
43
|
-
const
|
|
82
|
+
const resolved = resolveCommand(cmd);
|
|
83
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, {
|
|
44
84
|
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
45
85
|
encoding: 'utf8',
|
|
46
|
-
});
|
|
86
|
+
}));
|
|
47
87
|
return (result.stdout ?? '').trim();
|
|
48
88
|
}
|
|
@@ -2,24 +2,31 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
5
|
+
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
6
|
+
|
|
7
|
+
function stripQuotes(value) {
|
|
8
|
+
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
function readTaskContent(repoRoot, taskId) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
for (const dir of WORKSPACE_DIRS) {
|
|
13
|
+
const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
|
|
14
|
+
if (fs.existsSync(taskPath)) {
|
|
15
|
+
return fs.readFileSync(taskPath, 'utf8');
|
|
16
|
+
}
|
|
10
17
|
}
|
|
11
|
-
|
|
18
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
function resolveBranchFromTaskContent(content, taskId) {
|
|
15
22
|
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
16
23
|
if (frontmatterBranch && frontmatterBranch[1].trim()) {
|
|
17
|
-
return frontmatterBranch[1].trim();
|
|
24
|
+
return stripQuotes(frontmatterBranch[1].trim());
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
const contextBranch = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
|
|
21
28
|
if (contextBranch && contextBranch[1].trim()) {
|
|
22
|
-
return contextBranch[1].trim();
|
|
29
|
+
return stripQuotes(contextBranch[1].trim());
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
throw new Error(`Task ${taskId} has no branch field in task.md`);
|
package/lib/sandbox/tools.js
CHANGED
|
@@ -18,6 +18,10 @@ import { safeNameCandidates, sanitizeBranchName } from './constants.js';
|
|
|
18
18
|
* @property {string[]=} postSetupCmds
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
function hostJoin(basePath, ...segments) {
|
|
22
|
+
return basePath.startsWith('/') ? path.posix.join(basePath, ...segments) : path.join(basePath, ...segments);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
function createBuiltinTools(home, project) {
|
|
22
26
|
/** @type {Record<string, SandboxTool>} */
|
|
23
27
|
return {
|
|
@@ -25,7 +29,7 @@ function createBuiltinTools(home, project) {
|
|
|
25
29
|
id: 'claude-code',
|
|
26
30
|
name: 'Claude Code',
|
|
27
31
|
npmPackage: '@anthropic-ai/claude-code',
|
|
28
|
-
sandboxBase:
|
|
32
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
29
33
|
containerMount: '/home/devuser/.claude',
|
|
30
34
|
versionCmd: 'claude --version',
|
|
31
35
|
setupHint: 'Authenticates via host credentials live-mounted at ~/.claude/.credentials.json',
|
|
@@ -38,7 +42,7 @@ function createBuiltinTools(home, project) {
|
|
|
38
42
|
// letting ensureClaudeOnboarding actually take effect.
|
|
39
43
|
envVars: { CLAUDE_CONFIG_DIR: '/home/devuser/.claude' },
|
|
40
44
|
hostPreSeedDirs: [
|
|
41
|
-
{ hostDir:
|
|
45
|
+
{ hostDir: hostJoin(home, '.claude', 'plugins'), sandboxSubdir: 'plugins' }
|
|
42
46
|
],
|
|
43
47
|
pathRewriteFiles: [
|
|
44
48
|
'plugins/installed_plugins.json',
|
|
@@ -46,7 +50,7 @@ function createBuiltinTools(home, project) {
|
|
|
46
50
|
],
|
|
47
51
|
hostLiveMounts: [
|
|
48
52
|
{
|
|
49
|
-
hostPath:
|
|
53
|
+
hostPath: hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code', '.credentials.json'),
|
|
50
54
|
containerSubpath: '.credentials.json'
|
|
51
55
|
}
|
|
52
56
|
]
|
|
@@ -55,12 +59,12 @@ function createBuiltinTools(home, project) {
|
|
|
55
59
|
id: 'codex',
|
|
56
60
|
name: 'Codex',
|
|
57
61
|
npmPackage: '@openai/codex',
|
|
58
|
-
sandboxBase:
|
|
62
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
|
|
59
63
|
containerMount: '/home/devuser/.codex',
|
|
60
64
|
versionCmd: 'codex --version',
|
|
61
65
|
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
62
66
|
hostLiveMounts: [
|
|
63
|
-
{ hostPath:
|
|
67
|
+
{ hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
|
|
64
68
|
],
|
|
65
69
|
postSetupCmds: [
|
|
66
70
|
'test -d /workspace/.codex/commands && ln -sfn /workspace/.codex/commands /home/devuser/.codex/prompts || true'
|
|
@@ -70,13 +74,13 @@ function createBuiltinTools(home, project) {
|
|
|
70
74
|
id: 'opencode',
|
|
71
75
|
name: 'OpenCode',
|
|
72
76
|
npmPackage: 'opencode-ai',
|
|
73
|
-
sandboxBase:
|
|
77
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
|
|
74
78
|
containerMount: '/home/devuser/.local/share/opencode',
|
|
75
79
|
versionCmd: 'opencode version',
|
|
76
80
|
setupHint: 'Configure OpenCode credentials inside the container before first use.',
|
|
77
81
|
hostLiveMounts: [
|
|
78
82
|
{
|
|
79
|
-
hostPath:
|
|
83
|
+
hostPath: hostJoin(home, '.local', 'share', 'opencode', 'auth.json'),
|
|
80
84
|
containerSubpath: 'auth.json'
|
|
81
85
|
}
|
|
82
86
|
]
|
|
@@ -85,16 +89,16 @@ function createBuiltinTools(home, project) {
|
|
|
85
89
|
id: 'gemini-cli',
|
|
86
90
|
name: 'Gemini CLI',
|
|
87
91
|
npmPackage: '@google/gemini-cli',
|
|
88
|
-
sandboxBase:
|
|
92
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
|
|
89
93
|
containerMount: '/home/devuser/.gemini',
|
|
90
94
|
versionCmd: 'gemini --version',
|
|
91
95
|
setupHint: 'Run gemini inside the container to finish authentication.',
|
|
92
96
|
hostLiveMounts: [
|
|
93
|
-
{ hostPath:
|
|
97
|
+
{ hostPath: hostJoin(home, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' }
|
|
94
98
|
],
|
|
95
99
|
hostPreSeedFiles: [
|
|
96
|
-
{ hostPath:
|
|
97
|
-
{ hostPath:
|
|
100
|
+
{ hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
|
|
101
|
+
{ hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
|
|
98
102
|
]
|
|
99
103
|
}
|
|
100
104
|
};
|
|
@@ -119,15 +123,15 @@ export function resolveTools(config) {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export function toolConfigDir(tool, project, branch) {
|
|
122
|
-
return
|
|
126
|
+
return hostJoin(tool.sandboxBase, project, sanitizeBranchName(branch));
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
export function toolConfigDirCandidates(tool, project, branch) {
|
|
126
|
-
return safeNameCandidates(branch).map((name) =>
|
|
130
|
+
return safeNameCandidates(branch).map((name) => hostJoin(tool.sandboxBase, project, name));
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
export function toolProjectDirCandidates(tool, project) {
|
|
130
|
-
return [
|
|
134
|
+
return [hostJoin(tool.sandboxBase, project)];
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
export function toolNpmPackagesArg(tools) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "node scripts/build-inline.js",
|
|
49
|
-
"demo:regen": "
|
|
49
|
+
"demo:regen": "sh scripts/demo-regen.sh",
|
|
50
50
|
"prepare": "git config core.hooksPath .github/hooks || true",
|
|
51
51
|
"test": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js",
|
|
52
52
|
"prepublishOnly": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js"
|
|
@@ -18,6 +18,23 @@ git config core.hooksPath .github/hooks
|
|
|
18
18
|
|
|
19
19
|
This makes Git invoke the hooks synced into `.github/hooks/`, including `pre-commit` and `check-version-format.sh`.
|
|
20
20
|
|
|
21
|
+
## External Templates And Skills
|
|
22
|
+
|
|
23
|
+
If your team maintains private platform templates or shared custom skills, configure local sources in `.agents/.airc.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"templates": {
|
|
28
|
+
"sources": [{ "type": "local", "path": "~/private-templates" }]
|
|
29
|
+
},
|
|
30
|
+
"skills": {
|
|
31
|
+
"sources": [{ "type": "local", "path": "~/private-skills" }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Built-in templates take priority over external templates. Later external template sources override earlier external sources, and `update-agent-infra` reports conflicts in `templateSources.conflicts`. Only use trusted local paths because external templates and skills may contain executable scripts.
|
|
37
|
+
|
|
21
38
|
## Creating Your First Task
|
|
22
39
|
|
|
23
40
|
1. Copy the task template to the active workspace:
|
|
@@ -18,6 +18,23 @@ git config core.hooksPath .github/hooks
|
|
|
18
18
|
|
|
19
19
|
这样 Git 才会调用同步到 `.github/hooks/` 下的 hook,包括 `pre-commit` 和 `check-version-format.sh`。
|
|
20
20
|
|
|
21
|
+
## 外部模板与 Skill
|
|
22
|
+
|
|
23
|
+
如果团队维护私有平台模板或共享自定义 skill,可在 `.agents/.airc.json` 中配置本地源:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"templates": {
|
|
28
|
+
"sources": [{ "type": "local", "path": "~/private-templates" }]
|
|
29
|
+
},
|
|
30
|
+
"skills": {
|
|
31
|
+
"sources": [{ "type": "local", "path": "~/private-skills" }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
内置模板优先级高于外部模板。多个外部模板源之间,后面的 source 覆盖前面的 source,`update-agent-infra` 会在 `templateSources.conflicts` 中报告冲突。外部模板和 skill 可能包含可执行脚本,只使用可信本地路径。
|
|
37
|
+
|
|
21
38
|
## 创建第一个任务
|
|
22
39
|
|
|
23
40
|
1. 将任务模板复制到活跃工作区:
|