@fitlab-ai/agent-infra 0.7.1 → 0.7.2
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/bin/cli.ts +11 -0
- package/dist/bin/cli.js +12 -0
- package/dist/lib/sandbox/commands/create.js +10 -2
- package/dist/lib/sandbox/commands/enter.js +8 -7
- package/dist/lib/sandbox/commands/list-running.js +21 -32
- package/dist/lib/sandbox/commands/ls.js +20 -22
- package/dist/lib/sandbox/index.js +7 -3
- package/dist/lib/sandbox/task-resolver.js +1 -1
- package/dist/lib/sandbox/tools.js +1 -1
- package/dist/lib/table.js +29 -0
- package/dist/lib/task/commands/ls.js +122 -0
- package/dist/lib/task/commands/show.js +135 -0
- package/dist/lib/task/frontmatter.js +32 -0
- package/dist/lib/task/index.js +41 -0
- package/dist/lib/task/short-id.js +80 -0
- package/lib/sandbox/commands/create.ts +11 -2
- package/lib/sandbox/commands/enter.ts +8 -7
- package/lib/sandbox/commands/list-running.ts +23 -37
- package/lib/sandbox/commands/ls.ts +25 -25
- package/lib/sandbox/index.ts +7 -3
- package/lib/sandbox/task-resolver.ts +1 -1
- package/lib/sandbox/tools.ts +1 -1
- package/lib/table.ts +32 -0
- package/lib/task/commands/ls.ts +138 -0
- package/lib/task/commands/show.ts +139 -0
- package/lib/task/frontmatter.ts +30 -0
- package/lib/task/index.ts +44 -0
- package/lib/task/short-id.ts +97 -0
- package/package.json +1 -1
- package/templates/.agents/hooks/auto-resume.sh +87 -0
- 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/milestone-inference.github.en.md +4 -1
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -1
- package/templates/.agents/rules/next-step-output.en.md +59 -0
- package/templates/.agents/rules/next-step-output.zh-CN.md +59 -0
- package/templates/.agents/rules/task-short-id.en.md +54 -62
- package/templates/.agents/rules/task-short-id.zh-CN.md +35 -54
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +17 -0
- package/templates/.agents/scripts/task-short-id.js +32 -189
- package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -12
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -12
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +12 -10
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +12 -10
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -6
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -6
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/code-task/SKILL.en.md +10 -6
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/code-task/config/verify.en.json +2 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +10 -5
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +10 -5
- package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/reference/report-template.en.md +8 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +8 -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 +9 -9
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +9 -9
- package/templates/.agents/skills/complete-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/create-pr/config/verify.json +2 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
- package/templates/.agents/skills/create-task/SKILL.en.md +17 -17
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +17 -17
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +7 -7
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/plan-task/SKILL.en.md +10 -12
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -12
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +3 -2
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +3 -2
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +15 -15
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +15 -15
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +7 -1
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +3 -2
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +9 -9
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +9 -9
- package/templates/.agents/skills/review-code/reference/report-template.en.md +7 -1
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +7 -1
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +3 -2
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +3 -2
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +15 -15
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +15 -15
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -1
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -1
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +2 -0
- package/templates/.agents/templates/task.en.md +0 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +1 -1
- package/templates/.agents/workflows/feature-development.en.yaml +1 -1
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +1 -1
- package/templates/.agents/workflows/refactoring.en.yaml +1 -1
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +1 -1
- package/templates/.claude/settings.json +11 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const REGISTRY_NAME = '.short-ids.json';
|
|
4
|
+
function normalizeShortIdInput(input, opts) {
|
|
5
|
+
const L = opts.shortIdLength;
|
|
6
|
+
const m = /^#?(\d+)$/.exec(input);
|
|
7
|
+
if (!m) {
|
|
8
|
+
return { kind: 'pass', value: input };
|
|
9
|
+
}
|
|
10
|
+
const n = Number(m[1]);
|
|
11
|
+
if (n === 0) {
|
|
12
|
+
return {
|
|
13
|
+
kind: 'error',
|
|
14
|
+
message: `short id '${input}' is invalid (#${'0'.repeat(L)} is reserved)`
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const max = Math.pow(10, L) - 1;
|
|
18
|
+
if (n > max) {
|
|
19
|
+
return {
|
|
20
|
+
kind: 'error',
|
|
21
|
+
message: `short id ${n} exceeds shortIdLength=${L} capacity (max=${max}); archive tasks or raise task.shortIdLength in .agents/.airc.json`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { kind: 'shortId', value: `#${String(n).padStart(L, '0')}` };
|
|
25
|
+
}
|
|
26
|
+
function readRegistry(repoRoot) {
|
|
27
|
+
const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
|
|
28
|
+
if (!fs.existsSync(registryPath))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
32
|
+
const data = JSON.parse(raw);
|
|
33
|
+
if (!data || typeof data !== 'object' || !data.ids)
|
|
34
|
+
return null;
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function readBranchFromTaskMd(repoRoot, taskId) {
|
|
42
|
+
const taskMdPath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
|
|
43
|
+
if (!fs.existsSync(taskMdPath))
|
|
44
|
+
return null;
|
|
45
|
+
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
46
|
+
const m = content.match(/^branch:\s*(.+)$/m);
|
|
47
|
+
if (!m || !m[1])
|
|
48
|
+
return null;
|
|
49
|
+
return m[1].trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
50
|
+
}
|
|
51
|
+
function loadShortIdByTaskId(repoRoot) {
|
|
52
|
+
const registry = readRegistry(repoRoot);
|
|
53
|
+
const map = new Map();
|
|
54
|
+
if (!registry)
|
|
55
|
+
return map;
|
|
56
|
+
for (const [key, taskId] of Object.entries(registry.ids)) {
|
|
57
|
+
map.set(taskId, `#${key}`);
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
function lookupShortIdByBranch(branch, repoRoot, _opts) {
|
|
62
|
+
const registry = readRegistry(repoRoot);
|
|
63
|
+
if (!registry)
|
|
64
|
+
return null;
|
|
65
|
+
const matches = [];
|
|
66
|
+
for (const [key, taskId] of Object.entries(registry.ids)) {
|
|
67
|
+
const taskBranch = readBranchFromTaskMd(repoRoot, taskId);
|
|
68
|
+
if (taskBranch && taskBranch === branch) {
|
|
69
|
+
matches.push(`#${key}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (matches.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
if (matches.length > 1) {
|
|
75
|
+
process.stderr.write(`Warning: branch '${branch}' is bound to multiple active tasks: ${matches.join(', ')}; using ${matches[0]}\n`);
|
|
76
|
+
}
|
|
77
|
+
return matches[0];
|
|
78
|
+
}
|
|
79
|
+
export { normalizeShortIdInput, lookupShortIdByBranch, loadShortIdByTaskId };
|
|
80
|
+
//# sourceMappingURL=short-id.js.map
|
|
@@ -868,13 +868,22 @@ export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: strin
|
|
|
868
868
|
}
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
+
const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
|
|
872
|
+
['model', 'string'],
|
|
873
|
+
['model_reasoning_effort', 'string'],
|
|
874
|
+
['model_auto_compact_token_limit', 'number']
|
|
875
|
+
];
|
|
876
|
+
|
|
871
877
|
let changed = false;
|
|
872
|
-
for (const key of
|
|
878
|
+
for (const [key, type] of inheritSpecs) {
|
|
873
879
|
if (Object.hasOwn(sandboxParsed, key)) {
|
|
874
880
|
continue;
|
|
875
881
|
}
|
|
876
882
|
const value = hostParsed[key];
|
|
877
|
-
if (typeof value !== 'string' || value === '') {
|
|
883
|
+
if (type === 'string' && (typeof value !== 'string' || value === '')) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
|
|
878
887
|
continue;
|
|
879
888
|
}
|
|
880
889
|
sandboxParsed[key] = value;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from '../config.ts';
|
|
2
|
-
import { assertValidBranchName, containerNameCandidates
|
|
2
|
+
import { assertValidBranchName, containerNameCandidates } from '../constants.ts';
|
|
3
3
|
import { detectEngine } from '../engine.ts';
|
|
4
4
|
import {
|
|
5
5
|
formatCredentialWarnings,
|
|
@@ -13,12 +13,14 @@ 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 {
|
|
16
|
+
import { isTaskShortRef, resolveTaskShortRef } from './list-running.ts';
|
|
17
17
|
|
|
18
|
-
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | '#N'> [cmd...]
|
|
18
|
+
const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
19
19
|
|
|
20
|
-
'#N'
|
|
21
|
-
|
|
20
|
+
N (bare) and '#N' both reference the same active task short id from
|
|
21
|
+
.agents/workspace/active/.short-ids.json. They resolve only via that
|
|
22
|
+
registry — they do not reference a container's row position in
|
|
23
|
+
'ai sandbox ls' output.`;
|
|
22
24
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
23
25
|
|
|
24
26
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
@@ -122,8 +124,7 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
122
124
|
const [firstArg = '', ...cmd] = args;
|
|
123
125
|
let branch: string;
|
|
124
126
|
if (isTaskShortRef(firstArg)) {
|
|
125
|
-
|
|
126
|
-
branch = resolveTaskShortRef(firstArg, { running, repoRoot: config.repoRoot });
|
|
127
|
+
branch = resolveTaskShortRef(firstArg, { repoRoot: config.repoRoot });
|
|
127
128
|
} else {
|
|
128
129
|
branch = resolveTaskBranch(firstArg, config.repoRoot);
|
|
129
130
|
}
|
|
@@ -88,13 +88,14 @@ export function fetchSandboxRows(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Returns true iff `arg` is a syntactically valid task short reference
|
|
91
|
+
* Returns true iff `arg` is a syntactically valid task short reference.
|
|
92
|
+
* Accepts both bare numeric ('11') and '#'-prefixed ('#11') forms.
|
|
92
93
|
* Zero IO. Callers MUST use this as the gate before constructing any context
|
|
93
94
|
* for resolveTaskShortRef — that way non-matching arguments (e.g. '#abc',
|
|
94
95
|
* '#1.5', '#') never trigger sandbox list IO.
|
|
95
96
|
*/
|
|
96
97
|
export function isTaskShortRef(arg: string): boolean {
|
|
97
|
-
return
|
|
98
|
+
return /^#?\d+$/.test(arg);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
type RegistryLookup =
|
|
@@ -113,6 +114,9 @@ type RegistryLookup =
|
|
|
113
114
|
function tryResolveFromRegistry(arg: string, repoRoot: string): RegistryLookup {
|
|
114
115
|
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
115
116
|
if (!fs.existsSync(scriptPath)) return { status: 'miss' };
|
|
117
|
+
// Strip leading '#' when forwarding bare-numeric input through the script's CLI.
|
|
118
|
+
// (Script accepts both forms, but this avoids shell quoting confusion in error
|
|
119
|
+
// messages echoed back to the user.)
|
|
116
120
|
const result = spawnSync('node', [scriptPath, 'resolve', arg], { encoding: 'utf8', cwd: repoRoot });
|
|
117
121
|
if (result.status !== 0) return { status: 'miss' };
|
|
118
122
|
const taskId = (result.stdout || '').trim();
|
|
@@ -142,47 +146,29 @@ function tryResolveFromRegistry(arg: string, repoRoot: string): RegistryLookup {
|
|
|
142
146
|
);
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
function resolveByRunningIndex(arg: string, running: SandboxRow[]): string {
|
|
146
|
-
const n = Number(arg.slice(1));
|
|
147
|
-
if (n < 1) {
|
|
148
|
-
throw new Error(`Invalid sandbox index '${arg}': must be >= 1`);
|
|
149
|
-
}
|
|
150
|
-
if (running.length === 0) {
|
|
151
|
-
throw new Error(`No running sandbox to reference with '${arg}'`);
|
|
152
|
-
}
|
|
153
|
-
if (n > running.length) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`No running sandbox at index '${arg}' (only ${running.length} running)`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
const row = running[n - 1]!;
|
|
159
|
-
if (!row.branch) {
|
|
160
|
-
throw new Error(
|
|
161
|
-
`Cannot resolve branch for sandbox '${arg}' (container '${row.name}' missing branch label)`
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
return row.branch;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
149
|
/**
|
|
168
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a task short reference (bare 'N' or '#N') to a branch name for the
|
|
152
|
+
* sandbox entrypoint.
|
|
169
153
|
*
|
|
170
|
-
* Resolution
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
154
|
+
* Resolution: registry-only. Look up the short id in the global task-short-id
|
|
155
|
+
* registry under repoRoot; if hit, read the branch from the matching task.md.
|
|
156
|
+
* On miss (registry empty or short id absent), throw with an actionable
|
|
157
|
+
* message instead of falling back to a container's row position in
|
|
158
|
+
* 'ai sandbox ls' output — that fallback would make the same syntax mean
|
|
159
|
+
* different things depending on `docker ps` state.
|
|
175
160
|
*
|
|
176
161
|
* Precondition: callers MUST gate on isTaskShortRef(arg) === true.
|
|
177
162
|
*/
|
|
178
163
|
export function resolveTaskShortRef(
|
|
179
164
|
arg: string,
|
|
180
|
-
ctx: {
|
|
165
|
+
ctx: { repoRoot: string }
|
|
181
166
|
): string {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
const lookup = tryResolveFromRegistry(arg, ctx.repoRoot);
|
|
168
|
+
if (lookup.status === 'hit') return lookup.branch;
|
|
169
|
+
throw new Error(
|
|
170
|
+
`short ref '${arg}' is not in the active task registry. ` +
|
|
171
|
+
`'#N' and bare N resolve only via the registry (not by row position in 'ai sandbox ls'); ` +
|
|
172
|
+
`use a task short id (e.g. 'ai sandbox exec 11'), a TASK-id, or a branch name.`
|
|
173
|
+
);
|
|
188
174
|
}
|
|
@@ -6,40 +6,35 @@ import { loadConfig } from '../config.ts';
|
|
|
6
6
|
import { sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
7
7
|
import { detectEngine } from '../engine.ts';
|
|
8
8
|
import { resolveTools, toolProjectDirCandidates } from '../tools.ts';
|
|
9
|
+
import { formatTable } from '../../table.ts';
|
|
10
|
+
import { lookupShortIdByBranch } from '../../task/short-id.ts';
|
|
9
11
|
import { fetchSandboxRows } from './list-running.ts';
|
|
10
12
|
|
|
11
13
|
export { containerListFormat, parseLabels } from './list-running.ts';
|
|
12
14
|
|
|
13
15
|
const USAGE = `Usage: ai sandbox ls
|
|
14
16
|
|
|
15
|
-
Lists all containers for the current project. The
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
Lists all containers for the current project. The '#' column is a
|
|
18
|
+
display-only row number; the 'SHORT' column shows the active task short
|
|
19
|
+
id bound to each container's branch (via
|
|
20
|
+
.agents/workspace/active/.short-ids.json), or '-' if no active task is
|
|
21
|
+
bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').`;
|
|
18
22
|
|
|
19
|
-
const CONTAINER_TABLE_HEADERS = ['#', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
23
|
+
const CONTAINER_TABLE_HEADERS = ['#', 'SHORT', 'NAMES', 'STATUS', 'BRANCH'] as const;
|
|
20
24
|
|
|
21
25
|
type ContainerTableRow = {
|
|
22
|
-
|
|
26
|
+
row: string;
|
|
27
|
+
shortId: string;
|
|
23
28
|
name: string;
|
|
24
29
|
status: string;
|
|
25
30
|
branch: string;
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
export function formatContainerTable(rows: ContainerTableRow[]): string[] {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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))
|
|
35
|
-
] as const;
|
|
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();
|
|
38
|
-
|
|
39
|
-
return [
|
|
40
|
-
renderRow(CONTAINER_TABLE_HEADERS),
|
|
41
|
-
...columns.map((column) => renderRow(column))
|
|
42
|
-
];
|
|
34
|
+
return formatTable(
|
|
35
|
+
CONTAINER_TABLE_HEADERS,
|
|
36
|
+
rows.map((r) => [r.row, r.shortId, r.name, r.status, r.branch])
|
|
37
|
+
);
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
function listChildren(dir: string): string[] {
|
|
@@ -69,15 +64,20 @@ export function ls(args: string[] = []): void {
|
|
|
69
64
|
if (ordered.length === 0) {
|
|
70
65
|
p.log.warn(' No sandbox containers');
|
|
71
66
|
} else {
|
|
72
|
-
const tableRows: ContainerTableRow[] = ordered.map((
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const tableRows: ContainerTableRow[] = ordered.map((container, i) => {
|
|
68
|
+
const shortId = container.branch ? lookupShortIdByBranch(container.branch, config.repoRoot) : null;
|
|
69
|
+
return {
|
|
70
|
+
row: String(i + 1),
|
|
71
|
+
shortId: shortId ?? '-',
|
|
72
|
+
name: container.name,
|
|
73
|
+
status: container.status,
|
|
74
|
+
branch: container.branch
|
|
75
|
+
};
|
|
76
|
+
});
|
|
78
77
|
for (const line of formatContainerTable(tableRows)) {
|
|
79
78
|
process.stdout.write(` ${line}\n`);
|
|
80
79
|
}
|
|
80
|
+
process.stdout.write(` Total: ${ordered.length} containers\n`);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
p.log.step('Worktrees');
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -2,9 +2,13 @@ const USAGE = `Usage: ai sandbox <command> [options]
|
|
|
2
2
|
|
|
3
3
|
Commands:
|
|
4
4
|
create <branch> [base] Create a sandbox (VM + image + worktree + container)
|
|
5
|
-
exec <branch | '#N'> [cmd...]
|
|
6
|
-
Enter sandbox or run a command (
|
|
7
|
-
|
|
5
|
+
exec <branch | TASK-id | N | '#N'> [cmd...]
|
|
6
|
+
Enter sandbox or run a command. N (bare) is the
|
|
7
|
+
recommended form for task short ids (e.g.
|
|
8
|
+
'ai sandbox exec 11'); '#N' is also accepted.
|
|
9
|
+
ls List sandboxes for the current project (the '#'
|
|
10
|
+
column is a display-only row number; the 'SHORT'
|
|
11
|
+
column shows the active task short id, '-' if none)
|
|
8
12
|
prune [--dry-run] Remove orphaned per-branch state dirs
|
|
9
13
|
rebuild [--quiet] [--refresh]
|
|
10
14
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
|
-
const SHORT_ID_RE =
|
|
6
|
+
const SHORT_ID_RE = /^#?\d+$/;
|
|
7
7
|
const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
|
|
8
8
|
|
|
9
9
|
function resolveShortIdStrict(arg: string, repoRoot: string): string {
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -35,7 +35,7 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
35
35
|
'claude-code': {
|
|
36
36
|
id: 'claude-code',
|
|
37
37
|
name: 'Claude Code',
|
|
38
|
-
install: { type: 'npm', cmd: '@anthropic-ai/claude-code@
|
|
38
|
+
install: { type: 'npm', cmd: '@anthropic-ai/claude-code@latest' },
|
|
39
39
|
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
|
|
40
40
|
containerMount: '/home/devuser/.claude',
|
|
41
41
|
versionCmd: 'claude --version',
|
package/lib/table.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function formatTable(
|
|
2
|
+
headers: readonly string[],
|
|
3
|
+
rows: readonly (readonly string[])[]
|
|
4
|
+
): string[] {
|
|
5
|
+
const columnCount = headers.length;
|
|
6
|
+
const widths = headers.map((header, i) => {
|
|
7
|
+
const headerLen = header.length;
|
|
8
|
+
let max = headerLen;
|
|
9
|
+
for (const row of rows) {
|
|
10
|
+
const cell = row[i] ?? '';
|
|
11
|
+
if (cell.length > max) max = cell.length;
|
|
12
|
+
}
|
|
13
|
+
return max;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const renderRow = (values: readonly string[]): string => {
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
for (let i = 0; i < columnCount; i += 1) {
|
|
19
|
+
const cell = values[i] ?? '';
|
|
20
|
+
if (i === columnCount - 1) {
|
|
21
|
+
parts.push(cell);
|
|
22
|
+
} else {
|
|
23
|
+
parts.push(cell.padEnd(widths[i]!));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return parts.join(' ').trimEnd();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return [renderRow(headers), ...rows.map((row) => renderRow(row))];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { formatTable };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { formatTable } from '../../table.ts';
|
|
5
|
+
import { parseTaskFrontmatter, extractTitle } from '../frontmatter.ts';
|
|
6
|
+
import { loadShortIdByTaskId } from '../short-id.ts';
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: ai task ls [--all | --blocked | --completed]
|
|
9
|
+
|
|
10
|
+
Lists tasks under .agents/workspace/. Defaults to active tasks only.
|
|
11
|
+
--all Include active + blocked + completed (excludes archive)
|
|
12
|
+
--blocked Only blocked tasks
|
|
13
|
+
--completed Only completed tasks
|
|
14
|
+
|
|
15
|
+
Columns: # (display-only row number) / SHORT (task short id, usable as an argument) / type / status / current_step / branch / title
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
19
|
+
const TABLE_HEADERS = ['#', 'SHORT', 'TYPE', 'STATUS', 'STEP', 'BRANCH', 'TITLE'] as const;
|
|
20
|
+
|
|
21
|
+
type Selection = ('active' | 'blocked' | 'completed')[];
|
|
22
|
+
|
|
23
|
+
function detectRepoRoot(): string {
|
|
24
|
+
try {
|
|
25
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
28
|
+
}).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error('ai task: current directory is not inside a git repository');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ParseResult =
|
|
35
|
+
| { ok: true; selection: Selection }
|
|
36
|
+
| { ok: false; message: string };
|
|
37
|
+
|
|
38
|
+
function parseSelection(args: string[]): ParseResult {
|
|
39
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
40
|
+
if (positional.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `ai task ls: unexpected positional argument(s): ${positional.join(' ')}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const flags = args.filter((a) => a.startsWith('--'));
|
|
47
|
+
if (flags.length === 0) return { ok: true, selection: ['active'] };
|
|
48
|
+
if (flags.length > 1) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: 'ai task ls: pass at most one of --all / --blocked / --completed'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
switch (flags[0]) {
|
|
55
|
+
case '--all':
|
|
56
|
+
return { ok: true, selection: ['active', 'blocked', 'completed'] };
|
|
57
|
+
case '--blocked':
|
|
58
|
+
return { ok: true, selection: ['blocked'] };
|
|
59
|
+
case '--completed':
|
|
60
|
+
return { ok: true, selection: ['completed'] };
|
|
61
|
+
default:
|
|
62
|
+
return { ok: false, message: `ai task ls: unknown flag: ${flags[0]}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type TaskRow = {
|
|
67
|
+
shortId: string;
|
|
68
|
+
type: string;
|
|
69
|
+
status: string;
|
|
70
|
+
step: string;
|
|
71
|
+
branch: string;
|
|
72
|
+
title: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function collectTasks(repoRoot: string, state: 'active' | 'blocked' | 'completed'): TaskRow[] {
|
|
76
|
+
const dir = path.join(repoRoot, '.agents', 'workspace', state);
|
|
77
|
+
if (!fs.existsSync(dir)) return [];
|
|
78
|
+
// Short ids live only in the registry and only for active tasks; archived
|
|
79
|
+
// (blocked/completed) tasks have released their short id and render '-'.
|
|
80
|
+
const shortIdByTaskId = state === 'active' ? loadShortIdByTaskId(repoRoot) : new Map<string, string>();
|
|
81
|
+
const rows: TaskRow[] = [];
|
|
82
|
+
for (const entry of fs.readdirSync(dir).sort()) {
|
|
83
|
+
if (!TASK_ID_RE.test(entry)) continue;
|
|
84
|
+
const taskMdPath = path.join(dir, entry, 'task.md');
|
|
85
|
+
if (!fs.existsSync(taskMdPath)) continue;
|
|
86
|
+
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
87
|
+
const fm = parseTaskFrontmatter(content);
|
|
88
|
+
const title = extractTitle(content);
|
|
89
|
+
const shortId = shortIdByTaskId.get(entry) ?? '-';
|
|
90
|
+
rows.push({
|
|
91
|
+
shortId,
|
|
92
|
+
type: fm.type ?? '-',
|
|
93
|
+
status: fm.status ?? state,
|
|
94
|
+
step: fm.current_step ?? '-',
|
|
95
|
+
branch: fm.branch ?? '-',
|
|
96
|
+
title: title || fm.id || entry
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return rows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ls(args: string[] = []): void {
|
|
103
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
104
|
+
process.stdout.write(USAGE);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const result = parseSelection(args);
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
process.stderr.write(`${result.message}\n`);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { selection } = result;
|
|
114
|
+
const repoRoot = detectRepoRoot();
|
|
115
|
+
const rows: TaskRow[] = [];
|
|
116
|
+
for (const state of selection) {
|
|
117
|
+
rows.push(...collectTasks(repoRoot, state));
|
|
118
|
+
}
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
process.stdout.write(`No tasks under .agents/workspace/${selection.join('|')}\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const tableRows = rows.map((r, i) => [
|
|
124
|
+
String(i + 1),
|
|
125
|
+
r.shortId,
|
|
126
|
+
r.type,
|
|
127
|
+
r.status,
|
|
128
|
+
r.step,
|
|
129
|
+
r.branch,
|
|
130
|
+
r.title
|
|
131
|
+
]);
|
|
132
|
+
for (const line of formatTable(TABLE_HEADERS, tableRows)) {
|
|
133
|
+
process.stdout.write(`${line}\n`);
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(`Total: ${rows.length} tasks\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ls };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { normalizeShortIdInput } from '../short-id.ts';
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage: ai task show <N | #N | TASK-id>
|
|
7
|
+
|
|
8
|
+
Prints the task.md content for the matching task.
|
|
9
|
+
N (bare numeric) Recommended; resolves the active short id via the registry.
|
|
10
|
+
'#N' Compatibility form for old commands.
|
|
11
|
+
TASK-YYYYMMDD-HHMMSS Locates a task in active / blocked / completed / archive.
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
15
|
+
// Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
|
|
16
|
+
// Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
|
|
17
|
+
const FLAT_WORKSPACE_DIRS = ['active', 'blocked', 'completed'] as const;
|
|
18
|
+
|
|
19
|
+
function detectRepoRoot(): string {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error('ai task: current directory is not inside a git repository');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readShortIdLength(repoRoot: string): number {
|
|
31
|
+
try {
|
|
32
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
33
|
+
const v = cfg?.task?.shortIdLength;
|
|
34
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 1) return v;
|
|
35
|
+
} catch {
|
|
36
|
+
// fall through to default
|
|
37
|
+
}
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveShortIdToTaskId(arg: string, repoRoot: string): string {
|
|
42
|
+
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
43
|
+
if (!fs.existsSync(scriptPath)) {
|
|
44
|
+
throw new Error(`task-short-id.js not found at ${scriptPath}`);
|
|
45
|
+
}
|
|
46
|
+
const result = spawnSync('node', [scriptPath, 'resolve', arg], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
cwd: repoRoot
|
|
49
|
+
});
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
throw new Error((result.stderr || '').trim() || `failed to resolve '${arg}'`);
|
|
52
|
+
}
|
|
53
|
+
return result.stdout.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listSortedNumeric(dir: string, width: number): string[] {
|
|
57
|
+
if (!fs.existsSync(dir)) return [];
|
|
58
|
+
const pattern = new RegExp(`^\\d{${width}}$`);
|
|
59
|
+
return fs
|
|
60
|
+
.readdirSync(dir)
|
|
61
|
+
.filter((entry) => pattern.test(entry))
|
|
62
|
+
.sort()
|
|
63
|
+
.reverse();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findInArchive(repoRoot: string, taskId: string): string | null {
|
|
67
|
+
// archive-tasks SKILL writes to .agents/workspace/archive/YYYY/MM/DD/{taskId}/task.md
|
|
68
|
+
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
69
|
+
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
70
|
+
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
71
|
+
const archiveDir = path.join(repoRoot, '.agents', 'workspace', 'archive');
|
|
72
|
+
for (const year of listSortedNumeric(archiveDir, 4)) {
|
|
73
|
+
const yearDir = path.join(archiveDir, year);
|
|
74
|
+
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
75
|
+
const monthDir = path.join(yearDir, month);
|
|
76
|
+
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
77
|
+
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
78
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findTaskMd(repoRoot: string, taskId: string): string | null {
|
|
86
|
+
for (const sub of FLAT_WORKSPACE_DIRS) {
|
|
87
|
+
const candidate = path.join(repoRoot, '.agents', 'workspace', sub, taskId, 'task.md');
|
|
88
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
89
|
+
}
|
|
90
|
+
return findInArchive(repoRoot, taskId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function show(args: string[] = []): void {
|
|
94
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
95
|
+
process.stdout.write(USAGE);
|
|
96
|
+
if (args.length === 0) process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const repoRoot = detectRepoRoot();
|
|
100
|
+
const arg = args[0]!;
|
|
101
|
+
let taskId: string;
|
|
102
|
+
if (TASK_ID_RE.test(arg)) {
|
|
103
|
+
taskId = arg;
|
|
104
|
+
} else {
|
|
105
|
+
const shortIdLength = readShortIdLength(repoRoot);
|
|
106
|
+
const normalized = normalizeShortIdInput(arg, { shortIdLength });
|
|
107
|
+
if (normalized.kind === 'error') {
|
|
108
|
+
process.stderr.write(`ai task show: ${normalized.message}\n`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (normalized.kind === 'pass') {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`ai task show: '${arg}' is not a valid short id or TASK-id; ` +
|
|
115
|
+
`expected bare digits, '#N', or 'TASK-YYYYMMDD-HHMMSS'\n`
|
|
116
|
+
);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
taskId = resolveShortIdToTaskId(normalized.value, repoRoot);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
process.stderr.write(`ai task show: ${(e as Error).message}\n`);
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const taskMdPath = findTaskMd(repoRoot, taskId);
|
|
129
|
+
if (!taskMdPath) {
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`ai task show: task ${taskId} not found in active / blocked / completed / archive\n`
|
|
132
|
+
);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write(fs.readFileSync(taskMdPath, 'utf8'));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { show };
|