@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,186 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { ExecFileSyncOptions, StdioOptions, SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
type CommandOptions = {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
encoding?: BufferEncoding;
|
|
11
|
+
stdio?: StdioOptions;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
shell?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type RunProbeOptions = CommandOptions & {
|
|
17
|
+
spawnFn?: typeof spawnSync;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function normalizeOptions(opts: CommandOptions = {}, stdio: StdioOptions): CommandOptions {
|
|
21
|
+
return {
|
|
22
|
+
cwd: opts.cwd,
|
|
23
|
+
encoding: opts.encoding,
|
|
24
|
+
stdio,
|
|
25
|
+
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveCommand(cmd: string): string {
|
|
30
|
+
if (process.platform !== 'win32' || path.extname(cmd)) {
|
|
31
|
+
return cmd;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pathValue = process.env.Path || process.env.PATH || '';
|
|
35
|
+
const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
36
|
+
.split(';')
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
|
|
39
|
+
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
40
|
+
for (const extension of extensions) {
|
|
41
|
+
const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
|
|
42
|
+
if (fs.existsSync(candidate)) {
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
|
|
46
|
+
if (fs.existsSync(upperCandidate)) {
|
|
47
|
+
return upperCandidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cmd;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function commandOptions<T extends CommandOptions>(cmd: string, opts: T): T | (T & { shell: true }) {
|
|
56
|
+
if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
|
|
57
|
+
return { ...opts, shell: true };
|
|
58
|
+
}
|
|
59
|
+
return opts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function run(cmd: string, args: string[], opts: CommandOptions = {}): string {
|
|
63
|
+
const resolved = resolveCommand(cmd);
|
|
64
|
+
return execFileSync(resolved, args, commandOptions(resolved, {
|
|
65
|
+
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
66
|
+
encoding: 'utf8'
|
|
67
|
+
})).trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function runOk(cmd: string, args: string[], opts: CommandOptions = {}): boolean {
|
|
71
|
+
const resolved = resolveCommand(cmd);
|
|
72
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
|
|
73
|
+
return result.status === 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function restoreTerminal(): void {
|
|
77
|
+
if (!process.stdout.isTTY) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
process.stdout.write([
|
|
83
|
+
'\x1b[?1049l',
|
|
84
|
+
'\x1b[?25h',
|
|
85
|
+
'\x1b>',
|
|
86
|
+
'\x1b[?1000l',
|
|
87
|
+
'\x1b[?1002l',
|
|
88
|
+
'\x1b[?1003l',
|
|
89
|
+
'\x1b[?1006l'
|
|
90
|
+
].join(''));
|
|
91
|
+
} catch {
|
|
92
|
+
// Best-effort cleanup only; preserve the original command result.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.platform === 'win32') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
execFileSync('stty', ['sane'], { stdio: 'inherit' });
|
|
101
|
+
} catch {
|
|
102
|
+
// Some environments do not provide stty or reject sane; ANSI reset still helps.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function runInteractive(cmd: string, args: string[], opts: CommandOptions = {}): number {
|
|
107
|
+
const resolved = resolveCommand(cmd);
|
|
108
|
+
try {
|
|
109
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
110
|
+
return result.status ?? 1;
|
|
111
|
+
} finally {
|
|
112
|
+
restoreTerminal();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function runVerbose(cmd: string, args: string[], opts: CommandOptions = {}): void {
|
|
117
|
+
const resolved = resolveCommand(cmd);
|
|
118
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
|
|
119
|
+
|
|
120
|
+
if (result.status !== 0) {
|
|
121
|
+
if (result.signal === 'SIGTERM') {
|
|
122
|
+
throw new Error(`Command timed out after ${opts.timeout ?? DEFAULT_TIMEOUT_MS}ms: ${cmd}`);
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Command failed with exit code ${result.status}: ${cmd}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function runSafe(cmd: string, args: string[], opts: CommandOptions = {}): string {
|
|
129
|
+
const resolved = resolveCommand(cmd);
|
|
130
|
+
const result = spawnSync(resolved, args, commandOptions(resolved, {
|
|
131
|
+
...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
}));
|
|
134
|
+
if (result.status !== 0 && result.stderr) {
|
|
135
|
+
process.stderr.write(result.stderr);
|
|
136
|
+
}
|
|
137
|
+
return (result.stdout ?? '').trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function commandForEngine(engine: string, cmd: string, args: string[] = []): { cmd: string; args: string[] } {
|
|
141
|
+
if (engine === 'wsl2') {
|
|
142
|
+
const resolvedWrapper = resolveCommand('wsl.exe');
|
|
143
|
+
return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { cmd, args };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): string {
|
|
150
|
+
const command = commandForEngine(engine, cmd, args);
|
|
151
|
+
return run(command.cmd, command.args, opts);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function execEngine(engine: string, cmd: string, args: string[], opts: ExecFileSyncOptions = {}) {
|
|
155
|
+
const command = commandForEngine(engine, cmd, args);
|
|
156
|
+
return execFileSync(command.cmd, command.args, opts);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function runOkEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): boolean {
|
|
160
|
+
const command = commandForEngine(engine, cmd, args);
|
|
161
|
+
return runOk(command.cmd, command.args, opts);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function runSafeEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): string {
|
|
165
|
+
const command = commandForEngine(engine, cmd, args);
|
|
166
|
+
return runSafe(command.cmd, command.args, opts);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function runVerboseEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): void {
|
|
170
|
+
const command = commandForEngine(engine, cmd, args);
|
|
171
|
+
return runVerbose(command.cmd, command.args, opts);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function runInteractiveEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): number {
|
|
175
|
+
const command = commandForEngine(engine, cmd, args);
|
|
176
|
+
return runInteractive(command.cmd, command.args, opts);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function runProbe(cmd: string, args: string[], opts: RunProbeOptions = {}): SpawnSyncReturns<string | Buffer> {
|
|
180
|
+
const { spawnFn = spawnSync, ...commandOpts } = opts;
|
|
181
|
+
const resolved = resolveCommand(cmd);
|
|
182
|
+
return spawnFn(resolved, args, commandOptions(resolved, normalizeOptions(
|
|
183
|
+
{ encoding: 'utf8', ...commandOpts },
|
|
184
|
+
commandOpts.stdio ?? ['pipe', 'pipe', 'pipe']
|
|
185
|
+
)));
|
|
186
|
+
}
|
|
@@ -4,11 +4,11 @@ import path from 'node:path';
|
|
|
4
4
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
5
5
|
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
6
6
|
|
|
7
|
-
function stripQuotes(value) {
|
|
7
|
+
function stripQuotes(value: string): string {
|
|
8
8
|
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function readTaskContent(repoRoot, taskId) {
|
|
11
|
+
function readTaskContent(repoRoot: string, taskId: string): string {
|
|
12
12
|
for (const dir of WORKSPACE_DIRS) {
|
|
13
13
|
const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
|
|
14
14
|
if (fs.existsSync(taskPath)) {
|
|
@@ -18,21 +18,21 @@ function readTaskContent(repoRoot, taskId) {
|
|
|
18
18
|
throw new Error(`Task not found: ${taskId}`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function resolveBranchFromTaskContent(content, taskId) {
|
|
21
|
+
function resolveBranchFromTaskContent(content: string, taskId: string): string {
|
|
22
22
|
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
23
|
-
if (frontmatterBranch
|
|
23
|
+
if (frontmatterBranch?.[1]?.trim()) {
|
|
24
24
|
return stripQuotes(frontmatterBranch[1].trim());
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const contextBranch = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
|
|
28
|
-
if (contextBranch
|
|
28
|
+
if (contextBranch?.[1]?.trim()) {
|
|
29
29
|
return stripQuotes(contextBranch[1].trim());
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
throw new Error(`Task ${taskId} has no branch field in task.md`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function resolveTaskBranch(arg, repoRoot) {
|
|
35
|
+
export function resolveTaskBranch(arg: string, repoRoot: string): string {
|
|
36
36
|
if (!TASK_ID_RE.test(arg)) {
|
|
37
37
|
return arg;
|
|
38
38
|
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { safeNameCandidates, sanitizeBranchName } from './constants.ts';
|
|
2
|
+
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
4
|
+
export type SandboxTool = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
npmPackage: string;
|
|
8
|
+
sandboxBase: string;
|
|
9
|
+
containerMount: string;
|
|
10
|
+
versionCmd: string;
|
|
11
|
+
setupHint: string;
|
|
12
|
+
envVars?: Record<string, string>;
|
|
13
|
+
hostPreSeedFiles?: Array<{ hostPath: string; sandboxName: string }>;
|
|
14
|
+
hostPreSeedDirs?: Array<{ hostDir: string; sandboxSubdir: string }>;
|
|
15
|
+
pathRewriteFiles?: string[];
|
|
16
|
+
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
|
|
17
|
+
postSetupCmds?: string[];
|
|
18
|
+
};
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
type ToolsConfig = {
|
|
21
|
+
home: string;
|
|
22
|
+
project: string;
|
|
23
|
+
tools: string[];
|
|
24
|
+
};
|
|
24
25
|
|
|
25
|
-
function createBuiltinTools(home, project) {
|
|
26
|
-
/** @type {Record<string, SandboxTool>} */
|
|
26
|
+
function createBuiltinTools(home: string, project: string): Record<string, SandboxTool> {
|
|
27
27
|
return {
|
|
28
28
|
'claude-code': {
|
|
29
29
|
id: 'claude-code',
|
|
@@ -78,6 +78,10 @@ function createBuiltinTools(home, project) {
|
|
|
78
78
|
containerMount: '/home/devuser/.local/share/opencode',
|
|
79
79
|
versionCmd: 'opencode version',
|
|
80
80
|
setupHint: 'Configure OpenCode credentials inside the container before first use.',
|
|
81
|
+
// OpenCode reads opencode.json from $XDG_CONFIG_HOME/opencode by default,
|
|
82
|
+
// outside this tool mount. Pin the config file path so the inherited
|
|
83
|
+
// sandbox opencode.json is the one the TUI actually reads.
|
|
84
|
+
envVars: { OPENCODE_CONFIG: '/home/devuser/.local/share/opencode/opencode.json' },
|
|
81
85
|
hostLiveMounts: [
|
|
82
86
|
{
|
|
83
87
|
hostPath: hostJoin(home, '.local', 'share', 'opencode', 'auth.json'),
|
|
@@ -104,13 +108,13 @@ function createBuiltinTools(home, project) {
|
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
function validateTool(tool) {
|
|
111
|
+
function validateTool(tool: SandboxTool): void {
|
|
108
112
|
if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
|
|
109
113
|
throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
export function resolveTools(config) {
|
|
117
|
+
export function resolveTools(config: ToolsConfig): SandboxTool[] {
|
|
114
118
|
const builtins = createBuiltinTools(config.home, config.project);
|
|
115
119
|
return config.tools.map((id) => {
|
|
116
120
|
const tool = builtins[id];
|
|
@@ -122,18 +126,18 @@ export function resolveTools(config) {
|
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
export function toolConfigDir(tool, project, branch) {
|
|
129
|
+
export function toolConfigDir(tool: SandboxTool, project: string, branch: string): string {
|
|
126
130
|
return hostJoin(tool.sandboxBase, project, sanitizeBranchName(branch));
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
export function toolConfigDirCandidates(tool, project, branch) {
|
|
133
|
+
export function toolConfigDirCandidates(tool: SandboxTool, project: string, branch: string): string[] {
|
|
130
134
|
return safeNameCandidates(branch).map((name) => hostJoin(tool.sandboxBase, project, name));
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
export function toolProjectDirCandidates(tool, project) {
|
|
137
|
+
export function toolProjectDirCandidates(tool: SandboxTool, project: string): string[] {
|
|
134
138
|
return [hostJoin(tool.sandboxBase, project)];
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
export function toolNpmPackagesArg(tools) {
|
|
141
|
+
export function toolNpmPackagesArg(tools: SandboxTool[]): string {
|
|
138
142
|
return tools.map((tool) => tool.npmPackage).join(' ');
|
|
139
143
|
}
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { info, ok, err } from './log.
|
|
4
|
-
import { resolveTemplateDir } from './paths.
|
|
5
|
-
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.
|
|
3
|
+
import { info, ok, err } from './log.ts';
|
|
4
|
+
import { resolveTemplateDir } from './paths.ts';
|
|
5
|
+
import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.ts';
|
|
6
|
+
|
|
7
|
+
type FileRegistry = {
|
|
8
|
+
managed: string[];
|
|
9
|
+
merged: string[];
|
|
10
|
+
ejected: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UpdateConfig = {
|
|
14
|
+
project: string;
|
|
15
|
+
org: string;
|
|
16
|
+
language: string;
|
|
17
|
+
platform?: { type?: string };
|
|
18
|
+
sandbox?: Record<string, unknown>;
|
|
19
|
+
labels?: Record<string, unknown>;
|
|
20
|
+
files?: Partial<FileRegistry>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Defaults = {
|
|
24
|
+
platform: { type: string };
|
|
25
|
+
sandbox: Record<string, unknown>;
|
|
26
|
+
labels: Record<string, unknown>;
|
|
27
|
+
files: FileRegistry;
|
|
28
|
+
};
|
|
6
29
|
|
|
7
30
|
const defaults = JSON.parse(
|
|
8
31
|
fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8')
|
|
9
|
-
);
|
|
32
|
+
) as Defaults;
|
|
10
33
|
|
|
11
34
|
const CONFIG_DIR = '.agents';
|
|
12
35
|
const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
|
|
13
36
|
|
|
14
|
-
function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
15
|
-
const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0];
|
|
37
|
+
function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
|
|
38
|
+
const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
|
|
16
39
|
if (!top.startsWith('.')) return false;
|
|
17
40
|
|
|
18
41
|
const candidate = top.slice(1);
|
|
@@ -20,7 +43,7 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
|
|
|
20
43
|
return candidate !== platformType;
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
function syncFileRegistry(config, platformType) {
|
|
46
|
+
function syncFileRegistry(config: UpdateConfig, platformType: string) {
|
|
24
47
|
config.files ||= {};
|
|
25
48
|
const before = JSON.stringify({
|
|
26
49
|
files: {
|
|
@@ -38,7 +61,7 @@ function syncFileRegistry(config, platformType) {
|
|
|
38
61
|
...config.files.merged,
|
|
39
62
|
...config.files.ejected
|
|
40
63
|
];
|
|
41
|
-
const added = { managed: [], merged: [] };
|
|
64
|
+
const added: Pick<FileRegistry, 'managed' | 'merged'> = { managed: [], merged: [] };
|
|
42
65
|
|
|
43
66
|
for (const entry of defaults.files.managed) {
|
|
44
67
|
if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
|
|
@@ -66,7 +89,7 @@ function syncFileRegistry(config, platformType) {
|
|
|
66
89
|
return { added, changed: before !== after };
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
async function cmdUpdate() {
|
|
92
|
+
async function cmdUpdate(): Promise<void> {
|
|
70
93
|
console.log('');
|
|
71
94
|
console.log(' agent-infra update');
|
|
72
95
|
console.log(' ==================================');
|
|
@@ -90,7 +113,7 @@ async function cmdUpdate() {
|
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
// read project config
|
|
93
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
116
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as UpdateConfig;
|
|
94
117
|
const { project, org, language } = config;
|
|
95
118
|
const platformType = config.platform?.type || defaults.platform.type;
|
|
96
119
|
const replacements = { project, org };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -18,16 +18,18 @@
|
|
|
18
18
|
"registry": "https://registry.npmjs.org/"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
|
-
"agent-infra": "./bin/cli.js",
|
|
22
|
-
"ai": "./bin/cli.js"
|
|
21
|
+
"agent-infra": "./dist/bin/cli.js",
|
|
22
|
+
"ai": "./dist/bin/cli.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"
|
|
25
|
+
"dist/",
|
|
26
|
+
"!dist/**/*.map",
|
|
27
|
+
"bin/cli.ts",
|
|
26
28
|
"lib/",
|
|
27
29
|
"templates/"
|
|
28
30
|
],
|
|
29
31
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
32
|
+
"node": ">=22"
|
|
31
33
|
},
|
|
32
34
|
"keywords": [
|
|
33
35
|
"ai",
|
|
@@ -41,16 +43,24 @@
|
|
|
41
43
|
"installer"
|
|
42
44
|
],
|
|
43
45
|
"dependencies": {
|
|
44
|
-
"@clack/prompts": "1.
|
|
45
|
-
"
|
|
46
|
+
"@clack/prompts": "1.4.0",
|
|
47
|
+
"cross-spawn": "^7.0.6",
|
|
48
|
+
"picocolors": "1.1.1",
|
|
49
|
+
"smol-toml": "^1.6.1"
|
|
46
50
|
},
|
|
47
51
|
"scripts": {
|
|
48
|
-
"build": "node scripts/build-inline.js",
|
|
52
|
+
"build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
|
|
49
53
|
"demo:regen": "sh scripts/demo-regen.sh",
|
|
50
54
|
"prepare": "git config core.hooksPath .git-hooks || true",
|
|
51
|
-
"
|
|
52
|
-
"test:
|
|
53
|
-
"test": "node
|
|
54
|
-
"
|
|
55
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
|
|
56
|
+
"test:smoke": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts",
|
|
57
|
+
"test:core": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts tests/cli/cli.test.ts tests/cli/merge.test.ts tests/cli/sandbox.test.ts tests/core/custom-skills.test.ts tests/core/custom-tuis.test.ts tests/core/demo-regen.test.ts tests/scripts/find-existing-task.test.ts tests/scripts/platform-adapter-defaults.test.ts",
|
|
58
|
+
"test": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts",
|
|
59
|
+
"prepublishOnly": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/cross-spawn": "^6.0.6",
|
|
63
|
+
"@types/node": "^22.19.19",
|
|
64
|
+
"typescript": "~5.9"
|
|
55
65
|
}
|
|
56
66
|
}
|
|
@@ -155,10 +155,8 @@ Update task.md:
|
|
|
155
155
|
|
|
156
156
|
- Write `issue_number: {n}` into the frontmatter (replace if it exists; append at the end of the frontmatter otherwise)
|
|
157
157
|
- Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S%:z"`)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Create Issue** by {agent} — Created GitHub Issue #{n}
|
|
161
|
-
```
|
|
158
|
+
|
|
159
|
+
> Do NOT append an Activity Log entry here. The Issue creation event is already captured by the GitHub Issue itself and by the frontmatter `issue_number` field; the Activity Log only records the single `create-task` skill execution anchor (`Task Created`), written by the caller SKILL step 3.
|
|
162
160
|
|
|
163
161
|
### 8. Return the Result
|
|
164
162
|
|
|
@@ -155,10 +155,8 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
155
155
|
|
|
156
156
|
- 把 `issue_number: {n}` 写入 frontmatter(已存在则替换;不存在则在 frontmatter 末尾追加)
|
|
157
157
|
- 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S%:z"`)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Create Issue** by {agent} — Created GitHub Issue #{n}
|
|
161
|
-
```
|
|
158
|
+
|
|
159
|
+
> 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Task Created`),由调用方 SKILL 步骤 3 写入。
|
|
162
160
|
|
|
163
161
|
### 8. 返回结果
|
|
164
162
|
|
|
@@ -120,6 +120,35 @@ Read Issue comments or search for existing hidden markers:
|
|
|
120
120
|
gh api "repos/$upstream_repo/issues/{issue-number}/comments" --paginate
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## Historical Task Comment Scan
|
|
124
|
+
|
|
125
|
+
`find-existing-task.js` only consumes stdin and does not call `gh` directly. The AI selects the pipeline command for the host OS.
|
|
126
|
+
|
|
127
|
+
POSIX (bash / zsh):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
set -o pipefail
|
|
131
|
+
gh api "repos/$upstream_repo/issues/{issue-number}/comments" \
|
|
132
|
+
--paginate --jq '.[] | @json' \
|
|
133
|
+
| node .agents/scripts/find-existing-task.js
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Windows (PowerShell 7+ / pwsh):
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
$ErrorActionPreference = 'Stop'
|
|
140
|
+
gh api "repos/$upstream_repo/issues/{issue-number}/comments" `
|
|
141
|
+
--paginate --jq '.[] | @json' |
|
|
142
|
+
node .agents/scripts/find-existing-task.js
|
|
143
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
On PowerShell 5.1, explicitly enable UTF-8 stdio first; otherwise the pipe may corrupt multibyte characters:
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
[Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new()
|
|
150
|
+
```
|
|
151
|
+
|
|
123
152
|
## PR Template and Metadata Helpers
|
|
124
153
|
|
|
125
154
|
Read a repository PR template when present:
|
|
@@ -120,6 +120,35 @@ gh issue close {issue-number} -R "$upstream_repo" --reason "{reason}"
|
|
|
120
120
|
gh api "repos/$upstream_repo/issues/{issue-number}/comments" --paginate
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
## 历史任务评论扫描
|
|
124
|
+
|
|
125
|
+
`find-existing-task.js` 仅消费 stdin,不直接调用 `gh`。由 AI 按宿主 OS 选择下面的 pipeline 命令。
|
|
126
|
+
|
|
127
|
+
POSIX(bash / zsh):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
set -o pipefail
|
|
131
|
+
gh api "repos/$upstream_repo/issues/{issue-number}/comments" \
|
|
132
|
+
--paginate --jq '.[] | @json' \
|
|
133
|
+
| node .agents/scripts/find-existing-task.js
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Windows(PowerShell 7+ / pwsh):
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
$ErrorActionPreference = 'Stop'
|
|
140
|
+
gh api "repos/$upstream_repo/issues/{issue-number}/comments" `
|
|
141
|
+
--paginate --jq '.[] | @json' |
|
|
142
|
+
node .agents/scripts/find-existing-task.js
|
|
143
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
在 PowerShell 5.1 上需先显式启用 UTF-8 stdio,否则 pipe 可能损坏多字节字符:
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
[Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new()
|
|
150
|
+
```
|
|
151
|
+
|
|
123
152
|
## PR 模板与元数据辅助命令
|
|
124
153
|
|
|
125
154
|
存在仓库 PR 模板时读取:
|