@zenera/cli 1.1.24 → 1.1.26
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 +65 -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 +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -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 +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -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 +127 -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 +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -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 +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -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 +47 -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 +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -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/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- 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/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -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 +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -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 +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -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 +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- 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/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- 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 +1492 -660
- 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 +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- 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/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
package/dist/commands/models.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
// instead", and prints a ref on stdout that can be pasted straight back into
|
|
12
12
|
// agents.yaml — or read by an agent doing the pasting.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
import { createEmbedder,
|
|
14
|
+
import { createEmbedder, ModelRegistry } from '@zenera/neo';
|
|
15
15
|
import { parse } from "../args.js";
|
|
16
|
-
import { loadCatalog, loadCatalogs, matches, PREFERRED, } from "../catalog.js";
|
|
16
|
+
import { byGroup, groupOf, isPartner, loadCatalog, loadCatalogs, matches, PREFERRED, routerBalance, } from "../catalog.js";
|
|
17
17
|
import { ensureHome } from "../home.js";
|
|
18
18
|
import { envNames, form, isProvider, KeyStore, PROVIDERS } from "../keys.js";
|
|
19
|
-
import { probeModel } from "../liveness.js";
|
|
20
|
-
import { ago, bold, credentialError, cyan, dim, green, json, note, progress, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
21
|
-
const USAGE = 'zen models <providers|ls|search|show|test|pick> [ref] [options]';
|
|
19
|
+
import { probeChat, probeModel } from "../liveness.js";
|
|
20
|
+
import { ago, bold, CliError, credentialError, cyan, dim, EXIT, green, isInteractive, json, note, progress, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
21
|
+
const USAGE = 'zen models <providers|ls|search|show|test|pick|browse> [ref] [options]';
|
|
22
22
|
/**
|
|
23
23
|
* The keyring is materialised here, and only here.
|
|
24
24
|
*
|
|
@@ -112,15 +112,32 @@ function freshness(cat) {
|
|
|
112
112
|
return cat.origin === 'live' ? green('fetched now') : dim(`cached ${ago(cat.fetchedAt)}`);
|
|
113
113
|
}
|
|
114
114
|
const roleMark = (row) => row.roles.join('+');
|
|
115
|
+
const maker = (row) => !row.publisher ? '' : isPartner(row) ? yellow(row.publisher) : green(row.publisher);
|
|
115
116
|
function row(entry) {
|
|
116
117
|
return [
|
|
117
118
|
cyan(entry.ref),
|
|
119
|
+
maker(entry),
|
|
118
120
|
dim(roleMark(entry)),
|
|
119
121
|
dim(tokens(entry.contextLength)),
|
|
120
122
|
dim(entry.pricing?.free ? 'free' : ''),
|
|
121
123
|
dim(entry.name ?? ''),
|
|
122
124
|
];
|
|
123
125
|
}
|
|
126
|
+
/** The table, with a heading wherever a reseller's rows change maker. */
|
|
127
|
+
function grouped(entries) {
|
|
128
|
+
const lines = table(entries.map(row));
|
|
129
|
+
const out = [];
|
|
130
|
+
let last;
|
|
131
|
+
entries.forEach((e, i) => {
|
|
132
|
+
const group = groupOf(e);
|
|
133
|
+
if (group && group !== last) {
|
|
134
|
+
out.push(...(out.length > 0 ? [''] : []), bold(group));
|
|
135
|
+
}
|
|
136
|
+
last = group;
|
|
137
|
+
out.push(lines[i]);
|
|
138
|
+
});
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
124
141
|
/**
|
|
125
142
|
* Says how a listing was obtained whenever it was not the vendor's own word —
|
|
126
143
|
* on stderr, because it is narration about the answer rather than the answer.
|
|
@@ -175,6 +192,7 @@ const providers = async (ctx, args) => {
|
|
|
175
192
|
})));
|
|
176
193
|
note('');
|
|
177
194
|
note(dim('zen models ls <provider> what one of them serves'));
|
|
195
|
+
note(dim('zen models browse walk them with the arrow keys'));
|
|
178
196
|
note(dim('zen models pick --chat the first one that answers'));
|
|
179
197
|
};
|
|
180
198
|
// ---------------------------------------------------------------------------
|
|
@@ -198,7 +216,7 @@ const ls = async (ctx, args) => {
|
|
|
198
216
|
const all = cats
|
|
199
217
|
.flatMap((c) => c.entries)
|
|
200
218
|
.filter((e) => matches(e, '', { roles }))
|
|
201
|
-
.sort(
|
|
219
|
+
.sort(byGroup);
|
|
202
220
|
if (ctx.json) {
|
|
203
221
|
json({
|
|
204
222
|
models: all,
|
|
@@ -215,10 +233,13 @@ const ls = async (ctx, args) => {
|
|
|
215
233
|
note(dim('nothing matched — try: zen models ls --refresh'));
|
|
216
234
|
return;
|
|
217
235
|
}
|
|
218
|
-
writeAll(
|
|
236
|
+
writeAll(grouped(all.slice(0, limit)));
|
|
219
237
|
if (all.length > limit) {
|
|
220
238
|
note(dim(`${all.length - limit} more — narrow with \`zen models search\`, or pass --all`));
|
|
221
239
|
}
|
|
240
|
+
if (all.some(isPartner)) {
|
|
241
|
+
note(dim('partner models are billed through Vertex and must be enabled in Model Garden first: https://console.cloud.google.com/vertex-ai/model-garden'));
|
|
242
|
+
}
|
|
222
243
|
};
|
|
223
244
|
// ---------------------------------------------------------------------------
|
|
224
245
|
// search
|
|
@@ -255,7 +276,7 @@ const search = async (ctx, args) => {
|
|
|
255
276
|
// Cheapest first when price is known: search is usually the step before
|
|
256
277
|
// picking one, and the free ones are what most people are looking for.
|
|
257
278
|
.sort((a, b) => Number(b.pricing?.free ?? false) - Number(a.pricing?.free ?? false) ||
|
|
258
|
-
a
|
|
279
|
+
byGroup(a, b));
|
|
259
280
|
if (ctx.json) {
|
|
260
281
|
json({ query, matched: hits.length, models: hits.slice(0, limit) });
|
|
261
282
|
return;
|
|
@@ -377,9 +398,7 @@ function target(ref, provider, id, role) {
|
|
|
377
398
|
if (role !== 'chat') {
|
|
378
399
|
throw usageError(`${ref} is ${role}-only, and there is no way to ask it a question from here`, 'zen models test only exercises chat and embedding models');
|
|
379
400
|
}
|
|
380
|
-
|
|
381
|
-
// a cap at all, so this is not merely thrift.
|
|
382
|
-
return { ref, kind: 'model', model: createModel({ provider, model: id, maxTokens: 16 }) };
|
|
401
|
+
return { ref, kind: 'model', model: probeChat(provider, id) };
|
|
383
402
|
}
|
|
384
403
|
function verdict(probe) {
|
|
385
404
|
switch (probe.check.state) {
|
|
@@ -536,6 +555,68 @@ const pick = async (ctx, args) => {
|
|
|
536
555
|
throw credentialError(`no ${role} model answered on this machine`, 'add a credential: zen key add openai');
|
|
537
556
|
};
|
|
538
557
|
// ---------------------------------------------------------------------------
|
|
558
|
+
// browse
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
/**
|
|
561
|
+
* The same catalog, walked with the arrow keys: providers, what one serves,
|
|
562
|
+
* one model in full, with a key to ask it a question and a key to take it.
|
|
563
|
+
*
|
|
564
|
+
* The interface is drawn on stderr, so a pick reaches stdout alone and
|
|
565
|
+
* `$(zen models browse)` is the ref, the same contract `pick` keeps.
|
|
566
|
+
*/
|
|
567
|
+
const browse = async (ctx, args) => {
|
|
568
|
+
const { values, positionals } = parse(args, { refresh: { type: 'boolean' }, theme: { type: 'string' } }, 'zen models browse [provider] [--refresh] [--theme dark|light|auto]');
|
|
569
|
+
const initial = positionals[0];
|
|
570
|
+
if (initial !== undefined && !isProvider(initial)) {
|
|
571
|
+
throw usageError(`unknown provider "${initial}"`, `known: ${PROVIDERS.join(', ')}`);
|
|
572
|
+
}
|
|
573
|
+
if (ctx.json || !isInteractive()) {
|
|
574
|
+
throw usageError('browsing needs a person at a terminal', 'from a script: zen models ls --json, or zen models pick --chat');
|
|
575
|
+
}
|
|
576
|
+
const where = await credentials();
|
|
577
|
+
// Offline, like `providers`: the overview should not cost five listings.
|
|
578
|
+
// A provider is fetched when it is opened.
|
|
579
|
+
const cats = await loadCatalogs(PROVIDERS, { offline: true });
|
|
580
|
+
const { browse: draw } = await import("../tui/models.js");
|
|
581
|
+
const chosen = await draw({
|
|
582
|
+
providers: cats.map((c) => ({
|
|
583
|
+
provider: c.provider,
|
|
584
|
+
credential: source(where, c.provider),
|
|
585
|
+
models: c.entries.length,
|
|
586
|
+
origin: c.origin,
|
|
587
|
+
fetchedAt: c.fetchedAt,
|
|
588
|
+
})),
|
|
589
|
+
initial,
|
|
590
|
+
theme: values.theme,
|
|
591
|
+
load: (provider, refresh) => loadCatalog(provider, { refresh: refresh || values.refresh }),
|
|
592
|
+
balance: async (provider) => provider === 'openrouter' && source(where, provider) ? routerBalance() : undefined,
|
|
593
|
+
test: async (entry) => {
|
|
594
|
+
const role = entry.roles.includes('chat')
|
|
595
|
+
? 'chat'
|
|
596
|
+
: entry.roles.includes('embedding')
|
|
597
|
+
? 'embedding'
|
|
598
|
+
: (entry.roles[0] ?? 'chat');
|
|
599
|
+
const probe = await probeModel(target(entry.ref, entry.provider, entry.id, role));
|
|
600
|
+
return {
|
|
601
|
+
state: probe.check.state,
|
|
602
|
+
ms: probe.ms,
|
|
603
|
+
detail: probe.check.detail,
|
|
604
|
+
fix: probe.check.fix,
|
|
605
|
+
dimensions: probe.dimensions,
|
|
606
|
+
};
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
if (!chosen) {
|
|
610
|
+
// Leaving is a fine way to end a browse, but not an answer to a
|
|
611
|
+
// substitution that was waiting for one.
|
|
612
|
+
if (!process.stdout.isTTY) {
|
|
613
|
+
throw new CliError('nothing picked', EXIT.failed);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
write(chosen.ref);
|
|
618
|
+
};
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
539
620
|
// Dispatch
|
|
540
621
|
// ---------------------------------------------------------------------------
|
|
541
622
|
const SUBS = {
|
|
@@ -548,6 +629,8 @@ const SUBS = {
|
|
|
548
629
|
test,
|
|
549
630
|
check: test,
|
|
550
631
|
pick,
|
|
632
|
+
browse,
|
|
633
|
+
ui: browse,
|
|
551
634
|
};
|
|
552
635
|
export const models = {
|
|
553
636
|
summary: 'What this machine can use: list, search, test and pick models.',
|
|
@@ -570,6 +653,8 @@ export const models = {
|
|
|
570
653
|
' zen models show <ref> One model, in full.',
|
|
571
654
|
' zen models test <ref> … Ask it one real question.',
|
|
572
655
|
' zen models pick --embedding The first ref that answers, on stdout.',
|
|
656
|
+
' zen models browse [provider] Walk providers and models with the',
|
|
657
|
+
' arrow keys; t asks one, p prints its ref.',
|
|
573
658
|
],
|
|
574
659
|
run: async (ctx) => {
|
|
575
660
|
const [name, ...rest] = ctx.args;
|
|
@@ -588,7 +673,7 @@ export const models = {
|
|
|
588
673
|
await ls(ctx, ctx.args);
|
|
589
674
|
return;
|
|
590
675
|
}
|
|
591
|
-
throw usageError(`unknown: zen models ${name}`, `try: ${cyan('providers, ls, search, show, test, pick')} — or a provider: ${dim(PROVIDERS.join(', '))}`);
|
|
676
|
+
throw usageError(`unknown: zen models ${name}`, `try: ${cyan('providers, ls, search, show, test, pick, browse')} — or a provider: ${dim(PROVIDERS.join(', '))}`);
|
|
592
677
|
},
|
|
593
678
|
};
|
|
594
679
|
//# sourceMappingURL=models.js.map
|
package/dist/commands/open.js
CHANGED
|
@@ -3,9 +3,9 @@ import { accessSync, constants, existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { delimiter, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { one, parse } from "../args.js";
|
|
6
|
+
import { editorFiles } from "../meta/editor.js";
|
|
6
7
|
import { project as resolveProject } from "../resolve.js";
|
|
7
|
-
import {
|
|
8
|
-
import { CliError, cyan, dim, EXIT, json, note, usageError } from "../term.js";
|
|
8
|
+
import { CliError, cyan, dim, EXIT, isInteractive, json, note, usageError } from "../term.js";
|
|
9
9
|
const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
|
|
10
10
|
/**
|
|
11
11
|
* A project is resolved by name or from the cwd, and the path is handed to an
|
|
@@ -247,8 +247,8 @@ function verify(editor) {
|
|
|
247
247
|
? 'name one with --editor, or set $ZENERA_EDITOR'
|
|
248
248
|
: `${editor.from} names a command that is not on PATH`);
|
|
249
249
|
}
|
|
250
|
-
if ((editor.from === '$VISUAL' || editor.from === '$EDITOR') && !
|
|
251
|
-
throw usageError(`${editor.from} names a terminal editor and there is no terminal here`, 'name a windowed one with --editor, or set $ZENERA_EDITOR');
|
|
250
|
+
if ((editor.from === '$VISUAL' || editor.from === '$EDITOR') && !isInteractive()) {
|
|
251
|
+
throw usageError(`${editor.from} names a terminal editor and there is no one at a terminal here`, 'name a windowed one with --editor, or set $ZENERA_EDITOR');
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
async function launch(editor, dir) {
|
package/dist/commands/run.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { usageByModel } from '@zenera/neo';
|
|
1
2
|
import { writeFile } from 'node:fs/promises';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import { parse } from "../args.js";
|
|
4
5
|
import { concurrency, runBatch } from "../batch.js";
|
|
5
6
|
import * as Engine from "../engine.js";
|
|
7
|
+
import { agentEventLog } from "../eventlog.js";
|
|
6
8
|
import { duration, Narrator, stopMark, summary } from "../narrate.js";
|
|
7
9
|
import * as Projects from "../projects.js";
|
|
8
10
|
import { readBatch, readRequest } from "../request.js";
|
|
9
11
|
import { target } from "../resolve.js";
|
|
10
12
|
import { display } from "../session.js";
|
|
11
|
-
import { bold, cyan, dim, json, jsonText, note, readStdin, usageError, write } from "../term.js";
|
|
13
|
+
import { bold, cyan, dim, drivenByAgent, json, jsonText, note, readStdin, usageError, write, } from "../term.js";
|
|
12
14
|
import { parseChoice } from "../tui/theme.js";
|
|
15
|
+
import { appendUsage } from "../usage.js";
|
|
13
16
|
const USAGE = 'zen run [project] [prompt] [options]';
|
|
14
17
|
export const run = {
|
|
15
18
|
summary: 'Run the project — the TUI on a terminal, one shot otherwise.',
|
|
@@ -40,6 +43,10 @@ export const run = {
|
|
|
40
43
|
' --theme <dark|light> Colours for the TUI. Default: auto.',
|
|
41
44
|
' --out <file> Put the answer in this file instead of on screen.',
|
|
42
45
|
' With --json, the file gets the whole JSON envelope.',
|
|
46
|
+
' --events <file> Append what the agent does to this file as it does it:',
|
|
47
|
+
' one JSON line per model call, tool call and message.',
|
|
48
|
+
' --disposable Remove its containers when it ends, whatever `persist:`',
|
|
49
|
+
' says: for a session that will never be continued.',
|
|
43
50
|
' --yes Answer yes to every question.',
|
|
44
51
|
' --json Print machine-readable JSON (session, run, mounts, output, etc...).',
|
|
45
52
|
'',
|
|
@@ -121,6 +128,8 @@ export const run = {
|
|
|
121
128
|
plain: { type: 'boolean' },
|
|
122
129
|
theme: { type: 'string' },
|
|
123
130
|
out: { type: 'string' },
|
|
131
|
+
events: { type: 'string' },
|
|
132
|
+
disposable: { type: 'boolean' },
|
|
124
133
|
'batch-dir': { type: 'string' },
|
|
125
134
|
concurrency: { type: 'string' },
|
|
126
135
|
}, USAGE);
|
|
@@ -158,6 +167,9 @@ export const run = {
|
|
|
158
167
|
// TUI, where there is someone to ask, still asks.
|
|
159
168
|
const input = request ? request.input : prompt;
|
|
160
169
|
const shot = Boolean(prompt) || Boolean(request);
|
|
170
|
+
if (values.events && !shot) {
|
|
171
|
+
throw usageError('--events records a one-shot run', 'give a prompt or --input');
|
|
172
|
+
}
|
|
161
173
|
const where = await target({
|
|
162
174
|
cwd: ctx.cwd,
|
|
163
175
|
project: request?.project ?? values.project ?? (named ? head : undefined),
|
|
@@ -179,6 +191,7 @@ export const run = {
|
|
|
179
191
|
memoryReadOnly: values['memory-read-only'],
|
|
180
192
|
keys: values['no-keys'] ? false : undefined,
|
|
181
193
|
yes: values.yes || ctx.json,
|
|
194
|
+
disposable: values.disposable,
|
|
182
195
|
});
|
|
183
196
|
try {
|
|
184
197
|
// The TUI is for the case it is actually good at: a person at a
|
|
@@ -188,7 +201,8 @@ export const run = {
|
|
|
188
201
|
const drawing = !shot &&
|
|
189
202
|
!values.plain &&
|
|
190
203
|
!ctx.json &&
|
|
191
|
-
Boolean(process.stdout.isTTY && process.stdin.isTTY)
|
|
204
|
+
Boolean(process.stdout.isTTY && process.stdin.isTTY) &&
|
|
205
|
+
!drivenByAgent();
|
|
192
206
|
if (drawing) {
|
|
193
207
|
const { start } = await import("../tui/app.js");
|
|
194
208
|
await start(engine, {
|
|
@@ -222,6 +236,8 @@ const NOT_IN_A_BATCH = [
|
|
|
222
236
|
['workspace', 'they cannot share one; each item gets its own, or names one in the file'],
|
|
223
237
|
['plain', 'a batch never draws the TUI'],
|
|
224
238
|
['theme', 'a batch never draws the TUI'],
|
|
239
|
+
['events', 'each item already has its own output.json'],
|
|
240
|
+
['disposable', 'a batch removes its containers already'],
|
|
225
241
|
];
|
|
226
242
|
async function batch(ctx, values, rest, piped) {
|
|
227
243
|
if (rest.length > 0) {
|
|
@@ -261,11 +277,18 @@ async function once(engine, input, values, asJson, cwd, where) {
|
|
|
261
277
|
quiet: asJson,
|
|
262
278
|
live: Boolean(process.stderr.isTTY),
|
|
263
279
|
});
|
|
280
|
+
const record = values.events ? agentEventLog(resolve(cwd, values.events)) : undefined;
|
|
281
|
+
const onEvent = (event) => {
|
|
282
|
+
narrator.handle(event);
|
|
283
|
+
record?.(event);
|
|
284
|
+
};
|
|
264
285
|
// Ctrl-C asks the run to stop rather than killing the process, so the turn
|
|
265
|
-
// still lands on disk and the session stays resumable.
|
|
286
|
+
// still lands on disk and the session stays resumable. SIGTERM too: it is
|
|
287
|
+
// how a tuning stops a run it left behind, and its containers go with it.
|
|
266
288
|
const stopping = new AbortController();
|
|
267
289
|
const onInterrupt = () => stopping.abort();
|
|
268
290
|
process.once('SIGINT', onInterrupt);
|
|
291
|
+
process.once('SIGTERM', onInterrupt);
|
|
269
292
|
if (!asJson) {
|
|
270
293
|
// Two questions were just answered, possibly without being asked. Say
|
|
271
294
|
// which way they went: a run that quietly resumed the wrong session, or
|
|
@@ -278,12 +301,23 @@ async function once(engine, input, values, asJson, cwd, where) {
|
|
|
278
301
|
}
|
|
279
302
|
let outcome;
|
|
280
303
|
try {
|
|
281
|
-
outcome = await Engine.run(engine, input,
|
|
304
|
+
outcome = await Engine.run(engine, input, onEvent, stopping.signal);
|
|
282
305
|
}
|
|
283
306
|
finally {
|
|
284
307
|
narrator.done();
|
|
285
308
|
process.off('SIGINT', onInterrupt);
|
|
309
|
+
process.off('SIGTERM', onInterrupt);
|
|
286
310
|
}
|
|
311
|
+
appendUsage(where.project.dir, {
|
|
312
|
+
kind: 'run',
|
|
313
|
+
ts: new Date().toISOString(),
|
|
314
|
+
runDir: outcome.run.dir,
|
|
315
|
+
session: engine.session.id,
|
|
316
|
+
ok: true,
|
|
317
|
+
stopReason: outcome.result.stopReason,
|
|
318
|
+
durationMs: outcome.durationMs,
|
|
319
|
+
models: usageByModel(outcome.result.state.trajectory),
|
|
320
|
+
});
|
|
287
321
|
if (asJson) {
|
|
288
322
|
// --out is a destination, not a copy, and with --json the answer *is*
|
|
289
323
|
// the envelope — so that is what lands in the file, not the prose.
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { readProjectConfig, sandboxLimits, } from '@zenera/neo';
|
|
2
2
|
import { parse } from "../args.js";
|
|
3
3
|
import { resolveBuild } from "../image.js";
|
|
4
|
-
import { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, } from "../podman.js";
|
|
4
|
+
import { engineDisk, ensurePodmanReady, idleContainers, ownedContainers, podmanStatus, removeContainers, sessionOwners, } from "../podman.js";
|
|
5
5
|
import { current as currentProject, dirSize, isProjectDir, open as openProject, Registry, sessionIds, } from "../projects.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { layout, ResourceSampler } from "../resources.js";
|
|
7
|
+
import { ago, bold, bytes, count, cyan, dim, green, json, note, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
8
|
+
const USAGE = 'zen sandbox [status|top|up|pull|clean|disk] [options]';
|
|
9
|
+
const SUBCOMMANDS = ['status', 'top', 'up', 'pull', 'clean', 'disk'];
|
|
10
|
+
/** `top` refreshes this often unless told otherwise. */
|
|
11
|
+
const DEFAULT_INTERVAL_S = 2;
|
|
12
|
+
/** A one-shot report still needs two readings for a cpu figure. */
|
|
13
|
+
const ONCE_INTERVAL_MS = 1000;
|
|
8
14
|
/** Enough to see the pattern; the rest are a number. */
|
|
9
15
|
const LISTED = 6;
|
|
10
16
|
/** Under the labels, which is where the eye already is. */
|
|
11
17
|
const INDENT = ' '.repeat(11);
|
|
12
18
|
/** `agents.yaml` states memory in MiB; `bytes` prints bytes. */
|
|
13
19
|
const MIB = 1024 * 1024;
|
|
20
|
+
/** Below this many free locks, `status` says so: a batch can spend that in minutes. */
|
|
21
|
+
const FEW_LOCKS = 256;
|
|
14
22
|
// ---------------------------------------------------------------------------
|
|
15
23
|
// The container engine, on its own
|
|
16
24
|
//
|
|
@@ -26,6 +34,8 @@ export const sandbox = {
|
|
|
26
34
|
banner: { head: 'Zenera', accent: 'Sandbox', subtitle: 'Container Isolation', hue: 'azure' },
|
|
27
35
|
details: [
|
|
28
36
|
' status What is installed, running and pulled. Changes nothing.',
|
|
37
|
+
' top Memory and cpu, live: this computer, the machine, each',
|
|
38
|
+
' container — with a note on what every figure means.',
|
|
29
39
|
' up Install if asked, start the machine, pull or build the image.',
|
|
30
40
|
' pull Just the image: pulled, or built from the project\u2019s Dockerfile.',
|
|
31
41
|
' clean Remove every container this CLI created.',
|
|
@@ -33,6 +43,12 @@ export const sandbox = {
|
|
|
33
43
|
'',
|
|
34
44
|
' --project <name|dir> Which project the image and containers belong to.',
|
|
35
45
|
' --image <ref> Use this image instead of the project\u2019s.',
|
|
46
|
+
' --idle With clean: only what no live run is using — stopped ones,',
|
|
47
|
+
' and running ones whose run was killed before it cleaned up.',
|
|
48
|
+
' --once With top: measure once and print, no live screen. Also',
|
|
49
|
+
' what happens with --json or when output is not a terminal.',
|
|
50
|
+
' --interval <seconds> With top: how often the live screen refreshes (default 2).',
|
|
51
|
+
' --brief With top: the figures without the notes explaining them.',
|
|
36
52
|
'',
|
|
37
53
|
'Without --project this is the project you are standing in, and standing',
|
|
38
54
|
'nowhere is an answer: `status` then reports the engine and every container',
|
|
@@ -45,9 +61,15 @@ export const sandbox = {
|
|
|
45
61
|
const { values, positionals } = parse(ctx.args, {
|
|
46
62
|
project: { type: 'string' },
|
|
47
63
|
image: { type: 'string' },
|
|
64
|
+
stopped: { type: 'boolean' },
|
|
65
|
+
idle: { type: 'boolean' },
|
|
66
|
+
once: { type: 'boolean' },
|
|
67
|
+
interval: { type: 'string' },
|
|
68
|
+
brief: { type: 'boolean' },
|
|
69
|
+
theme: { type: 'string' },
|
|
48
70
|
}, USAGE);
|
|
49
71
|
const what = positionals[0] ?? 'status';
|
|
50
|
-
if (!
|
|
72
|
+
if (!SUBCOMMANDS.includes(what)) {
|
|
51
73
|
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
52
74
|
}
|
|
53
75
|
if (positionals.length > 1) {
|
|
@@ -69,8 +91,10 @@ export const sandbox = {
|
|
|
69
91
|
throw usageError('no project here, so there is no image to pull', 'name one with --project, or an image with --image');
|
|
70
92
|
}
|
|
71
93
|
return up(image, build, true, ctx.json, true);
|
|
94
|
+
case 'top':
|
|
95
|
+
return top(values, ctx.json);
|
|
72
96
|
case 'clean':
|
|
73
|
-
return clean(ctx.json);
|
|
97
|
+
return clean(ctx.json, Boolean(values.idle || values.stopped));
|
|
74
98
|
case 'disk':
|
|
75
99
|
return disk(ctx.json);
|
|
76
100
|
}
|
|
@@ -116,6 +140,7 @@ async function status(project, image, asJson) {
|
|
|
116
140
|
// a report about one project must not list another's — or the faker's,
|
|
117
141
|
// which wears the same label and belongs to no project at all.
|
|
118
142
|
const containers = project ? ofProject(project.dir, all) : all;
|
|
143
|
+
const idle = all.length > 0 ? idleContainers(all, await sessionOwners()) : [];
|
|
119
144
|
if (asJson) {
|
|
120
145
|
json({
|
|
121
146
|
...found,
|
|
@@ -124,6 +149,7 @@ async function status(project, image, asJson) {
|
|
|
124
149
|
limits: project?.limits ?? null,
|
|
125
150
|
fits: fits(found.capacity?.memory, project?.limits?.memory) ?? null,
|
|
126
151
|
containers,
|
|
152
|
+
idle: idle.map((c) => c.name),
|
|
127
153
|
});
|
|
128
154
|
return;
|
|
129
155
|
}
|
|
@@ -153,11 +179,64 @@ async function status(project, image, asJson) {
|
|
|
153
179
|
if (build) {
|
|
154
180
|
write(`${bold('dockerfile')} ${dim(build.dockerfile)}`);
|
|
155
181
|
}
|
|
156
|
-
writeAll(
|
|
182
|
+
writeAll(lockLines(found.freeLocks, idle.length));
|
|
183
|
+
writeAll(containerLines(containers, idle, project?.name, all.length));
|
|
157
184
|
if (!found.installed || !found.ready) {
|
|
158
185
|
note('');
|
|
159
186
|
note(dim('run `zen sandbox up` to fix what can be fixed.'));
|
|
160
187
|
}
|
|
188
|
+
else {
|
|
189
|
+
note('');
|
|
190
|
+
note(dim('memory and cpu, live: zen sandbox top'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Memory and cpu
|
|
195
|
+
//
|
|
196
|
+
// A live screen when a person is watching, one report when not: the same
|
|
197
|
+
// layout either way, so what a script reads is what the screen showed.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
async function top(values, asJson) {
|
|
200
|
+
const interval = values.interval === undefined ? DEFAULT_INTERVAL_S : Number(values.interval);
|
|
201
|
+
if (!Number.isFinite(interval) || interval < 0.5 || interval > 3600) {
|
|
202
|
+
throw usageError(`--interval must be a number of seconds from 0.5 to 3600, not ${values.interval}`, USAGE);
|
|
203
|
+
}
|
|
204
|
+
const sampler = new ResourceSampler();
|
|
205
|
+
const live = !asJson && !values.once && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
|
|
206
|
+
if (live) {
|
|
207
|
+
const { watch } = await import("../tui/sandbox.js");
|
|
208
|
+
await watch({
|
|
209
|
+
sampler,
|
|
210
|
+
intervalMs: interval * 1000,
|
|
211
|
+
explain: !values.brief,
|
|
212
|
+
theme: values.theme,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// The first reading has nothing to compare against, so its cpu figures are
|
|
217
|
+
// lifetime averages; the second one is over a second of real time.
|
|
218
|
+
await sampler.next();
|
|
219
|
+
await new Promise((done) => setTimeout(done, ONCE_INTERVAL_MS));
|
|
220
|
+
const view = await sampler.next();
|
|
221
|
+
if (asJson) {
|
|
222
|
+
json(view);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const width = process.stdout.columns || Number(process.env.COLUMNS) || 100;
|
|
226
|
+
writeAll(layout(view, { width, explain: !values.brief, sort: 'memory', wrapNotes: true }).map(ansi));
|
|
227
|
+
}
|
|
228
|
+
const PAINT = {
|
|
229
|
+
plain: (s) => s,
|
|
230
|
+
strong: bold,
|
|
231
|
+
heading: (s) => bold(cyan(s)),
|
|
232
|
+
dim,
|
|
233
|
+
good: green,
|
|
234
|
+
warn: yellow,
|
|
235
|
+
bad: red,
|
|
236
|
+
accent: cyan,
|
|
237
|
+
};
|
|
238
|
+
function ansi(line) {
|
|
239
|
+
return line.map((s) => PAINT[s.tone ?? 'plain'](s.text)).join('');
|
|
161
240
|
}
|
|
162
241
|
/**
|
|
163
242
|
* The hungriest container this project can start. An agent may override the
|
|
@@ -225,6 +304,22 @@ function ofProject(dir, containers) {
|
|
|
225
304
|
const sessions = new Set(sessionIds(dir));
|
|
226
305
|
return containers.filter((c) => c.key !== undefined && sessions.has(c.key));
|
|
227
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Every container the engine holds takes one of its locks, stopped or not, and
|
|
309
|
+
* at zero nothing can be created — for any project. Said only when it is close.
|
|
310
|
+
*/
|
|
311
|
+
function lockLines(free, idle) {
|
|
312
|
+
if (free === undefined || free >= FEW_LOCKS) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const left = free === 0 ? red('none free') : yellow(`${free} free`);
|
|
316
|
+
return [
|
|
317
|
+
`${bold('locks')} ${left} ${dim('· one per container, stopped ones included')}`,
|
|
318
|
+
...(idle > 0
|
|
319
|
+
? [`${INDENT}${dim(`${idle} no live run is using — zen sandbox clean --idle`)}`]
|
|
320
|
+
: []),
|
|
321
|
+
];
|
|
322
|
+
}
|
|
228
323
|
/**
|
|
229
324
|
* One per line rather than one long line, because there is normally more than
|
|
230
325
|
* one and the interesting part — how old, and whether anything is still up —
|
|
@@ -235,21 +330,31 @@ function ofProject(dir, containers) {
|
|
|
235
330
|
* stopped ones behind. `scope` names the project these belong to, and saying
|
|
236
331
|
* so is half the answer — the other half is that there are more elsewhere.
|
|
237
332
|
*/
|
|
238
|
-
function containerLines(containers, scope) {
|
|
333
|
+
function containerLines(containers, idle, scope, total = containers.length) {
|
|
239
334
|
const tail = scope
|
|
240
335
|
? `one per session in ${scope}, kept by \`persist: true\` — all of them: zen sandbox disk`
|
|
241
336
|
: 'one per session, kept by `persist: true` — see: zen sandbox disk';
|
|
337
|
+
const elsewhere = total > containers.length ? ` · ${total} on the machine` : '';
|
|
242
338
|
if (containers.length === 0) {
|
|
243
|
-
return [`${bold('containers')} ${dim(scope ? `none in ${scope}` : 'none')}`];
|
|
339
|
+
return [`${bold('containers')} ${dim(scope ? `none in ${scope}${elsewhere}` : 'none')}`];
|
|
244
340
|
}
|
|
341
|
+
// Running, but the run that started it is gone: nothing will ever stop it.
|
|
342
|
+
const orphans = new Set(idle.filter((c) => c.state === 'running').map((c) => c.name));
|
|
245
343
|
const running = containers.filter((c) => c.state === 'running').length;
|
|
246
|
-
const
|
|
344
|
+
const lost = containers.filter((c) => orphans.has(c.name)).length;
|
|
345
|
+
const head = `${bold('containers')} ${containers.length} ${dim((running ? `· ${running} running` : '· none running') +
|
|
346
|
+
(lost ? `, ${lost} of them orphaned` : '') +
|
|
347
|
+
elsewhere)}`;
|
|
247
348
|
const rows = containers
|
|
248
349
|
.slice(0, LISTED)
|
|
249
350
|
.map((c) => [
|
|
250
351
|
INDENT.slice(2),
|
|
251
352
|
c.name,
|
|
252
|
-
|
|
353
|
+
orphans.has(c.name)
|
|
354
|
+
? yellow('orphaned')
|
|
355
|
+
: c.state === 'running'
|
|
356
|
+
? green('running')
|
|
357
|
+
: dim(c.state),
|
|
253
358
|
dim(ago(c.createdAt)),
|
|
254
359
|
]);
|
|
255
360
|
const rest = containers.length - LISTED;
|
|
@@ -258,6 +363,9 @@ function containerLines(containers, scope) {
|
|
|
258
363
|
...table(rows),
|
|
259
364
|
...(rest > 0 ? [`${INDENT}${dim(`+${rest} more`)}`] : []),
|
|
260
365
|
`${INDENT}${dim(tail)}`,
|
|
366
|
+
...(lost
|
|
367
|
+
? [`${INDENT}${dim('orphaned: its run was killed — zen sandbox clean --idle')}`]
|
|
368
|
+
: []),
|
|
261
369
|
];
|
|
262
370
|
}
|
|
263
371
|
async function up(image, build, yes, asJson, rebuild = false) {
|
|
@@ -268,8 +376,9 @@ async function up(image, build, yes, asJson, rebuild = false) {
|
|
|
268
376
|
}
|
|
269
377
|
write(`${green('ready')}${image ? ` ${dim(image)}` : ''}`);
|
|
270
378
|
}
|
|
271
|
-
async function clean(asJson) {
|
|
272
|
-
const
|
|
379
|
+
async function clean(asJson, idle) {
|
|
380
|
+
const owned = await ownedContainers(undefined, undefined, { sizes: true });
|
|
381
|
+
const containers = idle ? idleContainers(owned, await sessionOwners()) : owned;
|
|
273
382
|
const names = containers.map((c) => c.name);
|
|
274
383
|
const freed = containers.reduce((n, c) => n + (c.size ?? 0), 0);
|
|
275
384
|
await removeContainers(names);
|
|
@@ -307,10 +416,30 @@ async function disk(asJson) {
|
|
|
307
416
|
write('');
|
|
308
417
|
}
|
|
309
418
|
writeAll(projectRows(projects, loose));
|
|
310
|
-
|
|
419
|
+
const idle = idleContainers(containers, await sessionOwners());
|
|
420
|
+
const live = containers.length - idle.length;
|
|
421
|
+
const sessions = projects.reduce((n, p) => n + p.files, 0);
|
|
422
|
+
const hints = [
|
|
423
|
+
...(idle.length > 0
|
|
424
|
+
? [`zen sandbox clean --idle ${count(idle.length, 'container')} no live run is using`]
|
|
425
|
+
: []),
|
|
426
|
+
...(live > 0
|
|
427
|
+
? [`zen sandbox clean also the ${live} in use, cutting off whatever runs there`]
|
|
428
|
+
: []),
|
|
429
|
+
...(usage && usage.images.reclaimable > 0
|
|
430
|
+
? [`podman image prune -a ${bytes(usage.images.reclaimable)} of unused images`]
|
|
431
|
+
: []),
|
|
432
|
+
// The ON DISK column is the bulk of the report and no command above touches it.
|
|
433
|
+
...(sessions > 0
|
|
434
|
+
? [
|
|
435
|
+
`${bytes(sessions)} ON DISK is project directories on this host, not the engine: ` +
|
|
436
|
+
'removing containers frees none of it',
|
|
437
|
+
]
|
|
438
|
+
: []),
|
|
439
|
+
];
|
|
440
|
+
if (hints.length > 0) {
|
|
311
441
|
write('');
|
|
312
|
-
|
|
313
|
-
write(dim(`podman image prune -a ${bytes(usage.images.reclaimable)} of unused images`));
|
|
442
|
+
writeAll(hints.map((h) => dim(h)));
|
|
314
443
|
}
|
|
315
444
|
}
|
|
316
445
|
function engineRows(usage) {
|
|
@@ -382,13 +511,16 @@ function projectRows(projects, loose) {
|
|
|
382
511
|
// Containers whose session directory is gone, and faker's, which are
|
|
383
512
|
// labelled the same way and belong to no project at all.
|
|
384
513
|
const size = loose.reduce((n, c) => n + (c.size ?? 0), 0);
|
|
514
|
+
const fakers = loose.filter((c) => c.key?.startsWith('faker-')).length;
|
|
385
515
|
rows.push([
|
|
386
516
|
dim('(unclaimed)'),
|
|
387
517
|
dim('—'),
|
|
388
518
|
dim('—'),
|
|
389
519
|
dim(String(loose.length)),
|
|
390
520
|
dim(bytes(size)),
|
|
391
|
-
dim(
|
|
521
|
+
dim(fakers === loose.length
|
|
522
|
+
? `zen faker ${fakers === 1 ? 'server' : 'servers'}, no project`
|
|
523
|
+
: 'no session owns these'),
|
|
392
524
|
]);
|
|
393
525
|
}
|
|
394
526
|
const total = (pick) => projects.reduce((n, p) => n + pick(p), 0);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command } from '../command.ts';
|
|
2
|
+
/** A file inside the cwd is served from the cwd, so its links up the tree still resolve. */
|
|
3
|
+
export declare function placeOf(cwd: string, target: string): {
|
|
4
|
+
root: string;
|
|
5
|
+
start: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const view: Command;
|
|
8
|
+
//# sourceMappingURL=view.d.ts.map
|