@zenera/cli 1.1.24 → 1.1.25
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 +53 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/commands/inspect.d.ts +47 -1
- package/dist/commands/inspect.js +167 -18
- package/dist/commands/memory.js +128 -29
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +96 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +76 -14
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +1 -0
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +15 -1
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +338 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +125 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +41 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.js +53 -2
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +31 -102
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/memory-instructions.md +31 -32
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +2 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface Sink {
|
|
2
|
+
answer(text: string): void;
|
|
3
|
+
narrate(line: string): void;
|
|
4
|
+
warn(line: string): void;
|
|
5
|
+
/** Kept by the log, never shown: the step-by-step nobody reads while it runs. */
|
|
6
|
+
detail?(line: string): void;
|
|
7
|
+
/** A transient last row, replaced each time and never kept. '' removes it. */
|
|
8
|
+
status?(line: string): void;
|
|
9
|
+
/** Called once when the run ends, so a repainting sink can clear itself. */
|
|
10
|
+
close?(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare const terminalSink: Sink;
|
|
13
|
+
/** Rows of narration the window keeps on screen. */
|
|
14
|
+
export declare const WINDOW_ROWS = 8;
|
|
15
|
+
/** How often the border is redrawn — the wave's frame, not the clock's. */
|
|
16
|
+
export declare const FRAME_MS = 80;
|
|
17
|
+
/** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
|
|
18
|
+
export declare const WAVE_STEP = 3;
|
|
19
|
+
/**
|
|
20
|
+
* How brightly one cell of the border is lit, given where the crest has got to.
|
|
21
|
+
*
|
|
22
|
+
* `position` counts clockwise from the top left corner, so one index walks the
|
|
23
|
+
* whole ring and the wave rounds the corners without a seam. Everything the
|
|
24
|
+
* tail has passed sits at the dimmest tone, which is the border's resting
|
|
25
|
+
* colour: the ring is always drawn, only the crest moves.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toneAt(position: number, head: number, span: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* The bottom rule as cells, with the status label set into it near the left.
|
|
30
|
+
*
|
|
31
|
+
* Waiting belongs to the frame rather than to the narration: a row of its own
|
|
32
|
+
* costs a step of what the agent is doing every time, and it settles just
|
|
33
|
+
* under the last line, which is where the eye already is. Near the left, where
|
|
34
|
+
* reading starts, and not so near that it displaces the corner.
|
|
35
|
+
*/
|
|
36
|
+
export declare function footerCells(label: string, width: number): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Narration held to a fixed number of rows that rewrite themselves in place.
|
|
39
|
+
*
|
|
40
|
+
* A long run is hundreds of tool calls, and printing each as its own line
|
|
41
|
+
* scrolls the question, the model line and every warning off the top of the
|
|
42
|
+
* screen — by the end the terminal holds a transcript nobody asked for and the
|
|
43
|
+
* answer is somewhere in the middle of it. Only the last few steps say what it
|
|
44
|
+
* is doing now, which is the only thing narration is for; the rest is in the
|
|
45
|
+
* session copilot recorded.
|
|
46
|
+
*
|
|
47
|
+
* Warnings are not narration: they leave the window and stay on the screen.
|
|
48
|
+
*
|
|
49
|
+
* Without a terminal there is nothing to rewrite over, so every line is its
|
|
50
|
+
* own — which is what a CI log wants anyway.
|
|
51
|
+
*/
|
|
52
|
+
export declare function windowSink(rows?: number): Sink;
|
|
53
|
+
/**
|
|
54
|
+
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
55
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
56
|
+
*
|
|
57
|
+
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
58
|
+
* is meant to produce a file you can read, not one with a border down its side.
|
|
59
|
+
*/
|
|
60
|
+
export declare function answerBox(text: string, columns?: number): string[];
|
|
61
|
+
//# sourceMappingURL=window.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { boxWidth, cut, cyan, dim, formatMarkdown, note, pad, red, styled, write } from "./host.js";
|
|
2
|
+
export const terminalSink = {
|
|
3
|
+
answer: (text) => write(text),
|
|
4
|
+
narrate: (line) => note(line),
|
|
5
|
+
warn: (line) => note(red(line)),
|
|
6
|
+
};
|
|
7
|
+
/** Rows of narration the window keeps on screen. */
|
|
8
|
+
export const WINDOW_ROWS = 8;
|
|
9
|
+
const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
10
|
+
/** How often the border is redrawn — the wave's frame, not the clock's. */
|
|
11
|
+
export const FRAME_MS = 80;
|
|
12
|
+
// Dimmest first, crest last. Written as escapes rather than `styleText` because
|
|
13
|
+
// a named colour has no ramp, which is also why `styled()` has to be asked.
|
|
14
|
+
const WAVE = [
|
|
15
|
+
'\u001b[0;38;5;24m',
|
|
16
|
+
'\u001b[0;38;5;31m',
|
|
17
|
+
'\u001b[0;38;5;38m',
|
|
18
|
+
'\u001b[0;38;5;45m',
|
|
19
|
+
'\u001b[0;38;5;51m',
|
|
20
|
+
'\u001b[1;38;5;159m',
|
|
21
|
+
'\u001b[1;38;5;231m',
|
|
22
|
+
];
|
|
23
|
+
const RESET = '\u001b[0m';
|
|
24
|
+
/** Cells each tone of the tail covers, so the crest reads as a band, not a dot. */
|
|
25
|
+
const WAVE_TAIL = 2;
|
|
26
|
+
/** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
|
|
27
|
+
export const WAVE_STEP = 3;
|
|
28
|
+
/**
|
|
29
|
+
* How brightly one cell of the border is lit, given where the crest has got to.
|
|
30
|
+
*
|
|
31
|
+
* `position` counts clockwise from the top left corner, so one index walks the
|
|
32
|
+
* whole ring and the wave rounds the corners without a seam. Everything the
|
|
33
|
+
* tail has passed sits at the dimmest tone, which is the border's resting
|
|
34
|
+
* colour: the ring is always drawn, only the crest moves.
|
|
35
|
+
*/
|
|
36
|
+
export function toneAt(position, head, span) {
|
|
37
|
+
const behind = (((head - position) % span) + span) % span;
|
|
38
|
+
const step = Math.floor(behind / WAVE_TAIL);
|
|
39
|
+
return step < WAVE.length ? WAVE.length - 1 - step : 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A run of border cells, coloured by the wave and emitting an escape only where
|
|
43
|
+
* the tone changes — a frame of the whole ring is a handful of them.
|
|
44
|
+
*
|
|
45
|
+
* `from` is the ring position of the first cell and `step` which way round the
|
|
46
|
+
* ring the cells run, because the bottom rule is printed left to right while
|
|
47
|
+
* the ring travels it right to left.
|
|
48
|
+
*/
|
|
49
|
+
function lit(cells, from, step, head, span) {
|
|
50
|
+
let out = '';
|
|
51
|
+
let tone = -1;
|
|
52
|
+
for (let i = 0; i < cells.length; i++) {
|
|
53
|
+
const next = toneAt(from + i * step, head, span);
|
|
54
|
+
if (next !== tone) {
|
|
55
|
+
out += WAVE[next];
|
|
56
|
+
tone = next;
|
|
57
|
+
}
|
|
58
|
+
out += cells[i];
|
|
59
|
+
}
|
|
60
|
+
return `${out}${RESET}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The bottom rule as cells, with the status label set into it near the left.
|
|
64
|
+
*
|
|
65
|
+
* Waiting belongs to the frame rather than to the narration: a row of its own
|
|
66
|
+
* costs a step of what the agent is doing every time, and it settles just
|
|
67
|
+
* under the last line, which is where the eye already is. Near the left, where
|
|
68
|
+
* reading starts, and not so near that it displaces the corner.
|
|
69
|
+
*/
|
|
70
|
+
export function footerCells(label, width) {
|
|
71
|
+
const room = width - 6;
|
|
72
|
+
const text = label && room > 0 ? [...label].slice(0, room) : [];
|
|
73
|
+
const cells = text.length > 0 ? [BOX.bl, BOX.h, ' ', ...text, ' '] : [BOX.bl];
|
|
74
|
+
while (cells.length < width - 1) {
|
|
75
|
+
cells.push(BOX.h);
|
|
76
|
+
}
|
|
77
|
+
return [...cells, BOX.br];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Narration held to a fixed number of rows that rewrite themselves in place.
|
|
81
|
+
*
|
|
82
|
+
* A long run is hundreds of tool calls, and printing each as its own line
|
|
83
|
+
* scrolls the question, the model line and every warning off the top of the
|
|
84
|
+
* screen — by the end the terminal holds a transcript nobody asked for and the
|
|
85
|
+
* answer is somewhere in the middle of it. Only the last few steps say what it
|
|
86
|
+
* is doing now, which is the only thing narration is for; the rest is in the
|
|
87
|
+
* session copilot recorded.
|
|
88
|
+
*
|
|
89
|
+
* Warnings are not narration: they leave the window and stay on the screen.
|
|
90
|
+
*
|
|
91
|
+
* Without a terminal there is nothing to rewrite over, so every line is its
|
|
92
|
+
* own — which is what a CI log wants anyway.
|
|
93
|
+
*/
|
|
94
|
+
export function windowSink(rows = WINDOW_ROWS) {
|
|
95
|
+
if (!process.stderr.isTTY) {
|
|
96
|
+
return terminalSink;
|
|
97
|
+
}
|
|
98
|
+
const kept = [];
|
|
99
|
+
const colour = styled();
|
|
100
|
+
let label = '';
|
|
101
|
+
let head = 0;
|
|
102
|
+
let painted = 0;
|
|
103
|
+
// The frame must stay under the viewport: a block taller than the screen
|
|
104
|
+
// scrolls its own top away, and then the cursor-up erase falls short and
|
|
105
|
+
// strands a copy of every repaint. The two border rows count towards it.
|
|
106
|
+
const height = () => Math.max(1, Math.min(rows, (process.stderr.rows ?? 24) - 4));
|
|
107
|
+
const columns = () => Math.max(20, (process.stderr.columns ?? 80) - 1);
|
|
108
|
+
const innerOf = () => columns() - 4;
|
|
109
|
+
const erase = () => {
|
|
110
|
+
if (painted > 0) {
|
|
111
|
+
process.stderr.write(`\u001b[${painted}A\u001b[0J`);
|
|
112
|
+
painted = 0;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/** The whole frame, border lit by the wave, every row exactly one row. */
|
|
116
|
+
const frameOf = (show, inner) => {
|
|
117
|
+
const width = inner + 4;
|
|
118
|
+
// Clockwise from the top left: across the top, down the right, back
|
|
119
|
+
// along the bottom, up the left. One ring, so one index addresses it.
|
|
120
|
+
const span = 2 * width + 2 * show.length;
|
|
121
|
+
const top = [BOX.tl, ...Array(width - 2).fill(BOX.h), BOX.tr];
|
|
122
|
+
const bottom = footerCells(label, width);
|
|
123
|
+
const edge = (position) => colour ? lit([BOX.v], position, 1, head, span) : cyan(BOX.v);
|
|
124
|
+
return [
|
|
125
|
+
colour ? lit(top, 0, 1, head, span) : cyan(top.join('')),
|
|
126
|
+
...show.map((line, r) => `${edge(span - 1 - r)} ${pad(cut(line, inner), inner)} ${edge(width + r)}`),
|
|
127
|
+
colour
|
|
128
|
+
? lit(bottom, 2 * width + show.length - 1, -1, head, span)
|
|
129
|
+
: cyan(bottom.join('')),
|
|
130
|
+
];
|
|
131
|
+
};
|
|
132
|
+
const paint = () => {
|
|
133
|
+
const show = kept.slice(-height());
|
|
134
|
+
if (show.length === 0 && label === '') {
|
|
135
|
+
erase();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// The wave wants a frame to run round before there is anything to say.
|
|
139
|
+
const lines = frameOf(show.length > 0 ? show : [''], innerOf());
|
|
140
|
+
if (painted === lines.length) {
|
|
141
|
+
// Erasing before a repaint is what flickers; overwriting in place
|
|
142
|
+
// does not, and at frame rate the whole border has to be redrawn.
|
|
143
|
+
process.stderr.write(`\u001b[${painted}A${lines.map((l) => `\r${l}\u001b[0K\n`).join('')}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
erase();
|
|
147
|
+
process.stderr.write(lines.map((l) => `${l}\n`).join(''));
|
|
148
|
+
painted = lines.length;
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
answer: (text) => {
|
|
152
|
+
erase();
|
|
153
|
+
write(text);
|
|
154
|
+
},
|
|
155
|
+
narrate: (line) => {
|
|
156
|
+
kept.push(...line.split('\n'));
|
|
157
|
+
if (kept.length > height() * 4) {
|
|
158
|
+
kept.splice(0, kept.length - height());
|
|
159
|
+
}
|
|
160
|
+
paint();
|
|
161
|
+
},
|
|
162
|
+
warn: (line) => {
|
|
163
|
+
erase();
|
|
164
|
+
note(red(line));
|
|
165
|
+
paint();
|
|
166
|
+
},
|
|
167
|
+
status: (line) => {
|
|
168
|
+
label = line;
|
|
169
|
+
head += WAVE_STEP;
|
|
170
|
+
paint();
|
|
171
|
+
},
|
|
172
|
+
close: erase,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
177
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
178
|
+
*
|
|
179
|
+
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
180
|
+
* is meant to produce a file you can read, not one with a border down its side.
|
|
181
|
+
*/
|
|
182
|
+
export function answerBox(text, columns = process.stdout.columns ?? 80) {
|
|
183
|
+
if (!process.stdout.isTTY) {
|
|
184
|
+
return text.split('\n');
|
|
185
|
+
}
|
|
186
|
+
const outer = boxWidth(text, columns);
|
|
187
|
+
const inner = outer - 4;
|
|
188
|
+
const body = formatMarkdown(text, inner);
|
|
189
|
+
const rule = BOX.h.repeat(outer - 2);
|
|
190
|
+
return [
|
|
191
|
+
'',
|
|
192
|
+
dim(`${BOX.tl}${rule}${BOX.tr}`),
|
|
193
|
+
...body.map((l) => `${dim(BOX.v)} ${pad(l, inner)} ${dim(BOX.v)}`),
|
|
194
|
+
dim(`${BOX.bl}${rule}${BOX.br}`),
|
|
195
|
+
'',
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=window.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type KeyEntry, type KeyStore, type Provider } from './host.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Copilot offers its tools as OpenAI *custom* tools, which the completions API
|
|
4
|
+
* rejects outright — `400 Invalid value: 'custom'`. Only the responses API
|
|
5
|
+
* takes them, and only the reasoning models serve it, so the wire API follows
|
|
6
|
+
* the model rather than being a flag nobody would know to set. Older models are
|
|
7
|
+
* listed rather than newer ones, so each new generation works without a release.
|
|
8
|
+
*/
|
|
9
|
+
export declare function wireApi(provider: Provider, id: string): string | undefined;
|
|
10
|
+
export interface Wiring {
|
|
11
|
+
provider: Provider;
|
|
12
|
+
/** what the model is called on the wire */
|
|
13
|
+
model: string;
|
|
14
|
+
/** every `COPILOT_*` name and value, ready to hand to the child */
|
|
15
|
+
env: Record<string, string>;
|
|
16
|
+
/** names whose values are secret, for `--secret-env-vars` and for masking */
|
|
17
|
+
secret: string[];
|
|
18
|
+
/** anything true but unwelcome: a location that will be slow, a stale default */
|
|
19
|
+
warnings: string[];
|
|
20
|
+
/** Claude on Vertex: a loopback relay to start, whose url becomes the base url */
|
|
21
|
+
relay?: {
|
|
22
|
+
key: string;
|
|
23
|
+
project: string;
|
|
24
|
+
location: string;
|
|
25
|
+
keyFile: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Where the relay's url goes once it is listening. */
|
|
29
|
+
export declare const RELAY_URL = "http://127.0.0.1:<relay>";
|
|
30
|
+
/**
|
|
31
|
+
* Turns a zen key entry and a model id into copilot's environment.
|
|
32
|
+
*
|
|
33
|
+
* `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
|
|
34
|
+
* gets one — including OpenAI, whose url copilot would otherwise have reached
|
|
35
|
+
* through a GitHub account we are deliberately not using.
|
|
36
|
+
*/
|
|
37
|
+
export declare function wire(store: KeyStore, entry: KeyEntry, id: string): Wiring;
|
|
38
|
+
/** Values a `--dry-run` may print. A key is four characters and an apology. */
|
|
39
|
+
export declare function masked(env: Record<string, string>, secret: readonly string[]): string[];
|
|
40
|
+
/** Non-fatal, said once, because a competing file is a silent override. */
|
|
41
|
+
export declare function providersWarning(): string | undefined;
|
|
42
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { credentialError, envOf, invokedAs, yellow, } from "./host.js";
|
|
4
|
+
const TYPES = {
|
|
5
|
+
openai: 'openai',
|
|
6
|
+
anthropic: 'anthropic',
|
|
7
|
+
google: 'openai',
|
|
8
|
+
vertex: 'openai',
|
|
9
|
+
openrouter: 'openai',
|
|
10
|
+
};
|
|
11
|
+
const BASE_URLS = {
|
|
12
|
+
openai: 'https://api.openai.com/v1',
|
|
13
|
+
anthropic: 'https://api.anthropic.com',
|
|
14
|
+
google: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
15
|
+
openrouter: 'https://openrouter.ai/api/v1',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Copilot offers its tools as OpenAI *custom* tools, which the completions API
|
|
19
|
+
* rejects outright — `400 Invalid value: 'custom'`. Only the responses API
|
|
20
|
+
* takes them, and only the reasoning models serve it, so the wire API follows
|
|
21
|
+
* the model rather than being a flag nobody would know to set. Older models are
|
|
22
|
+
* listed rather than newer ones, so each new generation works without a release.
|
|
23
|
+
*/
|
|
24
|
+
export function wireApi(provider, id) {
|
|
25
|
+
return provider === 'openai' && !/^(gpt-4|gpt-3\.5|chatgpt-)/.test(id)
|
|
26
|
+
? 'responses'
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Where the relay's url goes once it is listening. */
|
|
30
|
+
export const RELAY_URL = 'http://127.0.0.1:<relay>';
|
|
31
|
+
const vertexClaude = (id) => /^(anthropic\/)?claude-/.test(id);
|
|
32
|
+
/**
|
|
33
|
+
* Turns a zen key entry and a model id into copilot's environment.
|
|
34
|
+
*
|
|
35
|
+
* `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
|
|
36
|
+
* gets one — including OpenAI, whose url copilot would otherwise have reached
|
|
37
|
+
* through a GitHub account we are deliberately not using.
|
|
38
|
+
*/
|
|
39
|
+
export function wire(store, entry, id) {
|
|
40
|
+
const provider = entry.provider;
|
|
41
|
+
const env = {};
|
|
42
|
+
const secret = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
env.COPILOT_PROVIDER_TYPE = TYPES[provider];
|
|
45
|
+
env.COPILOT_MODEL = id;
|
|
46
|
+
if (provider === 'vertex' && vertexClaude(id)) {
|
|
47
|
+
const project = store.projectOf(entry)?.id;
|
|
48
|
+
if (entry.holds !== 'file' || !project) {
|
|
49
|
+
throw credentialError(`Claude on vertex needs a service account and a GCP project, not ${entry.provider}/${entry.name}`, 'add one: zen key add vertex --project <id>');
|
|
50
|
+
}
|
|
51
|
+
const model = id.replace(/^anthropic\//, '');
|
|
52
|
+
const key = randomBytes(24).toString('hex');
|
|
53
|
+
env.COPILOT_PROVIDER_TYPE = 'anthropic';
|
|
54
|
+
env.COPILOT_MODEL = model;
|
|
55
|
+
env.COPILOT_PROVIDER_BASE_URL = RELAY_URL;
|
|
56
|
+
env.COPILOT_PROVIDER_API_KEY = key;
|
|
57
|
+
return {
|
|
58
|
+
provider,
|
|
59
|
+
model,
|
|
60
|
+
env,
|
|
61
|
+
secret: ['COPILOT_PROVIDER_API_KEY'],
|
|
62
|
+
warnings,
|
|
63
|
+
relay: {
|
|
64
|
+
key,
|
|
65
|
+
project,
|
|
66
|
+
location: entry.location?.trim() || 'global',
|
|
67
|
+
keyFile: store.fileOf(entry),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (provider === 'vertex') {
|
|
72
|
+
const project = store.projectOf(entry)?.id;
|
|
73
|
+
if (!project) {
|
|
74
|
+
throw credentialError(`no GCP project on ${entry.provider}/${entry.name}`, 'set one: zen key add vertex --project <id>');
|
|
75
|
+
}
|
|
76
|
+
const named = entry.location?.trim() || 'global';
|
|
77
|
+
const partner = id.includes('/') && !id.startsWith('google/');
|
|
78
|
+
// Gemini serves the `us`/`eu` multi-regions; partner models answer them with a 404 or 429.
|
|
79
|
+
const location = partner && (named === 'us' || named === 'eu') ? 'global' : named;
|
|
80
|
+
if (location === 'global' && !partner) {
|
|
81
|
+
warnings.push('location `global` adds about ten seconds of cold start');
|
|
82
|
+
}
|
|
83
|
+
env.COPILOT_PROVIDER_BASE_URL = `https://aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/endpoints/openapi`;
|
|
84
|
+
// Vertex speaks publisher names on the wire and knows nothing of zen's
|
|
85
|
+
// provider prefix; the bare id stays as the model *id* so copilot can
|
|
86
|
+
// still match its catalogue for token limits and tool support.
|
|
87
|
+
env.COPILOT_PROVIDER_WIRE_MODEL = id.includes('/') ? id : `google/${id}`;
|
|
88
|
+
// Only Google's own ids are bare: `gpt-oss-…` would get GPT's custom `apply_patch`, which this endpoint rejects.
|
|
89
|
+
env.COPILOT_PROVIDER_MODEL_ID = id.startsWith('google/') ? id.slice('google/'.length) : id;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const url = BASE_URLS[provider];
|
|
93
|
+
if (!url) {
|
|
94
|
+
throw credentialError(`no BYOK endpoint known for ${provider}`);
|
|
95
|
+
}
|
|
96
|
+
env.COPILOT_PROVIDER_BASE_URL = url;
|
|
97
|
+
}
|
|
98
|
+
const api = wireApi(provider, id);
|
|
99
|
+
if (api) {
|
|
100
|
+
env.COPILOT_PROVIDER_WIRE_API = api;
|
|
101
|
+
}
|
|
102
|
+
if (entry.holds === 'file') {
|
|
103
|
+
// A Google access token is good for an hour and a session is not, so
|
|
104
|
+
// copilot is told how to mint one rather than handed one that will be
|
|
105
|
+
// stale by the time it matters.
|
|
106
|
+
env.COPILOT_PROVIDER_API_KEY_COMMAND = `${invokedAs('zen')} key token ${entry.provider}`;
|
|
107
|
+
env[envOf(entry)] = store.fileOf(entry);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
env.COPILOT_PROVIDER_API_KEY = store.reveal(entry);
|
|
111
|
+
secret.push('COPILOT_PROVIDER_API_KEY');
|
|
112
|
+
}
|
|
113
|
+
return { provider, model: env.COPILOT_PROVIDER_WIRE_MODEL ?? id, env, secret, warnings };
|
|
114
|
+
}
|
|
115
|
+
/** Values a `--dry-run` may print. A key is four characters and an apology. */
|
|
116
|
+
export function masked(env, secret) {
|
|
117
|
+
return Object.entries(env)
|
|
118
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
119
|
+
.map(([name, value]) => `${name}=${secret.includes(name) ? '••••' : value}`);
|
|
120
|
+
}
|
|
121
|
+
/** Non-fatal, said once, because a competing file is a silent override. */
|
|
122
|
+
export function providersWarning() {
|
|
123
|
+
const configured = process.env.COPILOT_PROVIDERS_CONFIG;
|
|
124
|
+
const path = configured ?? `${process.env.HOME ?? ''}/.copilot/providers.json`;
|
|
125
|
+
return existsSync(path)
|
|
126
|
+
? yellow(`${path} may override the provider zen just wired`)
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Provider } from './keys.ts';
|
|
2
|
+
export declare const PROVIDER_NAMES: ReadonlySet<string>;
|
|
3
|
+
/**
|
|
4
|
+
* `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
|
|
5
|
+
* counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
|
|
6
|
+
* bare id on an endpoint that speaks publisher names.
|
|
7
|
+
*
|
|
8
|
+
* The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
|
|
9
|
+
* selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
|
|
10
|
+
* came from `agents.yaml`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function splitRef(ref: string): {
|
|
13
|
+
provider?: Provider;
|
|
14
|
+
id: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=modelref.d.ts.map
|
package/dist/modelref.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Model refs as people type them
|
|
3
|
+
//
|
|
4
|
+
// `vertex/gemini-3.8-flash` from a person, `openai:gpt-5.6-sol` from
|
|
5
|
+
// `agents.yaml`. Read by `zen inspect ask` and by `zen meta`, so it lives with
|
|
6
|
+
// neither.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
export const PROVIDER_NAMES = new Set([
|
|
9
|
+
'openai',
|
|
10
|
+
'anthropic',
|
|
11
|
+
'google',
|
|
12
|
+
'vertex',
|
|
13
|
+
'openrouter',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
|
|
17
|
+
* counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
|
|
18
|
+
* bare id on an endpoint that speaks publisher names.
|
|
19
|
+
*
|
|
20
|
+
* The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
|
|
21
|
+
* selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
|
|
22
|
+
* came from `agents.yaml`.
|
|
23
|
+
*/
|
|
24
|
+
export function splitRef(ref) {
|
|
25
|
+
const colon = ref.indexOf(':');
|
|
26
|
+
if (colon > 0) {
|
|
27
|
+
const head = ref.slice(0, colon).split('/')[0];
|
|
28
|
+
if (PROVIDER_NAMES.has(head)) {
|
|
29
|
+
return { provider: head, id: ref.slice(colon + 1) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const slash = ref.indexOf('/');
|
|
33
|
+
if (slash < 0) {
|
|
34
|
+
return { id: ref };
|
|
35
|
+
}
|
|
36
|
+
const head = ref.slice(0, slash);
|
|
37
|
+
return PROVIDER_NAMES.has(head)
|
|
38
|
+
? { provider: head, id: ref.slice(slash + 1) }
|
|
39
|
+
: { id: ref };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=modelref.js.map
|
package/dist/narrate.js
CHANGED
|
@@ -117,8 +117,11 @@ export function duration(ms) {
|
|
|
117
117
|
if (ms < 60_000) {
|
|
118
118
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
119
119
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
120
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
121
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
122
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
123
|
+
const seconds = totalSeconds % 60;
|
|
124
|
+
return hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
|
|
122
125
|
}
|
|
123
126
|
export const stopMark = (reason) => reason === 'final' ? green('done') : reason === 'aborted' ? yellow('aborted') : red('failed');
|
|
124
127
|
//# sourceMappingURL=narrate.js.map
|
package/dist/podman.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface PodmanStatus {
|
|
|
41
41
|
swap: number;
|
|
42
42
|
cpus: number;
|
|
43
43
|
};
|
|
44
|
+
/** containers the engine can still create; a stopped one holds a lock too */
|
|
45
|
+
freeLocks?: number;
|
|
44
46
|
image?: string;
|
|
45
47
|
imagePresent?: boolean;
|
|
46
48
|
}
|
|
@@ -62,6 +64,30 @@ export declare class BuildError extends CliError {
|
|
|
62
64
|
}
|
|
63
65
|
/** What `zn sandbox status` prints. Changes nothing, and never throws. */
|
|
64
66
|
export declare function podmanStatus(opts?: PodmanOptions): Promise<PodmanStatus>;
|
|
67
|
+
/** Containers the engine can still create; undefined when it will not say. */
|
|
68
|
+
export declare function freeLocks(engine?: string, exec?: import("@zenera/neo").Runner): Promise<number | undefined>;
|
|
69
|
+
export interface LockOptions {
|
|
70
|
+
engine?: string;
|
|
71
|
+
exec?: typeof runProcess;
|
|
72
|
+
/** a container that may already exist, and so needs no new lock */
|
|
73
|
+
reuses?: string;
|
|
74
|
+
/** session id -> its directory; read from the registry when absent */
|
|
75
|
+
owners?: ReadonlyMap<string, string>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Refuses a run the engine cannot hold before anything is spent on it. Every
|
|
79
|
+
* container takes a lock, stopped ones included, and at zero the failure would
|
|
80
|
+
* otherwise land on the first shell command — mid-turn, or once per item.
|
|
81
|
+
*/
|
|
82
|
+
export declare function assertLocks(need: number, opts?: LockOptions): Promise<void>;
|
|
83
|
+
/** Every session on this machine, by id, from the projects the registry knows. */
|
|
84
|
+
export declare function sessionOwners(): Promise<Map<string, string>>;
|
|
85
|
+
/**
|
|
86
|
+
* Containers no live process is using: every stopped one, and a running one
|
|
87
|
+
* whose run exited without tearing it down (a kill, a crash, a closed lid).
|
|
88
|
+
* A running container that cannot be traced to its owner is left alone.
|
|
89
|
+
*/
|
|
90
|
+
export declare function idleContainers(containers: readonly OwnedContainer[], owners: ReadonlyMap<string, string>): OwnedContainer[];
|
|
65
91
|
export interface OwnedContainer {
|
|
66
92
|
name: string;
|
|
67
93
|
/** podman's own word: `running`, `exited`, `created`, `paused` */
|
package/dist/podman.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { runProcess, SandboxError } from '@zenera/neo';
|
|
1
|
+
import { pidAlive, runProcess, SandboxError } from '@zenera/neo';
|
|
2
2
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { isBusy, isProjectDir, Registry, sessionIds, sessionsDir } from "./projects.js";
|
|
5
6
|
import { CliError, confirm, dim, EXIT, isInteractive, note, progress } from "./term.js";
|
|
6
7
|
const DEFAULT_MACHINE_CPUS = 2;
|
|
7
8
|
const DEFAULT_MACHINE_MEMORY = 2048;
|
|
@@ -409,6 +410,9 @@ export async function podmanStatus(opts = {}) {
|
|
|
409
410
|
cpus: Number.isFinite(cpus) ? cpus : 0,
|
|
410
411
|
};
|
|
411
412
|
}
|
|
413
|
+
// Asked apart from the line above: an engine without the field fails
|
|
414
|
+
// the whole template, and that must not cost the report its memory.
|
|
415
|
+
status.freeLocks = await freeLocks(engine, run);
|
|
412
416
|
}
|
|
413
417
|
if (opts.image) {
|
|
414
418
|
status.image = opts.image;
|
|
@@ -425,6 +429,76 @@ function safeMachines(stdout) {
|
|
|
425
429
|
return [];
|
|
426
430
|
}
|
|
427
431
|
}
|
|
432
|
+
/** Containers the engine can still create; undefined when it will not say. */
|
|
433
|
+
export async function freeLocks(engine = 'podman', exec = runProcess) {
|
|
434
|
+
const res = await exec(engine, ['info', '--format', '{{.Host.FreeLocks}}'], {
|
|
435
|
+
timeoutMs: 30_000,
|
|
436
|
+
}).catch(() => undefined);
|
|
437
|
+
const free = res?.code === 0 ? res.stdout.trim() : '';
|
|
438
|
+
return /^\d+$/.test(free) ? Number(free) : undefined;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Refuses a run the engine cannot hold before anything is spent on it. Every
|
|
442
|
+
* container takes a lock, stopped ones included, and at zero the failure would
|
|
443
|
+
* otherwise land on the first shell command — mid-turn, or once per item.
|
|
444
|
+
*/
|
|
445
|
+
export async function assertLocks(need, opts = {}) {
|
|
446
|
+
const engine = opts.engine ?? 'podman';
|
|
447
|
+
const exec = opts.exec ?? runProcess;
|
|
448
|
+
const free = await freeLocks(engine, exec);
|
|
449
|
+
if (free === undefined || free >= need) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (opts.reuses) {
|
|
453
|
+
const there = await exec(engine, ['container', 'exists', opts.reuses], {
|
|
454
|
+
timeoutMs: 30_000,
|
|
455
|
+
}).catch(() => undefined);
|
|
456
|
+
if (there?.code === 0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const idle = idleContainers(await ownedContainers(engine, exec), opts.owners ?? (await sessionOwners())).length;
|
|
461
|
+
throw new CliError(`the container engine has ${free} locks free and this run needs ${need} — ` +
|
|
462
|
+
'every container holds one, stopped ones included', EXIT.sandbox, idle > 0
|
|
463
|
+
? `zen sandbox clean --idle frees ${idle}`
|
|
464
|
+
: `every container zen made is in use — \`${engine} ps --all\` shows what holds them`);
|
|
465
|
+
}
|
|
466
|
+
/** Every session on this machine, by id, from the projects the registry knows. */
|
|
467
|
+
export async function sessionOwners() {
|
|
468
|
+
const owners = new Map();
|
|
469
|
+
for (const entry of (await Registry.open()).entries) {
|
|
470
|
+
if (isProjectDir(entry.path)) {
|
|
471
|
+
for (const id of sessionIds(entry.path)) {
|
|
472
|
+
owners.set(id, join(sessionsDir(entry.path), id));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return owners;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Containers no live process is using: every stopped one, and a running one
|
|
480
|
+
* whose run exited without tearing it down (a kill, a crash, a closed lid).
|
|
481
|
+
* A running container that cannot be traced to its owner is left alone.
|
|
482
|
+
*/
|
|
483
|
+
export function idleContainers(containers, owners) {
|
|
484
|
+
return containers.filter((c) => {
|
|
485
|
+
if (c.state !== 'running') {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
const faker = /^faker-(\d+)$/.exec(c.key ?? '');
|
|
489
|
+
if (faker) {
|
|
490
|
+
return !pidAlive(Number(faker[1]));
|
|
491
|
+
}
|
|
492
|
+
const dir = c.key === undefined ? undefined : owners.get(c.key);
|
|
493
|
+
return dir !== undefined && !isBusy(dir);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* `ps --format json` is ~1 kB a container, so the runner's default 64 kB cap cut
|
|
498
|
+
* the listing at about sixty, the JSON stopped parsing, and every report said
|
|
499
|
+
* "none" while two thousand sat on the machine.
|
|
500
|
+
*/
|
|
501
|
+
const LISTING_BYTES = 64 * 1024 * 1024;
|
|
428
502
|
/**
|
|
429
503
|
* Containers this CLI created, whatever session they belong to, and whether
|
|
430
504
|
* each is up. `--all` is the point: with `persist: true` a session leaves a
|
|
@@ -438,7 +512,7 @@ export async function ownedContainers(engine = 'podman', exec = runProcess, opts
|
|
|
438
512
|
if (opts.sizes) {
|
|
439
513
|
args.push('--size');
|
|
440
514
|
}
|
|
441
|
-
const res = await exec(engine, args, { timeoutMs: 120_000 }).catch(() => undefined);
|
|
515
|
+
const res = await exec(engine, args, { timeoutMs: 120_000, maxBytes: LISTING_BYTES }).catch(() => undefined);
|
|
442
516
|
if (!res || res.code !== 0) {
|
|
443
517
|
return [];
|
|
444
518
|
}
|
|
@@ -471,8 +545,14 @@ export async function removeContainers(names, engine = 'podman', exec = runProce
|
|
|
471
545
|
if (names.length === 0) {
|
|
472
546
|
return;
|
|
473
547
|
}
|
|
474
|
-
|
|
548
|
+
// In slices, so two thousand of them get a timeout each slice can meet.
|
|
549
|
+
for (let i = 0; i < names.length; i += REMOVE_SLICE) {
|
|
550
|
+
await exec(engine, ['rm', '--force', '--volumes', ...names.slice(i, i + REMOVE_SLICE)], {
|
|
551
|
+
timeoutMs: 120_000,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
475
554
|
}
|
|
555
|
+
const REMOVE_SLICE = 100;
|
|
476
556
|
/** What the engine is using. Never throws: a disk report is not worth a crash. */
|
|
477
557
|
export async function engineDisk(engine = 'podman', exec = runProcess) {
|
|
478
558
|
const call = (args) => exec(engine, args, { timeoutMs: 120_000 }).catch(() => undefined);
|