@fitlab-ai/agent-infra 0.7.2 → 0.7.4
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 +35 -787
- package/README.zh-CN.md +37 -762
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/defaults.json +0 -1
- package/dist/lib/init.js +0 -3
- package/dist/lib/sandbox/commands/create.js +44 -3
- package/dist/lib/sandbox/commands/enter.js +13 -15
- package/dist/lib/sandbox/commands/list-running.js +36 -1
- package/dist/lib/sandbox/commands/ls.js +9 -4
- package/dist/lib/sandbox/commands/rm.js +99 -19
- package/dist/lib/sandbox/commands/start.js +36 -0
- package/dist/lib/sandbox/index.js +11 -1
- package/dist/lib/sandbox/readme-scaffold.js +6 -6
- package/dist/lib/table.js +11 -2
- package/dist/lib/task/artifacts.js +58 -0
- package/dist/lib/task/commands/cat.js +38 -0
- package/dist/lib/task/commands/files.js +47 -0
- package/dist/lib/task/commands/grep.js +143 -0
- package/dist/lib/task/commands/log.js +75 -0
- package/dist/lib/task/commands/ls.js +1 -1
- package/dist/lib/task/commands/show.js +5 -114
- package/dist/lib/task/commands/status.js +239 -0
- package/dist/lib/task/index.js +37 -0
- package/dist/lib/task/resolve-ref.js +150 -0
- package/dist/lib/task/short-id.js +10 -0
- package/dist/lib/update.js +25 -8
- package/lib/defaults.json +0 -1
- package/lib/init.ts +0 -10
- package/lib/sandbox/commands/create.ts +47 -4
- package/lib/sandbox/commands/enter.ts +33 -14
- package/lib/sandbox/commands/list-running.ts +43 -1
- package/lib/sandbox/commands/ls.ts +12 -4
- package/lib/sandbox/commands/rm.ts +128 -19
- package/lib/sandbox/commands/start.ts +61 -0
- package/lib/sandbox/index.ts +11 -1
- package/lib/sandbox/readme-scaffold.ts +6 -6
- package/lib/table.ts +14 -2
- package/lib/task/artifacts.ts +72 -0
- package/lib/task/commands/cat.ts +39 -0
- package/lib/task/commands/files.ts +53 -0
- package/lib/task/commands/grep.ts +147 -0
- package/lib/task/commands/log.ts +80 -0
- package/lib/task/commands/ls.ts +1 -1
- package/lib/task/commands/show.ts +5 -117
- package/lib/task/commands/status.ts +302 -0
- package/lib/task/index.ts +37 -0
- package/lib/task/resolve-ref.ts +160 -0
- package/lib/task/short-id.ts +10 -0
- package/lib/update.ts +28 -10
- package/package.json +1 -1
- package/templates/.agents/README.en.md +1 -0
- package/templates/.agents/README.zh-CN.md +1 -0
- package/templates/.agents/hooks/auto-resume.sh +21 -4
- package/templates/.agents/rules/README.en.md +41 -0
- package/templates/.agents/rules/README.zh-CN.md +40 -0
- package/templates/.agents/rules/debugging-guide.en.md +25 -0
- package/templates/.agents/rules/debugging-guide.zh-CN.md +25 -0
- package/templates/.agents/rules/next-step-output.en.md +6 -3
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -3
- package/templates/.agents/rules/pr-checks-commands.en.md +5 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +62 -0
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +62 -0
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/pr-sync.github.en.md +7 -0
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +7 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +8 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +8 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +8 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +8 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/commit/SKILL.en.md +2 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -3
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +31 -23
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +31 -23
- package/templates/.agents/skills/complete-task/SKILL.en.md +36 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +36 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +16 -7
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +16 -7
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -0
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +0 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +131 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +131 -0
- package/templates/.agents/skills/watch-pr/config/verify.json +22 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +43 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +43 -0
- package/templates/.agents/templates/task.en.md +1 -0
- package/templates/.agents/templates/task.zh-CN.md +1 -0
- package/templates/.agents/workflows/bug-fix.en.yaml +6 -4
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +5 -4
- package/templates/.agents/workflows/feature-development.en.yaml +6 -4
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +5 -4
- package/templates/.agents/workflows/refactoring.en.yaml +6 -4
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +5 -4
- package/templates/.claude/commands/watch-pr.en.md +8 -0
- package/templates/.claude/commands/watch-pr.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/watch-pr.en.toml +8 -0
- package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +8 -0
- package/templates/.opencode/commands/watch-pr.en.md +11 -0
- package/templates/.opencode/commands/watch-pr.zh-CN.md +11 -0
package/bin/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ Usage:
|
|
|
18
18
|
agent-infra init Initialize a new project with update-agent-infra seed command
|
|
19
19
|
agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
|
|
20
20
|
agent-infra sandbox Manage Docker-based AI sandboxes
|
|
21
|
-
agent-infra task Read-only views over .agents/workspace tasks (ls / show)
|
|
21
|
+
agent-infra task Read-only views over .agents/workspace tasks (ls / show / files / cat / status / log / grep)
|
|
22
22
|
agent-infra update Update seed files and sync file registry for an existing project
|
|
23
23
|
agent-infra version Show version
|
|
24
24
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ Usage:
|
|
|
22
22
|
agent-infra init Initialize a new project with update-agent-infra seed command
|
|
23
23
|
agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
|
|
24
24
|
agent-infra sandbox Manage Docker-based AI sandboxes
|
|
25
|
-
agent-infra task Read-only views over .agents/workspace tasks (ls / show)
|
|
25
|
+
agent-infra task Read-only views over .agents/workspace tasks (ls / show / files / cat / status / log / grep)
|
|
26
26
|
agent-infra update Update seed files and sync file registry for an existing project
|
|
27
27
|
agent-infra version Show version
|
|
28
28
|
|
package/dist/lib/defaults.json
CHANGED
package/dist/lib/init.js
CHANGED
|
@@ -158,8 +158,6 @@ async function cmdInit() {
|
|
|
158
158
|
info(`Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
|
|
159
159
|
+ ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`);
|
|
160
160
|
}
|
|
161
|
-
const requiresPRChoice = await select('Require Pull Request flow?', ['yes', 'no'], 'yes');
|
|
162
|
-
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
163
161
|
let enabledTUIs;
|
|
164
162
|
try {
|
|
165
163
|
enabledTUIs = await multiSelect('Built-in TUI command files to install/manage', BUILTIN_TUI_IDS.map((id) => ({ id, label: BUILTIN_TUI_DISPLAY[id] })));
|
|
@@ -220,7 +218,6 @@ async function cmdInit() {
|
|
|
220
218
|
org: orgName,
|
|
221
219
|
language,
|
|
222
220
|
platform: { type: platformType },
|
|
223
|
-
requiresPullRequest,
|
|
224
221
|
templateVersion: VERSION,
|
|
225
222
|
sandbox: structuredClone(defaults.sandbox),
|
|
226
223
|
task: structuredClone(defaults.task),
|
|
@@ -604,7 +604,32 @@ export function ensureClaudeSettings(toolDir, hostHomeDir) {
|
|
|
604
604
|
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
|
-
|
|
607
|
+
function resolveHostCatalogPath(value, hostHomeDir) {
|
|
608
|
+
if (typeof value !== 'string' || value === '') {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
let resolved;
|
|
612
|
+
if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
|
|
613
|
+
resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
|
|
614
|
+
}
|
|
615
|
+
else if (path.isAbsolute(value)) {
|
|
616
|
+
resolved = value;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
resolved = path.join(hostHomeDir, '.codex', value);
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
if (!fs.statSync(resolved).isFile()) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
fs.accessSync(resolved, fs.constants.R_OK);
|
|
626
|
+
return resolved;
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodexDir = '/home/devuser/.codex') {
|
|
608
633
|
if (!hostHomeDir) {
|
|
609
634
|
return;
|
|
610
635
|
}
|
|
@@ -650,6 +675,22 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
|
|
|
650
675
|
sandboxParsed[key] = value;
|
|
651
676
|
changed = true;
|
|
652
677
|
}
|
|
678
|
+
if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
|
|
679
|
+
const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
|
|
680
|
+
if (hostCatalogPath) {
|
|
681
|
+
try {
|
|
682
|
+
const basename = path.basename(hostCatalogPath);
|
|
683
|
+
const destDir = path.join(toolDir, 'model-catalogs');
|
|
684
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
685
|
+
fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
|
|
686
|
+
sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
|
|
687
|
+
changed = true;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
// Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
653
694
|
if (changed) {
|
|
654
695
|
fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
|
|
655
696
|
}
|
|
@@ -780,7 +821,7 @@ function runEngineTaskCommand(engine, cmd, args, opts = {}) {
|
|
|
780
821
|
return runTaskCommand(command.cmd, command.args, opts);
|
|
781
822
|
}
|
|
782
823
|
export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
|
|
783
|
-
const selectedEngine = engine ?? detectEngine(config);
|
|
824
|
+
const selectedEngine = engine ?? detectEngine({ engine: config.engine });
|
|
784
825
|
const { uid: hostUid, gid: hostGid } = resolveBuildUid({
|
|
785
826
|
engine: selectedEngine,
|
|
786
827
|
runFn,
|
|
@@ -1023,7 +1064,7 @@ export async function create(args) {
|
|
|
1023
1064
|
}
|
|
1024
1065
|
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
1025
1066
|
if (codexEntry) {
|
|
1026
|
-
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home);
|
|
1067
|
+
ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
|
|
1027
1068
|
ensureCodexWorkspaceTrust(codexEntry.dir);
|
|
1028
1069
|
}
|
|
1029
1070
|
const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from "../constants.js";
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
3
3
|
import { detectEngine } from "../engine.js";
|
|
4
4
|
import { formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
|
|
5
|
-
import { runInteractiveEngine
|
|
6
|
-
import { resolveTaskBranch } from "../task-resolver.js";
|
|
5
|
+
import { runInteractiveEngine } from "../shell.js";
|
|
7
6
|
import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
|
|
8
7
|
import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
|
|
9
8
|
import { detectHostTimezone } from "../host-timezone.js";
|
|
10
|
-
import {
|
|
9
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
|
|
11
10
|
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
12
11
|
|
|
13
12
|
N (bare) and '#N' both reference the same active task short id from
|
|
@@ -86,19 +85,18 @@ export async function enter(args) {
|
|
|
86
85
|
validateClaudeCredentialsEnvOverride();
|
|
87
86
|
const engine = detectEngine(config);
|
|
88
87
|
const [firstArg = '', ...cmd] = args;
|
|
89
|
-
|
|
90
|
-
if (isTaskShortRef(firstArg)) {
|
|
91
|
-
branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
95
|
-
}
|
|
88
|
+
const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
|
|
96
89
|
assertValidBranchName(branch);
|
|
97
|
-
const running =
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
throw new Error(`No
|
|
90
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
91
|
+
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
|
|
94
|
+
}
|
|
95
|
+
if (!found.running) {
|
|
96
|
+
process.stderr.write(`Sandbox '${found.name}' is stopped; starting it...\n`);
|
|
97
|
+
startSandboxContainer(engine, found.name);
|
|
101
98
|
}
|
|
99
|
+
const container = found.name;
|
|
102
100
|
if (config.tools.includes('claude-code')) {
|
|
103
101
|
try {
|
|
104
102
|
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { runSafeEngine } from "../shell.js";
|
|
4
|
+
import { runSafeEngine, runVerboseEngine } from "../shell.js";
|
|
5
|
+
import { resolveTaskBranch } from "../task-resolver.js";
|
|
5
6
|
export function containerListFormat() {
|
|
6
7
|
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
7
8
|
}
|
|
@@ -139,4 +140,38 @@ export function resolveTaskShortRef(arg, ctx) {
|
|
|
139
140
|
`'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
|
|
140
141
|
`use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`);
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a sandbox command argument (`<branch | TASK-id | N | '#N'>`) to a
|
|
145
|
+
* branch name, mirroring `ai sandbox exec` so that `start` and `exec` share one
|
|
146
|
+
* input contract. Short refs go through the registry-only resolver (which throws
|
|
147
|
+
* an actionable error on a miss); everything else flows through resolveTaskBranch
|
|
148
|
+
* (plain branch names pass through unchanged, TASK-ids resolve via task.md).
|
|
149
|
+
* Callers still run assertValidBranchName on the result.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveBranchArg(arg, ctx) {
|
|
152
|
+
return isTaskShortRef(arg)
|
|
153
|
+
? resolveTaskShortRef(arg, ctx)
|
|
154
|
+
: resolveTaskBranch(arg, ctx.repoRoot);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Pick the sandbox container row whose name matches one of the candidate
|
|
158
|
+
* container names (covers both the '..' and legacy '-' branch sanitizations).
|
|
159
|
+
* Pure: no IO. Returns null when no row matches.
|
|
160
|
+
*/
|
|
161
|
+
export function selectSandboxContainer(rows, candidates) {
|
|
162
|
+
return rows.find((row) => candidates.includes(row.name)) ?? null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Start an existing (stopped) sandbox container by name. Throws a distinct,
|
|
166
|
+
* actionable error when `docker start` fails, so callers can tell "start failed"
|
|
167
|
+
* apart from "container not found".
|
|
168
|
+
*/
|
|
169
|
+
export function startSandboxContainer(engine, name) {
|
|
170
|
+
try {
|
|
171
|
+
runVerboseEngine(engine, 'docker', ['start', name]);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new Error(`Failed to start sandbox container '${name}': ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
142
177
|
//# sourceMappingURL=list-running.js.map
|
|
@@ -16,10 +16,12 @@ Lists all containers for the current project. The '#' column is a
|
|
|
16
16
|
display-only row number; the 'SHORT' column shows the active task short
|
|
17
17
|
id bound to each container's branch (via
|
|
18
18
|
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
19
|
-
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11')
|
|
19
|
+
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
20
|
+
A '-' means no active task is bound to that branch, so the sandbox is free
|
|
21
|
+
to remove with "ai sandbox rm <branch>".`;
|
|
20
22
|
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'];
|
|
21
|
-
export function formatContainerTable(rows) {
|
|
22
|
-
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]));
|
|
23
|
+
export function formatContainerTable(rows, zebra = false) {
|
|
24
|
+
return formatTable(CONTAINER_TABLE_HEADERS, rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch]), { zebra });
|
|
23
25
|
}
|
|
24
26
|
function listChildren(dir) {
|
|
25
27
|
if (!fs.existsSync(dir)) {
|
|
@@ -54,10 +56,13 @@ export function ls(args = []) {
|
|
|
54
56
|
branch: container.branch
|
|
55
57
|
};
|
|
56
58
|
});
|
|
57
|
-
for (const line of formatContainerTable(tableRows)) {
|
|
59
|
+
for (const line of formatContainerTable(tableRows, Boolean(process.stdout.isTTY))) {
|
|
58
60
|
process.stdout.write(` ${line}\n`);
|
|
59
61
|
}
|
|
60
62
|
process.stdout.write(` Total: ${ordered.length} containers\n`);
|
|
63
|
+
if (tableRows.some((r) => r.shortId === '-')) {
|
|
64
|
+
process.stdout.write(` SHORT '-' = no active task bound; that sandbox is free to remove with 'ai sandbox rm <branch>'.\n`);
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
p.log.step('Worktrees');
|
|
63
68
|
const worktrees = listChildren(config.worktreeBase);
|
|
@@ -11,12 +11,17 @@ import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
|
|
|
11
11
|
import { runOk, runSafe, runSafeEngine } from "../shell.js";
|
|
12
12
|
import { resolveTaskBranch } from "../task-resolver.js";
|
|
13
13
|
import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from "../tools.js";
|
|
14
|
-
|
|
14
|
+
import { fetchSandboxRows } from "./list-running.js";
|
|
15
|
+
import { lookupShortIdByBranch } from "../../task/short-id.js";
|
|
16
|
+
const USAGE = `Usage:
|
|
17
|
+
ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
|
|
18
|
+
ai sandbox rm --all [--dry-run] [--yes] Remove every sandbox not bound to an active task
|
|
19
|
+
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
15
20
|
export { assertManagedPath } from "../managed-fs.js";
|
|
16
21
|
function projectToolDirs(config, tools) {
|
|
17
22
|
return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
|
|
18
23
|
}
|
|
19
|
-
async function rmOne(config, tools, branch) {
|
|
24
|
+
async function rmOne(config, tools, branch, options = {}) {
|
|
20
25
|
assertValidBranchName(branch);
|
|
21
26
|
const engine = detectEngine(config);
|
|
22
27
|
let effectiveBranch = branch;
|
|
@@ -25,7 +30,9 @@ async function rmOne(config, tools, branch) {
|
|
|
25
30
|
tool,
|
|
26
31
|
candidates: toolConfigDirCandidates(tool, config.project, branch)
|
|
27
32
|
}));
|
|
28
|
-
|
|
33
|
+
if (!options.quiet) {
|
|
34
|
+
p.intro(pc.cyan(`Removing sandbox for ${branch}`));
|
|
35
|
+
}
|
|
29
36
|
const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
30
37
|
const matchedContainers = containerNameCandidates(config, branch)
|
|
31
38
|
.filter((name) => existing.includes(name));
|
|
@@ -57,10 +64,12 @@ async function rmOne(config, tools, branch) {
|
|
|
57
64
|
}
|
|
58
65
|
const existingWorktrees = worktreeCandidates.filter((candidate) => fs.existsSync(candidate));
|
|
59
66
|
if (existingWorktrees.length > 0) {
|
|
60
|
-
const shouldRemoveWorktree =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
const shouldRemoveWorktree = options.assumeYes
|
|
68
|
+
? true
|
|
69
|
+
: await p.confirm({
|
|
70
|
+
message: `Remove worktree(s): ${existingWorktrees.join(', ')}?`,
|
|
71
|
+
initialValue: true
|
|
72
|
+
});
|
|
64
73
|
if (p.isCancel(shouldRemoveWorktree)) {
|
|
65
74
|
p.outro('Cancelled');
|
|
66
75
|
return;
|
|
@@ -69,10 +78,12 @@ async function rmOne(config, tools, branch) {
|
|
|
69
78
|
for (const worktree of existingWorktrees) {
|
|
70
79
|
removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree);
|
|
71
80
|
}
|
|
72
|
-
const shouldDeleteBranch =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
const shouldDeleteBranch = options.assumeYes
|
|
82
|
+
? true
|
|
83
|
+
: await p.confirm({
|
|
84
|
+
message: `Also delete local branch '${effectiveBranch}'?`,
|
|
85
|
+
initialValue: true
|
|
86
|
+
});
|
|
76
87
|
if (!p.isCancel(shouldDeleteBranch) && shouldDeleteBranch) {
|
|
77
88
|
if (!runOk('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch])) {
|
|
78
89
|
p.log.warn(`Local branch '${effectiveBranch}' was not deleted`);
|
|
@@ -92,18 +103,22 @@ async function rmOne(config, tools, branch) {
|
|
|
92
103
|
}
|
|
93
104
|
const shareBranch = shareBranchDir(config, effectiveBranch);
|
|
94
105
|
if (fs.existsSync(shareBranch)) {
|
|
95
|
-
const shouldRemoveShare =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
const shouldRemoveShare = options.assumeYes
|
|
107
|
+
? true
|
|
108
|
+
: await p.confirm({
|
|
109
|
+
message: `Remove share dir for branch '${effectiveBranch}' (${shareBranch})?`,
|
|
110
|
+
initialValue: true
|
|
111
|
+
});
|
|
99
112
|
if (!p.isCancel(shouldRemoveShare) && shouldRemoveShare) {
|
|
100
113
|
removeManagedDir(config.shareBase, shareBranch);
|
|
101
114
|
p.log.success(`Share dir removed: ${shareBranch}`);
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
|
-
|
|
117
|
+
if (!options.quiet) {
|
|
118
|
+
p.outro(pc.green('Sandbox removed'));
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
|
-
async function
|
|
121
|
+
async function rmPurge(config, tools) {
|
|
107
122
|
const engine = detectEngine(config);
|
|
108
123
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
109
124
|
const containers = runSafeEngine(engine, 'docker', [
|
|
@@ -193,6 +208,52 @@ async function rmAll(config, tools) {
|
|
|
193
208
|
}
|
|
194
209
|
p.outro(pc.green('All project sandboxes removed'));
|
|
195
210
|
}
|
|
211
|
+
async function rmUnbound(config, tools, options) {
|
|
212
|
+
const engine = detectEngine(config);
|
|
213
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
214
|
+
const removable = [...running, ...nonRunning].filter((row) => row.branch && lookupShortIdByBranch(row.branch, config.repoRoot) === null);
|
|
215
|
+
p.intro(pc.cyan(`Removing sandboxes not bound to an active task for ${config.project}`));
|
|
216
|
+
if (removable.length === 0) {
|
|
217
|
+
p.outro('No removable sandboxes: every container is bound to an active task (or none exist)');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
for (const row of removable) {
|
|
221
|
+
p.log.message(`${row.name} ${row.branch}`);
|
|
222
|
+
}
|
|
223
|
+
if (options.dryRun) {
|
|
224
|
+
p.outro(`Dry run: ${removable.length} sandbox(es) would be removed, nothing deleted`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!options.assumeYes) {
|
|
228
|
+
if (!process.stdin.isTTY) {
|
|
229
|
+
throw new Error('Refusing to remove sandboxes without confirmation in a non-interactive shell; pass --yes to proceed.');
|
|
230
|
+
}
|
|
231
|
+
const confirmed = await p.confirm({
|
|
232
|
+
message: `Remove these ${removable.length} sandbox(es)?`,
|
|
233
|
+
initialValue: false
|
|
234
|
+
});
|
|
235
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
236
|
+
p.outro('Cancelled');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const failures = [];
|
|
241
|
+
for (const row of removable) {
|
|
242
|
+
try {
|
|
243
|
+
await rmOne(config, tools, row.branch, { assumeYes: true, quiet: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
failures.push({ branch: row.branch, message: error instanceof Error ? error.message : String(error) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (failures.length > 0) {
|
|
250
|
+
for (const failure of failures) {
|
|
251
|
+
p.log.error(`Failed to remove '${failure.branch}': ${failure.message}`);
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Removed ${removable.length - failures.length}/${removable.length} sandbox(es); ${failures.length} failed`);
|
|
254
|
+
}
|
|
255
|
+
p.outro(pc.green(`Removed ${removable.length} sandbox(es)`));
|
|
256
|
+
}
|
|
196
257
|
export async function rm(args) {
|
|
197
258
|
const { values, positionals } = parseArgs({
|
|
198
259
|
args,
|
|
@@ -200,6 +261,9 @@ export async function rm(args) {
|
|
|
200
261
|
strict: true,
|
|
201
262
|
options: {
|
|
202
263
|
all: { type: 'boolean' },
|
|
264
|
+
purge: { type: 'boolean' },
|
|
265
|
+
'dry-run': { type: 'boolean' },
|
|
266
|
+
yes: { type: 'boolean', short: 'y' },
|
|
203
267
|
help: { type: 'boolean', short: 'h' }
|
|
204
268
|
}
|
|
205
269
|
});
|
|
@@ -207,13 +271,29 @@ export async function rm(args) {
|
|
|
207
271
|
process.stdout.write(`${USAGE}\n`);
|
|
208
272
|
return;
|
|
209
273
|
}
|
|
210
|
-
if (
|
|
274
|
+
if (values.all && values.purge) {
|
|
275
|
+
throw new Error('--all and --purge are mutually exclusive');
|
|
276
|
+
}
|
|
277
|
+
if ((values['dry-run'] || values.yes) && !values.all) {
|
|
278
|
+
throw new Error('--dry-run and --yes only apply to --all');
|
|
279
|
+
}
|
|
280
|
+
if ((values.all || values.purge) && positionals.length > 0) {
|
|
281
|
+
throw new Error(`${values.all ? '--all' : '--purge'} does not take a branch argument`);
|
|
282
|
+
}
|
|
283
|
+
if (!values.all && !values.purge && positionals.length !== 1) {
|
|
211
284
|
throw new Error(USAGE);
|
|
212
285
|
}
|
|
213
286
|
const config = loadConfig();
|
|
214
287
|
const tools = resolveTools(config);
|
|
288
|
+
if (values.purge) {
|
|
289
|
+
await rmPurge(config, tools);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
215
292
|
if (values.all) {
|
|
216
|
-
await
|
|
293
|
+
await rmUnbound(config, tools, {
|
|
294
|
+
dryRun: Boolean(values['dry-run']),
|
|
295
|
+
assumeYes: Boolean(values.yes)
|
|
296
|
+
});
|
|
217
297
|
return;
|
|
218
298
|
}
|
|
219
299
|
const branch = resolveTaskBranch(positionals[0] ?? '', config.repoRoot);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
|
|
3
|
+
import { detectEngine } from "../engine.js";
|
|
4
|
+
import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
|
|
5
|
+
const USAGE = `Usage: ai sandbox start <branch | TASK-id | N | '#N'>
|
|
6
|
+
|
|
7
|
+
Start an existing sandbox container that has stopped (for example after the
|
|
8
|
+
Docker daemon was restarted or replaced). The container must already exist:
|
|
9
|
+
if none is found, run 'ai sandbox create <branch>' first. A container that is
|
|
10
|
+
already running is left untouched.`;
|
|
11
|
+
export async function start(args) {
|
|
12
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
13
|
+
process.stdout.write(`${USAGE}\n`);
|
|
14
|
+
if (args.length === 0) {
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const [firstArg = ''] = args;
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const engine = detectEngine(config);
|
|
22
|
+
const branch = resolveBranchArg(firstArg, { repoRoot: config.repoRoot });
|
|
23
|
+
assertValidBranchName(branch);
|
|
24
|
+
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
25
|
+
const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
|
|
26
|
+
if (!found) {
|
|
27
|
+
throw new Error(`No sandbox container for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
|
|
28
|
+
}
|
|
29
|
+
if (found.running) {
|
|
30
|
+
process.stdout.write(`Sandbox '${found.name}' is already running.\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
startSandboxContainer(engine, found.name);
|
|
34
|
+
process.stdout.write(`Started sandbox '${found.name}'.\n`);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -6,6 +6,9 @@ Commands:
|
|
|
6
6
|
Enter sandbox or run a command. N (bare) is the
|
|
7
7
|
recommended form for task short ids (e.g.
|
|
8
8
|
'ai sandbox exec 11'); '#N' is also accepted.
|
|
9
|
+
start <branch | TASK-id | N | '#N'>
|
|
10
|
+
Start an existing stopped sandbox container
|
|
11
|
+
(e.g. after the Docker daemon restarted)
|
|
9
12
|
ls List sandboxes for the current project (the '#'
|
|
10
13
|
column is a display-only row number; the 'SHORT'
|
|
11
14
|
column shows the active task short id, '-' if none)
|
|
@@ -13,7 +16,9 @@ Commands:
|
|
|
13
16
|
rebuild [--quiet] [--refresh]
|
|
14
17
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
15
18
|
refresh Sync host Claude Code credentials to all sandbox copies
|
|
16
|
-
rm <branch>
|
|
19
|
+
rm <branch> | --all | --purge
|
|
20
|
+
Remove one sandbox, all sandboxes not bound to an
|
|
21
|
+
active task (--all), or tear down everything (--purge)
|
|
17
22
|
vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
|
|
18
23
|
|
|
19
24
|
Run 'ai sandbox <command> --help' for details.`;
|
|
@@ -50,6 +55,11 @@ export async function runSandbox(args) {
|
|
|
50
55
|
}
|
|
51
56
|
break;
|
|
52
57
|
}
|
|
58
|
+
case 'start': {
|
|
59
|
+
const { start } = await import("./commands/start.js");
|
|
60
|
+
await start(rest);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
53
63
|
case 'ls': {
|
|
54
64
|
const { ls } = await import("./commands/ls.js");
|
|
55
65
|
ls(rest);
|
|
@@ -9,7 +9,7 @@ symlink under \`$HOME\` (e.g. \`.tmux.conf\` -> \`/home/devuser/.tmux.conf\`),
|
|
|
9
9
|
overriding image defaults so your editor, shell, and tool preferences follow
|
|
10
10
|
you across \`ai sandbox destroy + create\`.
|
|
11
11
|
|
|
12
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
12
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#user-level-dotfiles-channel
|
|
13
13
|
|
|
14
14
|
Common usage - drop files or symlinks here:
|
|
15
15
|
|
|
@@ -37,7 +37,7 @@ only writes \`README.md\` when it is missing, never when it already exists.
|
|
|
37
37
|
(例如 \`.tmux.conf -> /home/devuser/.tmux.conf\`),覆盖镜像默认值,让你的编辑器、
|
|
38
38
|
shell、工具偏好跨 \`ai sandbox destroy + create\` 持久存在。
|
|
39
39
|
|
|
40
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
40
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#用户级-dotfiles-通道
|
|
41
41
|
|
|
42
42
|
常见用法:把文件或符号链接放进来:
|
|
43
43
|
|
|
@@ -62,7 +62,7 @@ This directory is mounted **read-write** into every sandbox container of this
|
|
|
62
62
|
project at \`/share/common\`, regardless of branch. Drop files here to share
|
|
63
63
|
between host and any sandbox without polluting the git worktree.
|
|
64
64
|
|
|
65
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
65
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
|
|
66
66
|
|
|
67
67
|
This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
68
68
|
|
|
@@ -73,7 +73,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
|
73
73
|
该目录被以**读写**方式挂载到本项目所有 sandbox 容器的 \`/share/common\`,
|
|
74
74
|
跨分支可见。可用来在宿主和任意 sandbox 之间传文件,无需弄脏 git worktree。
|
|
75
75
|
|
|
76
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
76
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
|
|
77
77
|
|
|
78
78
|
该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
|
|
79
79
|
`;
|
|
@@ -83,7 +83,7 @@ This directory is mounted **read-write** into the sandbox container of this
|
|
|
83
83
|
project's current branch at \`/share/branch\`. Files here are exclusive to this
|
|
84
84
|
branch's sandbox and do not leak across branches.
|
|
85
85
|
|
|
86
|
-
See: https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
86
|
+
See: https://github.com/fitlab-ai/agent-infra/blob/main/docs/en/sandbox.md#host-sandbox-file-exchange
|
|
87
87
|
|
|
88
88
|
This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
89
89
|
|
|
@@ -94,7 +94,7 @@ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
|
|
|
94
94
|
该目录被以**读写**方式挂载到本项目当前分支 sandbox 容器的 \`/share/branch\`,
|
|
95
95
|
仅当前分支可见,不会跨分支泄漏。
|
|
96
96
|
|
|
97
|
-
参考:https://github.com/fitlab-ai/agent-infra/blob/main/
|
|
97
|
+
参考:https://github.com/fitlab-ai/agent-infra/blob/main/docs/zh-CN/sandbox.md#宿主-沙箱文件交换
|
|
98
98
|
|
|
99
99
|
该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
|
|
100
100
|
`;
|
package/dist/lib/table.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
function formatTable(headers, rows, options = {}) {
|
|
3
|
+
const { zebra = false } = options;
|
|
2
4
|
const columnCount = headers.length;
|
|
3
5
|
const widths = headers.map((header, i) => {
|
|
4
6
|
const headerLen = header.length;
|
|
@@ -23,7 +25,14 @@ function formatTable(headers, rows) {
|
|
|
23
25
|
}
|
|
24
26
|
return parts.join(' ').trimEnd();
|
|
25
27
|
};
|
|
26
|
-
|
|
28
|
+
const dataLines = rows.map((row, i) => {
|
|
29
|
+
const line = renderRow(row);
|
|
30
|
+
// Zebra stripes: dim even-numbered data rows (rows 2, 4, 6... -> 0-based
|
|
31
|
+
// odd index). The header and odd rows are left untouched. When zebra is
|
|
32
|
+
// off, pc.dim is never called, so the output is byte-identical to before.
|
|
33
|
+
return zebra && i % 2 === 1 ? pc.dim(line) : line;
|
|
34
|
+
});
|
|
35
|
+
return [renderRow(headers), ...dataLines];
|
|
27
36
|
}
|
|
28
37
|
export { formatTable };
|
|
29
38
|
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Enumerate a task directory's artifacts ordered by modification time, oldest
|
|
5
|
+
* first, so the listing reads like the task's timeline. Filename ascending is a
|
|
6
|
+
* deterministic tiebreak when two files share the same mtime (e.g. written in
|
|
7
|
+
* the same millisecond).
|
|
8
|
+
*
|
|
9
|
+
* Only top-level regular files are included; subdirectories and dotfiles are
|
|
10
|
+
* skipped so every entry is something `cat` can print. The returned 1-based
|
|
11
|
+
* `index` is the source of truth shared by `files` and `cat`.
|
|
12
|
+
*/
|
|
13
|
+
function enumerateArtifacts(taskDir) {
|
|
14
|
+
const entries = fs
|
|
15
|
+
.readdirSync(taskDir, { withFileTypes: true })
|
|
16
|
+
.filter((dirent) => dirent.isFile() && !dirent.name.startsWith('.'))
|
|
17
|
+
.map((dirent) => {
|
|
18
|
+
const abs = path.join(taskDir, dirent.name);
|
|
19
|
+
const stat = fs.statSync(abs);
|
|
20
|
+
return { name: dirent.name, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
21
|
+
});
|
|
22
|
+
entries.sort((a, b) => {
|
|
23
|
+
if (a.mtimeMs !== b.mtimeMs)
|
|
24
|
+
return a.mtimeMs - b.mtimeMs;
|
|
25
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
26
|
+
});
|
|
27
|
+
return entries.map((entry, i) => ({ index: i + 1, ...entry }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve an artifact selector to an absolute path within `taskDir`. The
|
|
31
|
+
* selector is either a 1-based index `N` (as listed by `files`) or a filename
|
|
32
|
+
* (with or without the `.md` suffix). Throws with a clear message on failure.
|
|
33
|
+
*/
|
|
34
|
+
function resolveArtifact(taskDir, artifactOrN) {
|
|
35
|
+
if (path.basename(artifactOrN) !== artifactOrN) {
|
|
36
|
+
throw new Error('artifact name must not contain path separators');
|
|
37
|
+
}
|
|
38
|
+
if (/^\d+$/.test(artifactOrN)) {
|
|
39
|
+
const n = Number(artifactOrN);
|
|
40
|
+
const match = enumerateArtifacts(taskDir).find((a) => a.index === n);
|
|
41
|
+
if (!match) {
|
|
42
|
+
throw new Error(`invalid artifact index ${n} (run 'ai task files <ref>' to list)`);
|
|
43
|
+
}
|
|
44
|
+
return match.path;
|
|
45
|
+
}
|
|
46
|
+
const candidates = artifactOrN.endsWith('.md')
|
|
47
|
+
? [artifactOrN]
|
|
48
|
+
: [artifactOrN, `${artifactOrN}.md`];
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
const abs = path.join(taskDir, candidate);
|
|
51
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
52
|
+
return abs;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`artifact '${artifactOrN}' not found in task directory`);
|
|
56
|
+
}
|
|
57
|
+
export { enumerateArtifacts, resolveArtifact };
|
|
58
|
+
//# sourceMappingURL=artifacts.js.map
|