@zenera/cli 1.1.25 → 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 +13 -1
- package/dist/catalog.d.ts +20 -0
- package/dist/catalog.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.js +54 -14
- package/dist/commands/memory.js +28 -3
- package/dist/commands/meta.js +24 -5
- package/dist/commands/models.js +2 -1
- package/dist/commands/sandbox.js +73 -3
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +6 -0
- package/dist/eventlog.d.ts +5 -0
- package/dist/eventlog.js +25 -1
- package/dist/liveness.js +20 -2
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +1 -1
- package/dist/meta/editor.js +4 -21
- 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/command.d.ts +1 -1
- package/dist/meta/finetune/command.js +224 -46
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/types.d.ts +3 -1
- package/dist/meta/finetune/feedback.d.ts +14 -2
- package/dist/meta/finetune/feedback.js +22 -3
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +50 -3
- package/dist/meta/finetune/improvements.js +290 -33
- package/dist/meta/finetune/live.d.ts +17 -7
- package/dist/meta/finetune/live.js +56 -10
- package/dist/meta/finetune/loop.js +59 -10
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +9 -2
- package/dist/meta/finetune/report.js +551 -438
- package/dist/meta/finetune/sampler.js +8 -2
- package/dist/meta/finetune/train.d.ts +6 -2
- package/dist/meta/finetune/train.js +55 -21
- package/dist/meta/finetune/tui.d.ts +3 -3
- package/dist/meta/finetune/tui.js +37 -12
- package/dist/meta/finetune/tuning.d.ts +27 -1
- package/dist/meta/finetune/tuning.js +21 -2
- package/dist/meta/finetune/usage.d.ts +4 -0
- package/dist/meta/finetune/usage.js +35 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +2 -1
- package/dist/meta/host.js +2 -1
- package/dist/meta.d.ts +291 -0
- package/dist/meta.js +981 -0
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/scaffold.d.ts +3 -4
- package/dist/scaffold.js +21 -9
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +54 -5
- 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 +3 -1
- package/dist/tui/models.js +65 -7
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +29 -43
- package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
- package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
- package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -493
- package/templates/project/agents/tools-instructions.md +7 -0
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { runProcess } from '@zenera/neo';
|
|
2
|
+
import { cpus as hostCpus, platform as hostPlatform, totalmem } from 'node:os';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { idleContainers, ownedContainers } from "./podman.js";
|
|
5
|
+
import { isProjectDir, Registry, sessionIds, sessionsDir } from "./projects.js";
|
|
6
|
+
import { ago, bytes } from "./term.js";
|
|
7
|
+
import { wrap } from "./tui/wrap.js";
|
|
8
|
+
const MIB = 1024 * 1024;
|
|
9
|
+
const GIB = 1024 * MIB;
|
|
10
|
+
/** Process ids, footprints and container names are all short; a listing of many is not. */
|
|
11
|
+
const LISTING_BYTES = 16 * MIB;
|
|
12
|
+
/** How long a project-to-session map is trusted before the registry is read again. */
|
|
13
|
+
const OWNERS_TTL_MS = 30_000;
|
|
14
|
+
/** A helper started this close to the hypervisor belongs to the same machine. */
|
|
15
|
+
const SAME_START_S = 120;
|
|
16
|
+
/** What starts the machine, by executable name. */
|
|
17
|
+
const HYPERVISORS = new Set(['vfkit', 'krunkit', 'qemu-system-aarch64', 'qemu-system-x86_64']);
|
|
18
|
+
/** macOS runs a vfkit machine's memory in this XPC service, not in vfkit. */
|
|
19
|
+
const APPLE_VM = 'com.apple.Virtualization.VirtualMachine';
|
|
20
|
+
const NETWORK = new Set(['gvproxy']);
|
|
21
|
+
/** Reads one sample at a time and remembers the last, so each view has an interval. */
|
|
22
|
+
export class ResourceSampler {
|
|
23
|
+
engine;
|
|
24
|
+
#exec;
|
|
25
|
+
#platform;
|
|
26
|
+
#owners;
|
|
27
|
+
#now;
|
|
28
|
+
#previous;
|
|
29
|
+
#machine;
|
|
30
|
+
#cached;
|
|
31
|
+
constructor(opts = {}) {
|
|
32
|
+
this.engine = opts.engine ?? 'podman';
|
|
33
|
+
this.#exec = opts.exec ?? runProcess;
|
|
34
|
+
this.#platform = opts.platform ?? hostPlatform();
|
|
35
|
+
this.#owners = opts.owners ?? registryOwners;
|
|
36
|
+
this.#now = opts.now ?? Date.now;
|
|
37
|
+
}
|
|
38
|
+
async next() {
|
|
39
|
+
const sample = await this.sample();
|
|
40
|
+
const view = viewOf(sample, this.#previous);
|
|
41
|
+
this.#previous = sample;
|
|
42
|
+
return view;
|
|
43
|
+
}
|
|
44
|
+
async sample() {
|
|
45
|
+
const at = this.#now();
|
|
46
|
+
const problems = [];
|
|
47
|
+
const native = this.#platform === 'linux';
|
|
48
|
+
const [guest, stats, owned, processes, machine, owners] = await Promise.all([
|
|
49
|
+
guestMemory(this.engine, this.#exec),
|
|
50
|
+
containerStats(this.engine, this.#exec),
|
|
51
|
+
ownedContainers(this.engine, this.#exec),
|
|
52
|
+
this.#platform === 'darwin' ? hostProcesses(this.#exec) : Promise.resolve([]),
|
|
53
|
+
native ? Promise.resolve(undefined) : this.#machineName(),
|
|
54
|
+
this.#ownersCached(at),
|
|
55
|
+
]);
|
|
56
|
+
if (!guest) {
|
|
57
|
+
problems.push(native
|
|
58
|
+
? `${this.engine} did not answer — is it installed? zen sandbox up`
|
|
59
|
+
: `${this.engine} did not answer — is the machine running? zen sandbox up`);
|
|
60
|
+
}
|
|
61
|
+
if (stats === undefined && owned.some((c) => c.state === 'running')) {
|
|
62
|
+
problems.push(`${this.engine} stats did not answer, so no container figures`);
|
|
63
|
+
}
|
|
64
|
+
const orphans = idleContainers(owned, owners.dirs)
|
|
65
|
+
.filter((c) => c.state === 'running')
|
|
66
|
+
.map((c) => c.name);
|
|
67
|
+
return {
|
|
68
|
+
at,
|
|
69
|
+
platform: this.#platform,
|
|
70
|
+
engine: this.engine,
|
|
71
|
+
ready: guest !== undefined,
|
|
72
|
+
machine,
|
|
73
|
+
host: { memory: totalmem(), cpus: hostCpus().length, processes },
|
|
74
|
+
guest,
|
|
75
|
+
stats: stats ?? [],
|
|
76
|
+
owned,
|
|
77
|
+
orphans,
|
|
78
|
+
projects: owners.names,
|
|
79
|
+
problems,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
#machineName() {
|
|
83
|
+
this.#machine ??= this.#exec(this.engine, ['machine', 'list', '--format', 'json'], {
|
|
84
|
+
timeoutMs: 30_000,
|
|
85
|
+
})
|
|
86
|
+
.then((res) => {
|
|
87
|
+
if (res.code !== 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const all = JSON.parse(res.stdout.trim() || '[]');
|
|
91
|
+
return (all.find((m) => m.Default) ?? all[0])?.Name;
|
|
92
|
+
})
|
|
93
|
+
.catch(() => undefined);
|
|
94
|
+
return this.#machine;
|
|
95
|
+
}
|
|
96
|
+
async #ownersCached(at) {
|
|
97
|
+
if (this.#cached && at - this.#cached.at < OWNERS_TTL_MS) {
|
|
98
|
+
return this.#cached.owners;
|
|
99
|
+
}
|
|
100
|
+
const owners = await this.#owners().catch(() => ({
|
|
101
|
+
dirs: new Map(),
|
|
102
|
+
names: new Map(),
|
|
103
|
+
}));
|
|
104
|
+
this.#cached = { at, owners };
|
|
105
|
+
return owners;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Every session on this machine, by id: where it lives and which project it is in. */
|
|
109
|
+
export async function registryOwners() {
|
|
110
|
+
const dirs = new Map();
|
|
111
|
+
const names = new Map();
|
|
112
|
+
for (const entry of (await Registry.open()).entries) {
|
|
113
|
+
if (isProjectDir(entry.path)) {
|
|
114
|
+
for (const id of sessionIds(entry.path)) {
|
|
115
|
+
dirs.set(id, join(sessionsDir(entry.path), id));
|
|
116
|
+
names.set(id, entry.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { dirs, names };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Reading each layer
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
async function guestMemory(engine, exec) {
|
|
126
|
+
const res = await exec(engine, ['info', '--format', 'json'], { timeoutMs: 30_000 }).catch(() => undefined);
|
|
127
|
+
if (!res || res.code !== 0) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return parseInfo(res.stdout);
|
|
131
|
+
}
|
|
132
|
+
export function parseInfo(stdout) {
|
|
133
|
+
let host;
|
|
134
|
+
try {
|
|
135
|
+
host = JSON.parse(stdout).host;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const num = (key) => {
|
|
141
|
+
const v = host?.[key];
|
|
142
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
143
|
+
};
|
|
144
|
+
const total = num('memTotal');
|
|
145
|
+
if (total <= 0) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const free = num('memFree');
|
|
149
|
+
// An older podman reports no `memAvailable`; free alone undercounts, but
|
|
150
|
+
// it never claims room that is not there.
|
|
151
|
+
const available = num('memAvailable') || free;
|
|
152
|
+
return {
|
|
153
|
+
total,
|
|
154
|
+
free,
|
|
155
|
+
available,
|
|
156
|
+
swapTotal: num('swapTotal'),
|
|
157
|
+
swapFree: num('swapFree'),
|
|
158
|
+
cpus: num('cpus'),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Every running container, not only zen's: what the machine is short of is
|
|
163
|
+
* shared by everything in it, and an unlabelled container eats from the same
|
|
164
|
+
* memory a session needs. The raw `ContainerStats` rather than podman's own
|
|
165
|
+
* columns, which are strings like `2.5MB / 4.3GB` rounded for a person.
|
|
166
|
+
*/
|
|
167
|
+
async function containerStats(engine, exec) {
|
|
168
|
+
const res = await exec(engine, ['stats', '--no-stream', '--no-reset', '--format', '{{json .ContainerStats}}'], { timeoutMs: 30_000, maxBytes: LISTING_BYTES }).catch(() => undefined);
|
|
169
|
+
if (!res || res.code !== 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return parseStats(res.stdout);
|
|
173
|
+
}
|
|
174
|
+
export function parseStats(stdout) {
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const line of stdout.split('\n')) {
|
|
177
|
+
const text = line.trim();
|
|
178
|
+
if (!text.startsWith('{')) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = JSON.parse(text);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!raw.Name) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const nets = Object.values(raw.Network ?? {});
|
|
192
|
+
out.push({
|
|
193
|
+
id: (raw.ContainerID ?? '').slice(0, 12),
|
|
194
|
+
name: raw.Name,
|
|
195
|
+
cpuNano: raw.CPUNano ?? 0,
|
|
196
|
+
systemNano: raw.SystemNano ?? 0,
|
|
197
|
+
avgCpu: raw.AvgCPU ?? raw.CPU ?? 0,
|
|
198
|
+
memory: raw.MemUsage ?? 0,
|
|
199
|
+
limit: raw.MemLimit ?? 0,
|
|
200
|
+
pids: raw.PIDs ?? 0,
|
|
201
|
+
netIn: nets.reduce((n, i) => n + (i.RxBytes ?? 0), 0),
|
|
202
|
+
netOut: nets.reduce((n, i) => n + (i.TxBytes ?? 0), 0),
|
|
203
|
+
blockIn: raw.BlockInput ?? 0,
|
|
204
|
+
blockOut: raw.BlockOutput ?? 0,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The processes on this Mac that make up the machine. `ps` says how much cpu
|
|
211
|
+
* each has burned and what is in RAM; `footprint` says what Activity Monitor
|
|
212
|
+
* says, which is the number people come here about — it counts the pages
|
|
213
|
+
* macOS compressed or swapped too, and for a virtual machine that is nearly
|
|
214
|
+
* all of it.
|
|
215
|
+
*/
|
|
216
|
+
async function hostProcesses(exec) {
|
|
217
|
+
const res = await exec('ps', ['-axww', '-o', 'pid=,rss=,time=,etime=,comm='], {
|
|
218
|
+
timeoutMs: 10_000,
|
|
219
|
+
maxBytes: LISTING_BYTES,
|
|
220
|
+
}).catch(() => undefined);
|
|
221
|
+
if (!res || res.code !== 0) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
const found = machineProcesses(parsePs(res.stdout));
|
|
225
|
+
if (found.length === 0) {
|
|
226
|
+
return found;
|
|
227
|
+
}
|
|
228
|
+
const args = ['-f', 'bytes', '--noCategories', ...found.flatMap((p) => ['-p', String(p.pid)])];
|
|
229
|
+
const fp = await exec('footprint', args, { timeoutMs: 10_000 }).catch(() => undefined);
|
|
230
|
+
const sizes = fp && fp.code === 0 ? parseFootprint(fp.stdout) : new Map();
|
|
231
|
+
return found.map((p) => {
|
|
232
|
+
const size = sizes.get(p.pid);
|
|
233
|
+
return size ? { ...p, footprint: size.footprint, peak: size.peak } : p;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export function parsePs(stdout) {
|
|
237
|
+
const rows = [];
|
|
238
|
+
for (const line of stdout.split('\n')) {
|
|
239
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
240
|
+
if (!m) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
rows.push({
|
|
244
|
+
pid: Number(m[1]),
|
|
245
|
+
rss: Number(m[2]) * 1024,
|
|
246
|
+
cpuSeconds: clock(m[3]),
|
|
247
|
+
ageSeconds: clock(m[4]),
|
|
248
|
+
command: m[5],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return rows;
|
|
252
|
+
}
|
|
253
|
+
/** `[[dd-]hh:]mm:ss[.cc]` — how `ps` prints both cpu time and elapsed time. */
|
|
254
|
+
export function clock(text) {
|
|
255
|
+
const [days, rest] = text.includes('-') ? text.split('-', 2) : ['0', text];
|
|
256
|
+
const parts = (rest ?? '').split(':').map(Number).reverse();
|
|
257
|
+
if (parts.some((n) => !Number.isFinite(n))) {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
const [s = 0, m = 0, h = 0] = parts;
|
|
261
|
+
return Number(days) * 86400 + h * 3600 + m * 60 + s;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Picks the machine out of every process on the host. A Mac can run more than
|
|
265
|
+
* one virtual machine through the same framework, so the Apple service and
|
|
266
|
+
* the network helper are matched to the hypervisor by when they started.
|
|
267
|
+
*/
|
|
268
|
+
export function machineProcesses(rows) {
|
|
269
|
+
const named = (row) => basename(row.command);
|
|
270
|
+
const near = (a, b) => Math.abs(a.ageSeconds - b.ageSeconds) <= SAME_START_S;
|
|
271
|
+
const closest = (pool, to) => pool
|
|
272
|
+
.filter((r) => near(r, to))
|
|
273
|
+
.sort((a, b) => Math.abs(a.ageSeconds - to.ageSeconds) - Math.abs(b.ageSeconds - to.ageSeconds))[0];
|
|
274
|
+
const out = [];
|
|
275
|
+
const apple = rows.filter((r) => named(r) === APPLE_VM);
|
|
276
|
+
const helpers = rows.filter((r) => NETWORK.has(named(r)));
|
|
277
|
+
for (const hv of rows.filter((r) => HYPERVISORS.has(named(r)))) {
|
|
278
|
+
const name = named(hv);
|
|
279
|
+
const vm = name === 'vfkit' ? closest(apple, hv) : undefined;
|
|
280
|
+
const as = (row, role, label) => ({
|
|
281
|
+
pid: row.pid,
|
|
282
|
+
role,
|
|
283
|
+
name: label,
|
|
284
|
+
rss: row.rss,
|
|
285
|
+
cpuSeconds: row.cpuSeconds,
|
|
286
|
+
});
|
|
287
|
+
if (vm) {
|
|
288
|
+
out.push(as(vm, 'vm', `Virtual Machine Service for ${name}`));
|
|
289
|
+
out.push(as(hv, 'hypervisor', name));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// krunkit and qemu hold the guest's memory in their own process.
|
|
293
|
+
out.push(as(hv, 'vm', name));
|
|
294
|
+
}
|
|
295
|
+
const net = closest(helpers, hv);
|
|
296
|
+
if (net) {
|
|
297
|
+
out.push(as(net, 'network', named(net)));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
/** `footprint -f bytes` output: one block per process. */
|
|
303
|
+
export function parseFootprint(stdout) {
|
|
304
|
+
const out = new Map();
|
|
305
|
+
let pid;
|
|
306
|
+
for (const line of stdout.split('\n')) {
|
|
307
|
+
const head = /\[(\d+)\]:.*Footprint:\s*(\d+)\s*B/.exec(line);
|
|
308
|
+
if (head) {
|
|
309
|
+
pid = Number(head[1]);
|
|
310
|
+
out.set(pid, { footprint: Number(head[2]) });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const entry = pid === undefined ? undefined : out.get(pid);
|
|
314
|
+
const exact = /^\s*phys_footprint:\s*(\d+)\s*B/.exec(line);
|
|
315
|
+
if (entry && exact) {
|
|
316
|
+
entry.footprint = Number(exact[1]);
|
|
317
|
+
}
|
|
318
|
+
const peak = /^\s*phys_footprint_peak:\s*(\d+)\s*B/.exec(line);
|
|
319
|
+
if (entry && peak) {
|
|
320
|
+
entry.peak = Number(peak[1]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// From samples to a view
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
export function viewOf(now, before) {
|
|
329
|
+
const seconds = before ? (now.at - before.at) / 1000 : undefined;
|
|
330
|
+
const processes = now.host.processes.map((p) => {
|
|
331
|
+
const prior = before?.host.processes.find((q) => q.pid === p.pid);
|
|
332
|
+
const cpu = prior && seconds && seconds > 0
|
|
333
|
+
? Math.max(0, ((p.cpuSeconds - prior.cpuSeconds) / seconds) * 100)
|
|
334
|
+
: undefined;
|
|
335
|
+
return {
|
|
336
|
+
pid: p.pid,
|
|
337
|
+
role: p.role,
|
|
338
|
+
name: p.name,
|
|
339
|
+
memory: p.footprint ?? p.rss,
|
|
340
|
+
rss: p.rss,
|
|
341
|
+
footprint: p.footprint,
|
|
342
|
+
peak: p.peak,
|
|
343
|
+
cpu,
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
const vm = processes.find((p) => p.role === 'vm');
|
|
347
|
+
const machine = now.guest ? machineOf(now.guest, now.machine, vm) : undefined;
|
|
348
|
+
const owned = new Map(now.owned.map((c) => [c.name, c]));
|
|
349
|
+
const orphans = new Set(now.orphans);
|
|
350
|
+
const containers = now.stats.map((s) => {
|
|
351
|
+
const own = owned.get(s.name);
|
|
352
|
+
const prior = before?.stats.find((p) => p.id === s.id);
|
|
353
|
+
const span = prior ? s.systemNano - prior.systemNano : 0;
|
|
354
|
+
const measured = prior !== undefined && span > 0;
|
|
355
|
+
const kind = !own ? 'other' : own.key?.startsWith('faker-') ? 'faker' : 'zen';
|
|
356
|
+
return {
|
|
357
|
+
name: s.name,
|
|
358
|
+
id: s.id,
|
|
359
|
+
kind,
|
|
360
|
+
project: own?.key ? now.projects.get(own.key) : undefined,
|
|
361
|
+
orphaned: orphans.has(s.name),
|
|
362
|
+
cpu: measured ? Math.max(0, ((s.cpuNano - prior.cpuNano) / span) * 100) : s.avgCpu,
|
|
363
|
+
cpuOver: measured ? 'interval' : 'lifetime',
|
|
364
|
+
memory: s.memory,
|
|
365
|
+
// A container with no cap reports the whole machine as its limit.
|
|
366
|
+
limit: s.limit > 0 && (!now.guest || s.limit < now.guest.total * 0.99)
|
|
367
|
+
? s.limit
|
|
368
|
+
: undefined,
|
|
369
|
+
pids: s.pids,
|
|
370
|
+
netIn: s.netIn,
|
|
371
|
+
netOut: s.netOut,
|
|
372
|
+
blockIn: s.blockIn,
|
|
373
|
+
blockOut: s.blockOut,
|
|
374
|
+
createdAt: own?.createdAt,
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
const running = new Set(containers.map((c) => c.name));
|
|
378
|
+
const view = {
|
|
379
|
+
at: new Date(now.at).toISOString(),
|
|
380
|
+
interval: seconds,
|
|
381
|
+
platform: now.platform,
|
|
382
|
+
engine: now.engine,
|
|
383
|
+
ready: now.ready,
|
|
384
|
+
host: {
|
|
385
|
+
memory: now.host.memory,
|
|
386
|
+
cpus: now.host.cpus,
|
|
387
|
+
vm,
|
|
388
|
+
helpers: processes.filter((p) => p !== vm),
|
|
389
|
+
},
|
|
390
|
+
machine,
|
|
391
|
+
containers,
|
|
392
|
+
stopped: now.owned.filter((c) => !running.has(c.name) && c.state !== 'running').length,
|
|
393
|
+
totals: {
|
|
394
|
+
running: containers.length,
|
|
395
|
+
zen: containers.filter((c) => c.kind === 'zen').length,
|
|
396
|
+
faker: containers.filter((c) => c.kind === 'faker').length,
|
|
397
|
+
other: containers.filter((c) => c.kind === 'other').length,
|
|
398
|
+
orphaned: containers.filter((c) => c.orphaned).length,
|
|
399
|
+
memory: containers.reduce((n, c) => n + c.memory, 0),
|
|
400
|
+
cpu: containers.reduce((n, c) => n + c.cpu, 0),
|
|
401
|
+
},
|
|
402
|
+
hints: [],
|
|
403
|
+
problems: [...now.problems],
|
|
404
|
+
};
|
|
405
|
+
view.hints = hintsOf(view);
|
|
406
|
+
return view;
|
|
407
|
+
}
|
|
408
|
+
function machineOf(g, name, vm) {
|
|
409
|
+
const used = Math.max(0, g.total - g.available);
|
|
410
|
+
const cache = Math.max(0, g.available - g.free);
|
|
411
|
+
const touched = g.total - g.free;
|
|
412
|
+
const held = vm?.footprint !== undefined ? Math.max(0, vm.footprint - touched) : undefined;
|
|
413
|
+
return {
|
|
414
|
+
name,
|
|
415
|
+
total: g.total,
|
|
416
|
+
used,
|
|
417
|
+
cache,
|
|
418
|
+
free: g.free,
|
|
419
|
+
available: g.available,
|
|
420
|
+
swapTotal: g.swapTotal,
|
|
421
|
+
swapUsed: Math.max(0, g.swapTotal - g.swapFree),
|
|
422
|
+
cpus: g.cpus,
|
|
423
|
+
held,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** What a person can do about what the view shows, most useful first. */
|
|
427
|
+
function hintsOf(v) {
|
|
428
|
+
const out = [];
|
|
429
|
+
const m = v.machine;
|
|
430
|
+
const vm = v.host.vm;
|
|
431
|
+
const lost = v.containers.filter((c) => c.orphaned);
|
|
432
|
+
if (lost.length > 0) {
|
|
433
|
+
out.push(`${lost.length === 1 ? '1 container is' : `${lost.length} containers are`} still running ` +
|
|
434
|
+
`for zen runs that are gone, holding ${bytes(lost.reduce((n, c) => n + c.memory, 0))}. ` +
|
|
435
|
+
'Remove them with: zen sandbox clean --idle');
|
|
436
|
+
}
|
|
437
|
+
if (m && m.available < m.total * 0.15) {
|
|
438
|
+
out.push(`The machine is nearly out of memory: ${bytes(m.available)} left of ${bytes(m.total)}. ` +
|
|
439
|
+
'Commands may be killed with exit 137. Stop what you do not need, or give the machine more memory.');
|
|
440
|
+
}
|
|
441
|
+
for (const c of v.containers) {
|
|
442
|
+
if (c.limit && c.memory > c.limit * 0.9) {
|
|
443
|
+
out.push(`${c.name} is using ${pct((c.memory / c.limit) * 100)} of its own limit (${bytes(c.limit)}). ` +
|
|
444
|
+
'Its next big command may be killed with exit 137; raise `sandbox.memory` in agents.yaml.');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (m && m.swapUsed > 64 * MIB) {
|
|
448
|
+
out.push(`The machine has moved ${bytes(m.swapUsed)} to disk (swap): it ran out of RAM at some point, ` +
|
|
449
|
+
'which slows everything inside it down.');
|
|
450
|
+
}
|
|
451
|
+
if (vm && m && vm.memory - m.used > GIB && vm.memory > m.used * 2) {
|
|
452
|
+
out.push(`macOS counts ${bytes(vm.memory)} for the machine, but programs inside use only ${bytes(m.used)}. ` +
|
|
453
|
+
'The rest is cache and memory used earlier that Linux does not hand back to macOS. ' +
|
|
454
|
+
'To get it back now: podman machine stop && podman machine start ' +
|
|
455
|
+
'(cuts off any command running in a session).');
|
|
456
|
+
}
|
|
457
|
+
if (vm && m && m.total > v.host.memory * 0.5) {
|
|
458
|
+
out.push(`The machine may grow to ${bytes(m.total)}, ${pct((m.total / v.host.memory) * 100)} of this computer's memory. ` +
|
|
459
|
+
'To cap it lower: podman machine stop; podman machine set --memory 8192; podman machine start');
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
export const SORTS = ['memory', 'cpu', 'name'];
|
|
464
|
+
const sp = (text, tone) => ({ text, tone });
|
|
465
|
+
/** Where a note starts: label, value and detail all fit before it. */
|
|
466
|
+
const NOTE_COLUMN = 48;
|
|
467
|
+
/** A note that does not fit beside its row goes under it, indented this far. */
|
|
468
|
+
const NOTE_INDENT = 6;
|
|
469
|
+
const LABEL = 14;
|
|
470
|
+
const VALUE = 9;
|
|
471
|
+
const BAR = 30;
|
|
472
|
+
export function pct(n) {
|
|
473
|
+
return `${n < 10 ? n.toFixed(1) : Math.round(n)}%`;
|
|
474
|
+
}
|
|
475
|
+
function pressure(fraction) {
|
|
476
|
+
return fraction >= 0.9 ? 'bad' : fraction >= 0.7 ? 'warn' : 'good';
|
|
477
|
+
}
|
|
478
|
+
/** One stacked bar: each part a run of its own character and tone. */
|
|
479
|
+
function bar(parts, width) {
|
|
480
|
+
let left = width;
|
|
481
|
+
const spans = [];
|
|
482
|
+
parts.forEach(([fraction, char, tone], i) => {
|
|
483
|
+
const n = i === parts.length - 1
|
|
484
|
+
? left
|
|
485
|
+
: Math.min(left, Math.round(Math.max(0, fraction) * width));
|
|
486
|
+
left -= n;
|
|
487
|
+
if (n > 0) {
|
|
488
|
+
spans.push(sp(char.repeat(n), tone));
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return spans;
|
|
492
|
+
}
|
|
493
|
+
/** Drops trailing padding, which a styled span cannot have trimmed afterwards. */
|
|
494
|
+
function finish(line) {
|
|
495
|
+
const last = line.at(-1);
|
|
496
|
+
if (last) {
|
|
497
|
+
line[line.length - 1] = sp(last.text.trimEnd(), last.tone);
|
|
498
|
+
}
|
|
499
|
+
return line.filter((s) => s.text !== '');
|
|
500
|
+
}
|
|
501
|
+
class Lines {
|
|
502
|
+
out = [];
|
|
503
|
+
#o;
|
|
504
|
+
constructor(o) {
|
|
505
|
+
this.#o = o;
|
|
506
|
+
}
|
|
507
|
+
blank() {
|
|
508
|
+
this.out.push([]);
|
|
509
|
+
}
|
|
510
|
+
push(...spans) {
|
|
511
|
+
this.out.push(finish(spans));
|
|
512
|
+
}
|
|
513
|
+
/** A heading in capitals, then what follows it on the same line, quieter. */
|
|
514
|
+
heading(title, rest) {
|
|
515
|
+
this.push(sp(title, 'heading'), sp(rest ? ` ${rest}` : '', 'dim'));
|
|
516
|
+
}
|
|
517
|
+
/** A sentence under a heading, wrapped, only when explaining. */
|
|
518
|
+
intro(text) {
|
|
519
|
+
if (!this.#o.explain || this.#o.intros === false) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
for (const line of wrap(text, Math.max(20, this.#o.width - 2))) {
|
|
523
|
+
this.push(sp(` ${line}`, 'dim'));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* `label value detail note`. The note is what the figure means: beside
|
|
528
|
+
* the figure when it fits, wrapped under the row in a report, and in its
|
|
529
|
+
* short form on a live screen too narrow for the long one.
|
|
530
|
+
*/
|
|
531
|
+
row(label, value, tone, detail = [], note = '', short = '') {
|
|
532
|
+
const head = [sp(` ${label.padEnd(LABEL)}`, 'dim'), sp(value.padStart(VALUE), tone)];
|
|
533
|
+
const used = [...head, ...detail].reduce((n, s) => n + s.text.length, 0);
|
|
534
|
+
if (!this.#o.explain || !note) {
|
|
535
|
+
this.push(...head, ...detail);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const column = Math.max(NOTE_COLUMN, used + 2);
|
|
539
|
+
const gap = sp(' '.repeat(column - used));
|
|
540
|
+
if (column + note.length <= this.#o.width) {
|
|
541
|
+
this.push(...head, ...detail, gap, sp(note, 'dim'));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (!this.#o.wrapNotes) {
|
|
545
|
+
this.push(...head, ...detail, gap, sp(short || note, 'dim'));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
this.push(...head, ...detail);
|
|
549
|
+
for (const l of wrap(note, Math.max(20, this.#o.width - NOTE_INDENT))) {
|
|
550
|
+
this.push(sp(`${' '.repeat(NOTE_INDENT)}${l}`, 'dim'));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/** Containers keep at least this many rows on a short screen before the hints get any. */
|
|
555
|
+
const MIN_CONTAINER_ROWS = 3;
|
|
556
|
+
export function layout(view, o) {
|
|
557
|
+
const full = sections(view, o);
|
|
558
|
+
if (o.rows === undefined) {
|
|
559
|
+
return [...full.top, ...full.head, ...full.rows, ...full.tail];
|
|
560
|
+
}
|
|
561
|
+
// Too tall: the prose under the headings goes first, then container rows
|
|
562
|
+
// and hints share what is left, the containers getting a floor.
|
|
563
|
+
const fits = (s) => s.top.length + s.head.length + s.rows.length + s.tail.length <= o.rows;
|
|
564
|
+
const s = fits(full) || !o.explain ? full : sections(view, { ...o, intros: false });
|
|
565
|
+
if (fits(s)) {
|
|
566
|
+
return [...s.top, ...s.head, ...s.rows, ...s.tail];
|
|
567
|
+
}
|
|
568
|
+
const avail = Math.max(0, o.rows - s.top.length - s.head.length);
|
|
569
|
+
const floor = Math.min(s.rows.length, MIN_CONTAINER_ROWS, avail);
|
|
570
|
+
let shown = Math.max(floor, Math.min(s.rows.length, avail - s.tail.length));
|
|
571
|
+
const cut = shown < s.rows.length;
|
|
572
|
+
if (cut) {
|
|
573
|
+
shown = Math.max(0, shown - 1);
|
|
574
|
+
}
|
|
575
|
+
const hints = Math.max(0, avail - shown - (cut ? 1 : 0));
|
|
576
|
+
const more = cut
|
|
577
|
+
? [[sp(` +${s.rows.length - shown} more — a taller window shows them`, 'dim')]]
|
|
578
|
+
: [];
|
|
579
|
+
const tail = hints >= s.tail.length
|
|
580
|
+
? s.tail
|
|
581
|
+
: hints > 0
|
|
582
|
+
? [
|
|
583
|
+
...s.tail.slice(0, wholeHints(s.tail, hints - 1)),
|
|
584
|
+
[sp(' … a taller window shows the rest', 'dim')],
|
|
585
|
+
]
|
|
586
|
+
: [];
|
|
587
|
+
return [...s.top, ...s.head, ...s.rows.slice(0, shown), ...more, ...tail].slice(0, o.rows);
|
|
588
|
+
}
|
|
589
|
+
const HINT_MARK = ' • ';
|
|
590
|
+
/** The longest prefix of `n` lines or fewer that ends between two hints, never inside one. */
|
|
591
|
+
function wholeHints(tail, n) {
|
|
592
|
+
let k = n;
|
|
593
|
+
while (k > 0 && k < tail.length && tail[k][0]?.text !== HINT_MARK) {
|
|
594
|
+
k--;
|
|
595
|
+
}
|
|
596
|
+
return k;
|
|
597
|
+
}
|
|
598
|
+
function sections(view, o) {
|
|
599
|
+
const top = new Lines(o);
|
|
600
|
+
const when = new Date(view.at).toLocaleTimeString();
|
|
601
|
+
const how = o.live !== undefined
|
|
602
|
+
? `refreshing every ${o.live}s · ${when}`
|
|
603
|
+
: view.interval
|
|
604
|
+
? `cpu measured over ${view.interval.toFixed(1)}s · ${when}`
|
|
605
|
+
: when;
|
|
606
|
+
top.push(sp('zen sandbox', 'strong'), sp(' · memory and cpu', 'plain'), sp(` ${how}`, 'dim'));
|
|
607
|
+
for (const p of view.problems) {
|
|
608
|
+
top.push(sp(` ✗ ${p}`, 'bad'));
|
|
609
|
+
}
|
|
610
|
+
top.blank();
|
|
611
|
+
hostSection(top, view);
|
|
612
|
+
machineSection(top, view, o);
|
|
613
|
+
const head = new Lines(o);
|
|
614
|
+
containerHead(head, view, o);
|
|
615
|
+
const tail = new Lines(o);
|
|
616
|
+
hintSection(tail, view, o);
|
|
617
|
+
return { top: top.out, head: head.out, rows: containerRows(view, o), tail: tail.out };
|
|
618
|
+
}
|
|
619
|
+
function hostSection(l, v) {
|
|
620
|
+
const vm = v.host.vm;
|
|
621
|
+
if (!vm) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const mac = v.platform === 'darwin' ? 'THIS MAC' : 'THIS COMPUTER';
|
|
625
|
+
l.heading(mac, `${bytes(v.host.memory)} memory · ${v.host.cpus} cores`);
|
|
626
|
+
l.intro('Containers do not run on this computer directly: they run inside one Linux virtual ' +
|
|
627
|
+
'machine, and these rows are what that machine costs it.');
|
|
628
|
+
const share = vm.memory / v.host.memory;
|
|
629
|
+
l.row('machine', bytes(vm.memory), pressure(share * 2), [
|
|
630
|
+
sp(' '),
|
|
631
|
+
...bar([
|
|
632
|
+
[share, '█', pressure(share * 2)],
|
|
633
|
+
[1, '░', 'dim'],
|
|
634
|
+
], 20),
|
|
635
|
+
sp(` ${pct(share * 100)}`, 'dim'),
|
|
636
|
+
], vm.footprint !== undefined
|
|
637
|
+
? `what Activity Monitor shows for “${vm.name}”: memory the machine has touched, which grows and rarely shrinks`
|
|
638
|
+
: `memory held by ${vm.name}`, vm.footprint !== undefined ? 'Activity Monitor’s figure' : '');
|
|
639
|
+
if (vm.footprint !== undefined && vm.rss < vm.footprint * 0.8) {
|
|
640
|
+
l.row('in RAM now', bytes(vm.rss), 'plain', [sp(` ${pct((vm.rss / vm.footprint) * 100)} of it`, 'dim')], 'the part physically in RAM; macOS has compressed or swapped the rest', 'rest compressed or swapped');
|
|
641
|
+
}
|
|
642
|
+
if (vm.peak !== undefined && vm.peak > vm.memory * 1.02) {
|
|
643
|
+
l.row('peak', bytes(vm.peak), 'plain', [], 'the most it has held since the machine started', 'most since it started');
|
|
644
|
+
}
|
|
645
|
+
l.row('cpu', vm.cpu === undefined ? '…' : pct(vm.cpu), vm.cpu === undefined ? 'dim' : pressure(vm.cpu / 100 / Math.max(1, v.host.cpus)), [sp(' of one core', 'dim')], '100% is one core kept fully busy by everything inside the machine', '100% = one core busy');
|
|
646
|
+
if (v.host.helpers.length > 0) {
|
|
647
|
+
const mem = v.host.helpers.reduce((n, p) => n + p.memory, 0);
|
|
648
|
+
l.row('helpers', bytes(mem), 'plain', [sp(` ${v.host.helpers.map((p) => p.name).join(', ')}`, 'dim')], 'programs beside the machine: vfkit starts it, gvproxy carries its network', 'start it, carry its network');
|
|
649
|
+
}
|
|
650
|
+
l.blank();
|
|
651
|
+
}
|
|
652
|
+
function machineSection(l, v, o) {
|
|
653
|
+
const m = v.machine;
|
|
654
|
+
if (!m) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const native = v.platform === 'linux';
|
|
658
|
+
l.heading(native ? 'THIS COMPUTER' : 'INSIDE THE MACHINE', [m.name, `${bytes(m.total)} memory`, `${m.cpus} cpus`].filter(Boolean).join(' · '));
|
|
659
|
+
l.intro(native
|
|
660
|
+
? 'Containers run directly on this computer, so they share its memory with everything else on it.'
|
|
661
|
+
: 'The Linux machine every container shares. Its size was set when it was created; ' +
|
|
662
|
+
'`podman machine set --memory` changes it.');
|
|
663
|
+
const width = Math.max(10, Math.min(BAR * 2, o.width - 4));
|
|
664
|
+
l.push(sp(' '), ...bar([
|
|
665
|
+
[m.used / m.total, '█', pressure(m.used / m.total)],
|
|
666
|
+
[m.cache / m.total, '▒', 'accent'],
|
|
667
|
+
[1, '░', 'dim'],
|
|
668
|
+
], width));
|
|
669
|
+
if (o.explain) {
|
|
670
|
+
l.push(sp(' '), sp('█', pressure(m.used / m.total)), sp(' in use ', 'dim'), sp('▒', 'accent'), sp(' cache ', 'dim'), sp('░ free', 'dim'));
|
|
671
|
+
}
|
|
672
|
+
const of = (n) => [sp(` ${pct((n / m.total) * 100).padStart(4)}`, 'dim')];
|
|
673
|
+
l.row('in use', bytes(m.used), pressure(m.used / m.total), of(m.used), 'held by programs: the containers and the machine’s own services', 'held by programs');
|
|
674
|
+
l.row('cache', bytes(m.cache), 'accent', of(m.cache), 'recently read files kept for speed; given back the moment a program needs room', 'file cache, freed on demand');
|
|
675
|
+
l.row('free', bytes(m.free), 'plain', of(m.free), 'not used for anything', 'unused');
|
|
676
|
+
l.row('available', bytes(m.available), pressure(1 - m.available / m.total), of(m.available), 'what a new command could get right now: free plus cache', 'free + cache: room left');
|
|
677
|
+
l.row('swap', bytes(m.swapUsed), m.swapUsed > 64 * MIB ? 'warn' : 'plain', [sp(` of ${bytes(m.swapTotal)}`, 'dim')], 'memory moved to disk when RAM ran out; above zero means it ran short and slowed down', 'RAM spilled to disk');
|
|
678
|
+
if (m.held !== undefined && m.held > 256 * MIB) {
|
|
679
|
+
l.row('held, unused', bytes(m.held), 'warn', [], 'counted by macOS, used by nothing inside: freed after earlier use, never handed back', 'macOS counts it, nothing uses it');
|
|
680
|
+
}
|
|
681
|
+
l.blank();
|
|
682
|
+
}
|
|
683
|
+
function containerHead(l, v, o) {
|
|
684
|
+
const t = v.totals;
|
|
685
|
+
const kinds = [
|
|
686
|
+
t.zen ? `${t.zen} zen` : '',
|
|
687
|
+
t.faker ? `${t.faker} faker` : '',
|
|
688
|
+
t.other ? `${t.other} other` : '',
|
|
689
|
+
]
|
|
690
|
+
.filter(Boolean)
|
|
691
|
+
.join(', ');
|
|
692
|
+
l.heading('CONTAINERS', t.running === 0
|
|
693
|
+
? `none running${v.stopped ? ` · ${v.stopped} stopped` : ''}`
|
|
694
|
+
: [
|
|
695
|
+
`${t.running} running${kinds ? ` (${kinds})` : ''}`,
|
|
696
|
+
`${bytes(t.memory)} memory`,
|
|
697
|
+
`cpu ${pct(t.cpu)}`,
|
|
698
|
+
v.stopped ? `${v.stopped} stopped` : '',
|
|
699
|
+
]
|
|
700
|
+
.filter(Boolean)
|
|
701
|
+
.join(' · '));
|
|
702
|
+
l.intro('Running ones only: a stopped container keeps its files but uses no memory or cpu. ' +
|
|
703
|
+
'zen makes one per session; zen faker servers and anything else in podman are listed too.');
|
|
704
|
+
if (t.running === 0) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (o.explain && o.intros !== false) {
|
|
708
|
+
const over = v.interval ? `the last ${v.interval.toFixed(0)}s` : 'its whole life so far';
|
|
709
|
+
const legend = [
|
|
710
|
+
`CPU — share of one core over ${over}; 100% is one core fully busy.`,
|
|
711
|
+
'MEMORY — what its programs hold now, cache not counted; the bar is how close it is to LIMIT.',
|
|
712
|
+
'LIMIT — the most it may use (`sandbox.memory` in agents.yaml); past it a command is killed with exit 137.',
|
|
713
|
+
'NET — received ↓ and sent ↑ since it started. orphaned — its zen run is gone, yet it keeps running.',
|
|
714
|
+
];
|
|
715
|
+
for (const text of legend) {
|
|
716
|
+
for (const line of wrap(text, Math.max(20, o.width - 4))) {
|
|
717
|
+
l.push(sp(` ${line}`, 'dim'));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
l.push(sp(' '), ...tableColumns(o.width).flatMap((c, i) => [
|
|
722
|
+
sp(i > 0 ? ' ' : ''),
|
|
723
|
+
sp(c.align === 'right' ? c.title.padStart(c.width) : c.title.padEnd(c.width), 'strong'),
|
|
724
|
+
]));
|
|
725
|
+
}
|
|
726
|
+
/** The name gets whatever the other columns leave, but never less than this. */
|
|
727
|
+
const NAME_MIN = 24;
|
|
728
|
+
const COLUMNS = [
|
|
729
|
+
{
|
|
730
|
+
title: 'CPU',
|
|
731
|
+
width: 7,
|
|
732
|
+
align: 'right',
|
|
733
|
+
keep: 0,
|
|
734
|
+
cell: (c) => [sp(pct(c.cpu), c.cpuOver === 'lifetime' ? 'dim' : pressure(c.cpu / 100))],
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
title: 'MEMORY',
|
|
738
|
+
width: 9,
|
|
739
|
+
align: 'right',
|
|
740
|
+
keep: 0,
|
|
741
|
+
cell: (c) => [sp(bytes(c.memory), c.limit ? pressure(c.memory / c.limit) : 'plain')],
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
title: '',
|
|
745
|
+
width: 10,
|
|
746
|
+
align: 'left',
|
|
747
|
+
keep: 3,
|
|
748
|
+
cell: (c) => c.limit
|
|
749
|
+
? bar([
|
|
750
|
+
[c.memory / c.limit, '█', pressure(c.memory / c.limit)],
|
|
751
|
+
[1, '░', 'dim'],
|
|
752
|
+
], 10)
|
|
753
|
+
: [sp('·', 'dim')],
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
title: 'LIMIT',
|
|
757
|
+
width: 9,
|
|
758
|
+
align: 'right',
|
|
759
|
+
keep: 4,
|
|
760
|
+
cell: (c) => [sp(c.limit ? bytes(c.limit) : 'none', 'dim')],
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
title: 'PIDS',
|
|
764
|
+
width: 5,
|
|
765
|
+
align: 'right',
|
|
766
|
+
keep: 2,
|
|
767
|
+
cell: (c) => [sp(String(c.pids))],
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
title: 'NET ↓ / ↑',
|
|
771
|
+
width: 19,
|
|
772
|
+
align: 'left',
|
|
773
|
+
keep: 1,
|
|
774
|
+
cell: (c) => [sp(`${bytes(c.netIn)} ↓ ${bytes(c.netOut)} ↑`, 'dim')],
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
title: 'AGE',
|
|
778
|
+
width: 5,
|
|
779
|
+
align: 'left',
|
|
780
|
+
keep: 2,
|
|
781
|
+
cell: (c) => [
|
|
782
|
+
sp(c.createdAt ? ago(c.createdAt).replace(' ago', '').replace('just now', 'now') : '', 'dim'),
|
|
783
|
+
],
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
title: 'WHAT',
|
|
787
|
+
width: 8,
|
|
788
|
+
align: 'left',
|
|
789
|
+
keep: 0,
|
|
790
|
+
cell: (c) => [
|
|
791
|
+
sp(c.orphaned ? 'orphaned' : c.kind, c.orphaned ? 'warn' : c.kind === 'other' ? 'dim' : 'plain'),
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
title: 'PROJECT',
|
|
796
|
+
width: 14,
|
|
797
|
+
align: 'left',
|
|
798
|
+
keep: 5,
|
|
799
|
+
cell: (c) => [sp(clipTo(c.project ?? (c.kind === 'faker' ? '(faker)' : '—'), 14))],
|
|
800
|
+
},
|
|
801
|
+
];
|
|
802
|
+
/** The columns that fit, in order, dropping the least needed until the name has room. */
|
|
803
|
+
function tableColumns(width) {
|
|
804
|
+
let cols = [...COLUMNS];
|
|
805
|
+
const used = () => 2 + cols.reduce((n, c) => n + c.width + 1, 0);
|
|
806
|
+
const order = [...COLUMNS].filter((c) => c.keep > 0).sort((a, b) => a.keep - b.keep);
|
|
807
|
+
for (const drop of order) {
|
|
808
|
+
if (used() + NAME_MIN <= width) {
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
cols = cols.filter((c) => c !== drop);
|
|
812
|
+
}
|
|
813
|
+
return [...cols, { title: 'NAME', width: 0, align: 'left', keep: 0, cell: () => [] }];
|
|
814
|
+
}
|
|
815
|
+
function containerRows(v, o) {
|
|
816
|
+
const sorted = [...v.containers].sort((a, b) => {
|
|
817
|
+
switch (o.sort ?? 'memory') {
|
|
818
|
+
case 'cpu':
|
|
819
|
+
return b.cpu - a.cpu;
|
|
820
|
+
case 'name':
|
|
821
|
+
return a.name.localeCompare(b.name);
|
|
822
|
+
default:
|
|
823
|
+
return b.memory - a.memory;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
const cols = tableColumns(o.width).slice(0, -1);
|
|
827
|
+
const room = Math.max(NAME_MIN, o.width - 2 - cols.reduce((n, c) => n + c.width + 1, 0));
|
|
828
|
+
return sorted.map((c) => finish([
|
|
829
|
+
sp(' '),
|
|
830
|
+
...cols.flatMap((col) => {
|
|
831
|
+
const spans = col.cell(c);
|
|
832
|
+
const len = spans.reduce((n, s) => n + s.text.length, 0);
|
|
833
|
+
const pad = sp(' '.repeat(Math.max(0, col.width - len)));
|
|
834
|
+
return col.align === 'right' ? [pad, ...spans, sp(' ')] : [...spans, pad, sp(' ')];
|
|
835
|
+
}),
|
|
836
|
+
sp(clipTo(c.name, room), 'dim'),
|
|
837
|
+
]));
|
|
838
|
+
}
|
|
839
|
+
function clipTo(text, width) {
|
|
840
|
+
return text.length <= width ? text : `${text.slice(0, width - 1)}…`;
|
|
841
|
+
}
|
|
842
|
+
function hintSection(l, v, o) {
|
|
843
|
+
if (v.hints.length === 0) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
l.blank();
|
|
847
|
+
l.heading('WHAT TO DO', '');
|
|
848
|
+
for (const hint of v.hints) {
|
|
849
|
+
const [first = '', ...rest] = wrap(hint, Math.max(20, o.width - 6));
|
|
850
|
+
l.push(sp(HINT_MARK, 'warn'), sp(first));
|
|
851
|
+
for (const line of rest) {
|
|
852
|
+
l.push(sp(` ${line}`));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
//# sourceMappingURL=resources.js.map
|