@fitlab-ai/agent-infra 0.6.4 → 0.7.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 +63 -27
- package/README.zh-CN.md +61 -25
- package/bin/cli.ts +18 -6
- package/dist/bin/cli.js +20 -6
- package/dist/lib/cp.js +127 -0
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/init.js +3 -0
- package/dist/lib/sandbox/clipboard/bridge.js +23 -4
- package/dist/lib/sandbox/clipboard/index.js +12 -3
- package/dist/lib/sandbox/commands/create.js +11 -2
- package/dist/lib/sandbox/commands/enter.js +29 -6
- package/dist/lib/sandbox/commands/list-running.js +108 -0
- package/dist/lib/sandbox/commands/ls.js +24 -45
- package/dist/lib/sandbox/commands/rebuild.js +15 -7
- package/dist/lib/sandbox/config.js +3 -0
- package/dist/lib/sandbox/index.js +6 -4
- package/dist/lib/sandbox/readme-scaffold.js +148 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
- package/dist/lib/sandbox/tools.js +213 -8
- package/dist/lib/update.js +12 -1
- package/lib/cp.ts +177 -0
- package/lib/defaults.json +1 -0
- package/lib/init.ts +10 -0
- package/lib/sandbox/clipboard/bridge.ts +23 -4
- package/lib/sandbox/clipboard/index.ts +12 -3
- package/lib/sandbox/commands/create.ts +18 -2
- package/lib/sandbox/commands/enter.ts +48 -6
- package/lib/sandbox/commands/list-running.ts +135 -0
- package/lib/sandbox/commands/ls.ts +28 -49
- package/lib/sandbox/commands/rebuild.ts +24 -7
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/index.ts +6 -4
- package/lib/sandbox/readme-scaffold.ts +177 -0
- package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
- package/lib/sandbox/runtimes/base.dockerfile +3 -3
- package/lib/sandbox/tools.ts +248 -9
- package/lib/update.ts +15 -1
- package/package.json +1 -1
- package/templates/.agents/QUICKSTART.en.md +1 -1
- package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
- package/templates/.agents/README.en.md +79 -2
- package/templates/.agents/README.zh-CN.md +79 -2
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +6 -5
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
- package/templates/.agents/rules/pr-sync.github.en.md +4 -5
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
- package/templates/.agents/rules/task-management.en.md +9 -6
- package/templates/.agents/rules/task-management.zh-CN.md +9 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -2
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
- package/templates/.agents/scripts/validate-artifact.js +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
- package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
- package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
- package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
- package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
- package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
- package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
- package/templates/.agents/skills/commit/SKILL.en.md +2 -2
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -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 +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
- package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
- package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
- package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
- package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
- package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
- package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
- package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
- package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
- package/templates/.agents/skills/test/SKILL.en.md +2 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
- package/templates/.agents/templates/task.en.md +3 -3
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
- package/templates/.agents/workflows/feature-development.en.yaml +115 -70
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
- package/templates/.agents/workflows/refactoring.en.yaml +123 -78
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
- package/templates/.claude/commands/code-task.en.md +8 -0
- package/templates/.claude/commands/code-task.zh-CN.md +8 -0
- package/templates/.claude/commands/review-analysis.en.md +8 -0
- package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
- package/templates/.claude/commands/review-code.en.md +8 -0
- package/templates/.claude/commands/review-code.zh-CN.md +8 -0
- package/templates/.claude/commands/review-plan.en.md +8 -0
- package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
- package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
- package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
- package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
- package/templates/.opencode/commands/code-task.en.md +11 -0
- package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-analysis.en.md +11 -0
- package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-code.en.md +11 -0
- package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
- package/templates/.opencode/commands/review-plan.en.md +11 -0
- package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
- package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
- package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
- package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
- package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
- package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
- package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
- package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
- package/templates/.claude/commands/implement-task.en.md +0 -8
- package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
- package/templates/.claude/commands/refine-task.en.md +0 -8
- package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
- package/templates/.claude/commands/review-task.en.md +0 -8
- package/templates/.claude/commands/review-task.zh-CN.md +0 -8
- package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
- package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
- package/templates/.opencode/commands/implement-task.en.md +0 -11
- package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/refine-task.en.md +0 -11
- package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
- package/templates/.opencode/commands/review-task.en.md +0 -11
- package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
- /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
package/lib/cp.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { platform as currentPlatform, tmpdir as defaultTmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { createClipboardAdapter, type ClipboardAdapter } from './sandbox/clipboard/index.ts';
|
|
8
|
+
|
|
9
|
+
const USAGE = 'Usage: ai cp <ssh-alias>\n\nCopy the local clipboard image (PNG) to a remote macOS NSPasteboard over ssh/scp.\n';
|
|
10
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
export type SpawnResult = {
|
|
13
|
+
status: number | null;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
error?: Error;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SpawnFn = (cmd: string, args: string[], input?: string) => SpawnResult;
|
|
20
|
+
type CreateAdapterFn = (options?: { platformName?: NodeJS.Platform }) => ClipboardAdapter | null;
|
|
21
|
+
type MkDTempFn = (prefix: string) => string;
|
|
22
|
+
type WriteFileFn = (file: string, data: Buffer) => void;
|
|
23
|
+
type RmFn = (target: string, options: { recursive: boolean; force: boolean }) => void;
|
|
24
|
+
|
|
25
|
+
export type CpDeps = {
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
createAdapter?: CreateAdapterFn;
|
|
28
|
+
spawnFn?: SpawnFn;
|
|
29
|
+
randomId?: () => string;
|
|
30
|
+
mkdtempFn?: MkDTempFn;
|
|
31
|
+
writeFileFn?: WriteFileFn;
|
|
32
|
+
rmFn?: RmFn;
|
|
33
|
+
tmpdir?: () => string;
|
|
34
|
+
writeStdout?: (chunk: string) => unknown;
|
|
35
|
+
writeStderr?: (chunk: string) => unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function runCommand(cmd: string, args: string[], input?: string): SpawnResult {
|
|
39
|
+
const result = spawnSync(cmd, args, {
|
|
40
|
+
input,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
timeout: COMMAND_TIMEOUT_MS
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
status: result.status,
|
|
48
|
+
stdout: result.stdout ?? '',
|
|
49
|
+
stderr: result.stderr ?? '',
|
|
50
|
+
error: result.error
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdCp(args: string[], deps: CpDeps = {}): Promise<number> {
|
|
55
|
+
const {
|
|
56
|
+
platform = currentPlatform(),
|
|
57
|
+
createAdapter = createClipboardAdapter,
|
|
58
|
+
spawnFn = runCommand,
|
|
59
|
+
randomId = randomUUID,
|
|
60
|
+
mkdtempFn = fs.mkdtempSync,
|
|
61
|
+
writeFileFn = fs.writeFileSync,
|
|
62
|
+
rmFn = fs.rmSync,
|
|
63
|
+
tmpdir = defaultTmpdir,
|
|
64
|
+
writeStdout = (chunk: string) => process.stdout.write(chunk),
|
|
65
|
+
writeStderr = (chunk: string) => process.stderr.write(chunk)
|
|
66
|
+
} = deps;
|
|
67
|
+
|
|
68
|
+
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
69
|
+
writeStdout(USAGE);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let positionals: string[];
|
|
74
|
+
try {
|
|
75
|
+
({ positionals } = parseArgs({ args, allowPositionals: true, strict: true }));
|
|
76
|
+
} catch {
|
|
77
|
+
writeStderr(USAGE);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const alias = positionals[0];
|
|
82
|
+
if (!alias || positionals.length !== 1) {
|
|
83
|
+
writeStderr(USAGE);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
if (alias.startsWith('-')) {
|
|
87
|
+
writeStderr(`invalid ssh alias '${alias}': must not start with '-'\n`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (platform !== 'darwin') {
|
|
92
|
+
writeStderr(`ai cp currently supports macOS senders only (got ${platform})\n`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const adapter = createAdapter({ platformName: platform });
|
|
97
|
+
const png = adapter?.readImagePng() ?? null;
|
|
98
|
+
if (png === null) {
|
|
99
|
+
writeStderr('no image on clipboard\n');
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let uploaded = false;
|
|
104
|
+
let localTmpDir: string | null = null;
|
|
105
|
+
let remotePath: string | null = null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
localTmpDir = mkdtempFn(path.join(tmpdir(), 'agent-infra-cp-'));
|
|
109
|
+
const localPng = path.join(localTmpDir, 'clipboard.png');
|
|
110
|
+
writeFileFn(localPng, png);
|
|
111
|
+
|
|
112
|
+
remotePath = `/tmp/agent-infra-cp-${randomId()}.png`;
|
|
113
|
+
const upload = spawnFn('scp', [
|
|
114
|
+
'-o',
|
|
115
|
+
'BatchMode=yes',
|
|
116
|
+
'-o',
|
|
117
|
+
'ConnectTimeout=10',
|
|
118
|
+
localPng,
|
|
119
|
+
`${alias}:${remotePath}`
|
|
120
|
+
]);
|
|
121
|
+
if (upload.status !== 0) {
|
|
122
|
+
writeStderr(`failed to upload image to ${alias}:\n${commandDetail(upload)}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
uploaded = true;
|
|
126
|
+
|
|
127
|
+
// Remote write currently targets macOS only: it pipes an AppleScript to the
|
|
128
|
+
// remote `osascript` to set its NSPasteboard. This is the extension point for
|
|
129
|
+
// other remote platforms later (e.g. dispatch on remote OS to wl-copy/xclip
|
|
130
|
+
// on Linux); a non-macOS remote fails here with a clear non-zero error today.
|
|
131
|
+
const setRemote = spawnFn('ssh', [
|
|
132
|
+
'-o',
|
|
133
|
+
'BatchMode=yes',
|
|
134
|
+
'-o',
|
|
135
|
+
'ConnectTimeout=10',
|
|
136
|
+
alias,
|
|
137
|
+
'osascript',
|
|
138
|
+
'-'
|
|
139
|
+
], remoteSetScript(remotePath));
|
|
140
|
+
if (setRemote.status !== 0) {
|
|
141
|
+
writeStderr(`failed to set remote clipboard on ${alias}:\n${commandDetail(setRemote)}\n`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeStdout(`copied clipboard image to ${alias}\n`);
|
|
146
|
+
return 0;
|
|
147
|
+
} finally {
|
|
148
|
+
if (uploaded && remotePath) {
|
|
149
|
+
spawnFn('ssh', [
|
|
150
|
+
'-o',
|
|
151
|
+
'BatchMode=yes',
|
|
152
|
+
'-o',
|
|
153
|
+
'ConnectTimeout=10',
|
|
154
|
+
alias,
|
|
155
|
+
'rm',
|
|
156
|
+
'-f',
|
|
157
|
+
remotePath
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
if (localTmpDir) {
|
|
161
|
+
rmFn(localTmpDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function commandDetail(result: SpawnResult): string {
|
|
167
|
+
const detail = result.stderr || result.error?.message || result.stdout || 'unknown error';
|
|
168
|
+
return detail.trimEnd();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function remoteSetScript(remotePath: string): string {
|
|
172
|
+
const escapedPath = remotePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
173
|
+
return [
|
|
174
|
+
`set theFile to POSIX file "${escapedPath}"`,
|
|
175
|
+
'set the clipboard to (read theFile as «class PNGf»)'
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
package/lib/defaults.json
CHANGED
package/lib/init.ts
CHANGED
|
@@ -24,6 +24,7 @@ type Defaults = {
|
|
|
24
24
|
files: FileRegistry;
|
|
25
25
|
sandbox: Record<string, unknown>;
|
|
26
26
|
labels: Record<string, unknown>;
|
|
27
|
+
requiresPullRequest: boolean;
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
type AgentConfig = {
|
|
@@ -31,6 +32,7 @@ type AgentConfig = {
|
|
|
31
32
|
org: string;
|
|
32
33
|
language: string;
|
|
33
34
|
platform: { type: string };
|
|
35
|
+
requiresPullRequest: boolean;
|
|
34
36
|
templateVersion: string;
|
|
35
37
|
sandbox: Record<string, unknown>;
|
|
36
38
|
labels: Record<string, unknown>;
|
|
@@ -207,6 +209,13 @@ async function cmdInit(): Promise<void> {
|
|
|
207
209
|
);
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
const requiresPRChoice = await select(
|
|
213
|
+
'Require Pull Request flow?',
|
|
214
|
+
['yes', 'no'],
|
|
215
|
+
'yes'
|
|
216
|
+
);
|
|
217
|
+
const requiresPullRequest = requiresPRChoice !== 'no';
|
|
218
|
+
|
|
210
219
|
const templateSources = parseLocalSources(await prompt(
|
|
211
220
|
'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
|
|
212
221
|
''
|
|
@@ -280,6 +289,7 @@ async function cmdInit(): Promise<void> {
|
|
|
280
289
|
org: orgName,
|
|
281
290
|
language,
|
|
282
291
|
platform: { type: platformType },
|
|
292
|
+
requiresPullRequest,
|
|
283
293
|
templateVersion: VERSION,
|
|
284
294
|
sandbox: structuredClone(defaults.sandbox),
|
|
285
295
|
labels: structuredClone(defaults.labels),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { createClipboardAdapter, type ClipboardAdapter } from './index.ts';
|
|
3
4
|
import { buildBracketedPaste, CtrlVDetector, type CtrlVMatch } from './keys.ts';
|
|
4
5
|
import {
|
|
@@ -32,6 +33,26 @@ type BridgeOptions = {
|
|
|
32
33
|
const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
|
|
33
34
|
const PARTIAL_ESCAPE_FLUSH_MS = 30;
|
|
34
35
|
|
|
36
|
+
// Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
|
|
37
|
+
// cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
|
|
38
|
+
// set on the shared host TTY. With ONLCR on, the kernel rewrites the bare \n
|
|
39
|
+
// that tmux emits after homing the cursor inside the right pane into \r\n,
|
|
40
|
+
// snapping the cursor to column 1 so the following erase/redraw wipes the left
|
|
41
|
+
// pane. Clearing OPOST brings the host TTY in line with the non-bridge path.
|
|
42
|
+
// Best-effort: setRawMode(false) on teardown restores the original termios, and
|
|
43
|
+
// a missing/failed stty only reinstates the redraw glitch.
|
|
44
|
+
function disableOutputPostProcessing(stdin: NodeJS.ReadStream): void {
|
|
45
|
+
const candidate = (stdin as { fd?: unknown }).fd;
|
|
46
|
+
if (typeof candidate !== 'number') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
spawnSync('stty', ['-opost'], { stdio: [candidate, 'ignore', 'ignore'] });
|
|
51
|
+
} catch {
|
|
52
|
+
// stty unavailable or fd is not a tty; leave the terminal as-is.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export async function runInteractiveWithClipboardBridge(options: BridgeOptions): Promise<number> {
|
|
36
57
|
const {
|
|
37
58
|
engine,
|
|
@@ -56,14 +77,11 @@ export async function runInteractiveWithClipboardBridge(options: BridgeOptions):
|
|
|
56
77
|
return runInteractive(engine, 'docker', dockerArgs);
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
if (platformName !== 'darwin') {
|
|
60
|
-
return runInteractive(engine, 'docker', dockerArgs);
|
|
61
|
-
}
|
|
62
80
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
63
81
|
return fallback('host stdin/stdout is not a TTY');
|
|
64
82
|
}
|
|
65
83
|
if (!adapter) {
|
|
66
|
-
return fallback('
|
|
84
|
+
return fallback('no clipboard adapter available on this platform');
|
|
67
85
|
}
|
|
68
86
|
const available = adapter.available();
|
|
69
87
|
if (!available.ok) {
|
|
@@ -185,6 +203,7 @@ async function runBridge({
|
|
|
185
203
|
|
|
186
204
|
try {
|
|
187
205
|
stdin.setRawMode?.(true);
|
|
206
|
+
disableOutputPostProcessing(stdin);
|
|
188
207
|
stdin.resume();
|
|
189
208
|
stdin.on('data', onData);
|
|
190
209
|
stdout.on('resize', onResize);
|
|
@@ -6,8 +6,17 @@ export type ClipboardAdapter = DarwinClipboardAdapter;
|
|
|
6
6
|
export function createClipboardAdapter({
|
|
7
7
|
platformName = platform()
|
|
8
8
|
}: { platformName?: NodeJS.Platform } = {}): ClipboardAdapter | null {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
switch (platformName) {
|
|
10
|
+
case 'darwin':
|
|
11
|
+
return createDarwinClipboardAdapter();
|
|
12
|
+
case 'linux':
|
|
13
|
+
// Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
|
|
14
|
+
// see Issue #386 follow-up. Returning null disables the bridge for now.
|
|
15
|
+
return null;
|
|
16
|
+
case 'win32':
|
|
17
|
+
// Future work: native Win32 clipboard reader. Returning null disables the bridge.
|
|
18
|
+
return null;
|
|
19
|
+
default:
|
|
20
|
+
return null;
|
|
11
21
|
}
|
|
12
|
-
return createDarwinClipboardAdapter();
|
|
13
22
|
}
|
|
@@ -36,13 +36,20 @@ import {
|
|
|
36
36
|
runVerboseEngine
|
|
37
37
|
} from '../shell.ts';
|
|
38
38
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
imageSignatureFields,
|
|
41
|
+
resolveTools,
|
|
42
|
+
toolConfigDirCandidates,
|
|
43
|
+
toolNpmPackagesArg,
|
|
44
|
+
toolShellInstallScriptBase64
|
|
45
|
+
} from '../tools.ts';
|
|
40
46
|
import type { SandboxTool } from '../tools.ts';
|
|
41
47
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
42
48
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
43
49
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
44
50
|
import { resolveBuildUid } from '../engines/native.ts';
|
|
45
51
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
52
|
+
import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
|
|
46
53
|
import {
|
|
47
54
|
prepareClaudeCredentials,
|
|
48
55
|
redactCommandError,
|
|
@@ -112,7 +119,7 @@ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTo
|
|
|
112
119
|
return createHash('sha256')
|
|
113
120
|
.update(JSON.stringify({
|
|
114
121
|
dockerfile: preparedDockerfile.signature,
|
|
115
|
-
tools: tools
|
|
122
|
+
tools: imageSignatureFields(tools)
|
|
116
123
|
}))
|
|
117
124
|
.digest('hex')
|
|
118
125
|
.slice(0, 12);
|
|
@@ -1062,6 +1069,8 @@ export function buildImage(
|
|
|
1062
1069
|
`HOST_GID=${hostGid}`,
|
|
1063
1070
|
'--build-arg',
|
|
1064
1071
|
`AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
|
|
1072
|
+
'--build-arg',
|
|
1073
|
+
`AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
|
|
1065
1074
|
'--label',
|
|
1066
1075
|
sandboxLabel(config),
|
|
1067
1076
|
'--label',
|
|
@@ -1287,6 +1296,13 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1287
1296
|
message(`Created default sandbox aliases at ${aliasesFile.path}`);
|
|
1288
1297
|
}
|
|
1289
1298
|
|
|
1299
|
+
const readmeResults = ensureSandboxDiscoveryReadmes(effectiveConfig, branch);
|
|
1300
|
+
for (const { created, path: readmePath } of readmeResults) {
|
|
1301
|
+
if (created) {
|
|
1302
|
+
message(`Created discovery README at ${readmePath}`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1290
1306
|
const gitconfigPath = path.join(effectiveConfig.home, '.gitconfig');
|
|
1291
1307
|
const gitconfigContent = fs.existsSync(gitconfigPath)
|
|
1292
1308
|
? fs.readFileSync(gitconfigPath, 'utf8')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from '../config.ts';
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
3
3
|
import { detectEngine } from '../engine.ts';
|
|
4
4
|
import {
|
|
5
5
|
formatCredentialWarnings,
|
|
@@ -13,8 +13,12 @@ import { resolveTaskBranch } from '../task-resolver.ts';
|
|
|
13
13
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
14
14
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
|
|
15
15
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
16
|
+
import { fetchSandboxRows, isTaskShortRef, resolveTaskShortRef } from './list-running.ts';
|
|
16
17
|
|
|
17
|
-
const USAGE = `Usage: ai sandbox exec <branch> [cmd...]
|
|
18
|
+
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
|
|
19
|
+
|
|
20
|
+
'#N' references the N-th running sandbox in 'ai sandbox ls' order (1-based).
|
|
21
|
+
Quote it as '#N' to avoid shell '#' comment handling.`;
|
|
18
22
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
19
23
|
|
|
20
24
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
@@ -45,6 +49,37 @@ export function hostTimezoneEnvFlags(detect = detectHostTimezone): string[] {
|
|
|
45
49
|
return tz ? ['-e', `TZ=${tz}`] : [];
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
export function clipboardBridgeDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
53
|
+
const value = (env.AI_SANDBOX_NO_CLIPBOARD_BRIDGE ?? '').trim().toLowerCase();
|
|
54
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function runSandboxInteractive(params: {
|
|
58
|
+
engine: string;
|
|
59
|
+
dockerArgs: string[];
|
|
60
|
+
container: string;
|
|
61
|
+
home: string;
|
|
62
|
+
env?: NodeJS.ProcessEnv;
|
|
63
|
+
runBridge?: typeof runInteractiveWithClipboardBridge;
|
|
64
|
+
runInteractive?: typeof runInteractiveEngine;
|
|
65
|
+
}): number | Promise<number> {
|
|
66
|
+
const {
|
|
67
|
+
engine,
|
|
68
|
+
dockerArgs,
|
|
69
|
+
container,
|
|
70
|
+
home,
|
|
71
|
+
env = process.env,
|
|
72
|
+
runBridge = runInteractiveWithClipboardBridge,
|
|
73
|
+
runInteractive = runInteractiveEngine
|
|
74
|
+
} = params;
|
|
75
|
+
|
|
76
|
+
if (clipboardBridgeDisabled(env)) {
|
|
77
|
+
return runInteractive(engine, 'docker', dockerArgs);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return runBridge({ engine, dockerArgs, container, home });
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
export function formatCredentialSyncStatus(
|
|
49
84
|
result: ReturnType<typeof reconcileClaudeCredentials>,
|
|
50
85
|
isTTY = process.stderr.isTTY
|
|
@@ -84,8 +119,14 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
84
119
|
const config = loadConfig();
|
|
85
120
|
validateClaudeCredentialsEnvOverride();
|
|
86
121
|
const engine = detectEngine(config);
|
|
87
|
-
const [
|
|
88
|
-
|
|
122
|
+
const [firstArg = '', ...cmd] = args;
|
|
123
|
+
let branch: string;
|
|
124
|
+
if (isTaskShortRef(firstArg)) {
|
|
125
|
+
const { running } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
126
|
+
branch = resolveTaskShortRef(firstArg, { running });
|
|
127
|
+
} else {
|
|
128
|
+
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
129
|
+
}
|
|
89
130
|
assertValidBranchName(branch);
|
|
90
131
|
const running = runSafeEngine(engine, 'docker', ['ps', '--format', '{{.Names}}']).split('\n');
|
|
91
132
|
const container = containerNameCandidates(config, branch).find((name) => running.includes(name));
|
|
@@ -115,9 +156,10 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
115
156
|
process.stderr.write(`Warning: dotfiles snapshot rebuild failed: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
|
|
159
|
+
const dockerArgs = ['exec', '-it', ...envFlags, container, 'bash', TMUX_ENTRY_PATH];
|
|
160
|
+
return runSandboxInteractive({
|
|
119
161
|
engine,
|
|
120
|
-
dockerArgs
|
|
162
|
+
dockerArgs,
|
|
121
163
|
container,
|
|
122
164
|
home: config.home
|
|
123
165
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { runSafeEngine } from '../shell.ts';
|
|
2
|
+
|
|
3
|
+
export type SandboxRow = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
running: boolean;
|
|
8
|
+
index: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function containerListFormat(): string {
|
|
12
|
+
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseLabels(csv: string): Record<string, string> {
|
|
16
|
+
if (!csv) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const labels: Record<string, string> = {};
|
|
21
|
+
for (const pair of csv.split(',')) {
|
|
22
|
+
if (!pair) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const eq = pair.indexOf('=');
|
|
26
|
+
if (eq < 0) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
labels[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
30
|
+
}
|
|
31
|
+
return labels;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseSandboxRows(rawOutput: string, branchKey: string): SandboxRow[] {
|
|
35
|
+
if (!rawOutput) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return rawOutput.split('\n').map((line) => {
|
|
39
|
+
const [name = '', status = '', labelsCsv = ''] = line.split('\t');
|
|
40
|
+
const branch = parseLabels(labelsCsv)[branchKey] ?? '';
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
status,
|
|
44
|
+
branch,
|
|
45
|
+
running: status.startsWith('Up '),
|
|
46
|
+
index: null
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sortAndIndexSandboxRows(rows: SandboxRow[]): {
|
|
52
|
+
running: SandboxRow[];
|
|
53
|
+
nonRunning: SandboxRow[];
|
|
54
|
+
} {
|
|
55
|
+
const byName = (a: SandboxRow, b: SandboxRow): number => {
|
|
56
|
+
if (a.name < b.name) return -1;
|
|
57
|
+
if (a.name > b.name) return 1;
|
|
58
|
+
return 0;
|
|
59
|
+
};
|
|
60
|
+
const running = rows.filter((row) => row.running).sort(byName).map((row, i) => ({
|
|
61
|
+
...row,
|
|
62
|
+
index: i + 1
|
|
63
|
+
}));
|
|
64
|
+
const nonRunning = rows.filter((row) => !row.running).sort(byName).map((row) => ({
|
|
65
|
+
...row,
|
|
66
|
+
index: null
|
|
67
|
+
}));
|
|
68
|
+
return { running, nonRunning };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function fetchSandboxRows(
|
|
72
|
+
engine: string,
|
|
73
|
+
label: string,
|
|
74
|
+
branchKey: string
|
|
75
|
+
): { running: SandboxRow[]; nonRunning: SandboxRow[] } {
|
|
76
|
+
const raw = runSafeEngine(engine, 'docker', [
|
|
77
|
+
'ps',
|
|
78
|
+
'-a',
|
|
79
|
+
'--filter',
|
|
80
|
+
`label=${label}`,
|
|
81
|
+
'--format',
|
|
82
|
+
containerListFormat()
|
|
83
|
+
]);
|
|
84
|
+
return sortAndIndexSandboxRows(parseSandboxRows(raw, branchKey));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true iff `arg` is a syntactically valid task short reference ('#N').
|
|
89
|
+
* Zero IO. Callers MUST use this as the gate before constructing any context
|
|
90
|
+
* for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
|
|
91
|
+
* '#1.5', '#') never trigger sandbox list IO.
|
|
92
|
+
*/
|
|
93
|
+
export function isTaskShortRef(arg: string): boolean {
|
|
94
|
+
return /^#\d+$/.test(arg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a task short reference ('#N') to a branch name.
|
|
99
|
+
*
|
|
100
|
+
* Current implementation: treats the digits as a 1-based index into the
|
|
101
|
+
* supplied running-sandbox list (ls view order). This is the *only*
|
|
102
|
+
* resolution path until the global task-short-id registry lands in a
|
|
103
|
+
* follow-up task; do NOT read task.md or scan .agents/workspace/ from this
|
|
104
|
+
* helper here.
|
|
105
|
+
*
|
|
106
|
+
* Precondition: callers MUST gate on isTaskShortRef(arg) === true before
|
|
107
|
+
* constructing ctx and calling this function. Throws when arg is a valid
|
|
108
|
+
* short ref but cannot be resolved (out of range, no running sandboxes,
|
|
109
|
+
* etc.); the caller surfaces the error to the user.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveTaskShortRef(
|
|
112
|
+
arg: string,
|
|
113
|
+
ctx: { running: SandboxRow[] }
|
|
114
|
+
): string {
|
|
115
|
+
const n = Number(arg.slice(1));
|
|
116
|
+
if (n < 1) {
|
|
117
|
+
throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
|
|
118
|
+
}
|
|
119
|
+
const { running } = ctx;
|
|
120
|
+
if (running.length === 0) {
|
|
121
|
+
throw new Error(`No running sandbox to reference with '${arg}'`);
|
|
122
|
+
}
|
|
123
|
+
if (n > running.length) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`No running sandbox at index '${arg}' (only ${running.length} running)`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const row = running[n - 1]!;
|
|
129
|
+
if (!row.branch) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return row.branch;
|
|
135
|
+
}
|
|
@@ -5,51 +5,36 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { loadConfig } from '../config.ts';
|
|
6
6
|
import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
7
7
|
import { detectEngine } from '../engine.ts';
|
|
8
|
-
import { runSafeEngine } from '../shell.ts';
|
|
9
8
|
import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
|
|
9
|
+
import { fetchSandboxRows } from './list-running.ts';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export { containerListFormat, parseLabels } from './list-running.ts';
|
|
12
|
+
|
|
13
|
+
const USAGE = `Usage: ai sandbox ls
|
|
14
|
+
|
|
15
|
+
Lists all containers for the current project. The leftmost '#' column
|
|
16
|
+
numbers running sandboxes; use it as "ai sandbox exec '#N'" to enter one.
|
|
17
|
+
Quote '#N' to avoid shell '#' comment handling.`;
|
|
18
|
+
|
|
19
|
+
const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
13
20
|
|
|
14
21
|
type ContainerTableRow = {
|
|
22
|
+
index: string;
|
|
15
23
|
name: string;
|
|
16
24
|
status: string;
|
|
17
25
|
branch: string;
|
|
18
26
|
};
|
|
19
27
|
|
|
20
|
-
// Exported to lock the docker/podman-compatible format in unit tests.
|
|
21
|
-
export function containerListFormat(): string {
|
|
22
|
-
return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function parseLabels(csv: string): Record<string, string> {
|
|
26
|
-
if (!csv) {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const labels: Record<string, string> = {};
|
|
31
|
-
for (const pair of csv.split(',')) {
|
|
32
|
-
if (!pair) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const eq = pair.indexOf('=');
|
|
36
|
-
if (eq < 0) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
labels[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
40
|
-
}
|
|
41
|
-
return labels;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
28
|
export function formatContainerTable(rows: ContainerTableRow[]): string[] {
|
|
45
|
-
const columns = rows.map((row) => [row.name, row.status, row.branch] as const);
|
|
29
|
+
const columns = rows.map((row) => [row.index, row.name, row.status, row.branch] as const);
|
|
46
30
|
const widths = [
|
|
47
|
-
Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.
|
|
48
|
-
Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.
|
|
49
|
-
Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.
|
|
31
|
+
Math.max(CONTAINER_TABLE_HEADERS[0].length, ...rows.map((row) => row.index.length)),
|
|
32
|
+
Math.max(CONTAINER_TABLE_HEADERS[1].length, ...rows.map((row) => row.name.length)),
|
|
33
|
+
Math.max(CONTAINER_TABLE_HEADERS[2].length, ...rows.map((row) => row.status.length)),
|
|
34
|
+
Math.max(CONTAINER_TABLE_HEADERS[3].length, ...rows.map((row) => row.branch.length))
|
|
50
35
|
] as const;
|
|
51
|
-
const renderRow = (values: readonly [string, string, string]): string =>
|
|
52
|
-
`${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2]}`.trimEnd();
|
|
36
|
+
const renderRow = (values: readonly [string, string, string, string]): string =>
|
|
37
|
+
`${values[0].padEnd(widths[0])} ${values[1].padEnd(widths[1])} ${values[2].padEnd(widths[2])} ${values[3]}`.trimEnd();
|
|
53
38
|
|
|
54
39
|
return [
|
|
55
40
|
renderRow(CONTAINER_TABLE_HEADERS),
|
|
@@ -75,28 +60,22 @@ export function ls(args: string[] = []): void {
|
|
|
75
60
|
const engine = detectEngine(config);
|
|
76
61
|
const tools = resolveTools(config);
|
|
77
62
|
const label = sandboxLabel(config);
|
|
78
|
-
const
|
|
79
|
-
'ps',
|
|
80
|
-
'-a',
|
|
81
|
-
'--filter',
|
|
82
|
-
`label=${label}`,
|
|
83
|
-
'--format',
|
|
84
|
-
containerListFormat()
|
|
85
|
-
]);
|
|
63
|
+
const { running, nonRunning } = fetchSandboxRows(engine, label, sandboxBranchLabel(config));
|
|
86
64
|
|
|
87
65
|
p.intro(pc.cyan(`Sandbox status for ${config.project}`));
|
|
88
66
|
|
|
89
67
|
p.log.step('Containers');
|
|
90
|
-
|
|
68
|
+
const ordered = [...running, ...nonRunning];
|
|
69
|
+
if (ordered.length === 0) {
|
|
91
70
|
p.log.warn(' No sandbox containers');
|
|
92
71
|
} else {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
for (const line of formatContainerTable(
|
|
72
|
+
const tableRows: ContainerTableRow[] = ordered.map((row) => ({
|
|
73
|
+
index: row.index === null ? '' : String(row.index),
|
|
74
|
+
name: row.name,
|
|
75
|
+
status: row.status,
|
|
76
|
+
branch: row.branch
|
|
77
|
+
}));
|
|
78
|
+
for (const line of formatContainerTable(tableRows)) {
|
|
100
79
|
process.stdout.write(` ${line}\n`);
|
|
101
80
|
}
|
|
102
81
|
}
|