@3minapi/cli 0.0.0-stage → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/bin.js +54 -0
  4. package/dist/cli.js +241 -0
  5. package/dist/commands/auth.js +573 -0
  6. package/dist/commands/common.js +48 -0
  7. package/dist/commands/dashboard.js +298 -0
  8. package/dist/commands/delete.js +167 -0
  9. package/dist/commands/doctor.js +326 -0
  10. package/dist/commands/guide.js +84 -0
  11. package/dist/commands/index.js +28 -0
  12. package/dist/commands/init.js +248 -0
  13. package/dist/commands/logs.js +154 -0
  14. package/dist/commands/new.js +109 -0
  15. package/dist/commands/paths.js +64 -0
  16. package/dist/commands/reload.js +32 -0
  17. package/dist/commands/results.js +445 -0
  18. package/dist/commands/start.js +443 -0
  19. package/dist/commands/stop.js +68 -0
  20. package/dist/commands/tasks.js +93 -0
  21. package/dist/commands/worker.js +178 -0
  22. package/dist/core/api.js +132 -0
  23. package/dist/core/config.js +188 -0
  24. package/dist/core/control-db.js +462 -0
  25. package/dist/core/credentials.js +75 -0
  26. package/dist/core/domain/constants.js +121 -0
  27. package/dist/core/domain/contract.js +102 -0
  28. package/dist/core/domain/errors.js +57 -0
  29. package/dist/core/domain/index.js +3 -0
  30. package/dist/core/domain/types.js +1 -0
  31. package/dist/core/error-report.js +103 -0
  32. package/dist/core/host/detach.js +49 -0
  33. package/dist/core/host/live-connection.js +270 -0
  34. package/dist/core/host/live-receiver.js +264 -0
  35. package/dist/core/host/module.js +1 -0
  36. package/dist/core/host/poller.js +113 -0
  37. package/dist/core/host/pull-step.js +54 -0
  38. package/dist/core/host/receiver.js +1 -0
  39. package/dist/core/host/reload.js +34 -0
  40. package/dist/core/host/runner-lock.js +245 -0
  41. package/dist/core/host/session.js +84 -0
  42. package/dist/core/host/usage.js +20 -0
  43. package/dist/core/log-read.js +59 -0
  44. package/dist/core/logger.js +206 -0
  45. package/dist/core/names.js +27 -0
  46. package/dist/core/network.js +10 -0
  47. package/dist/core/open-browser.js +26 -0
  48. package/dist/core/output.js +68 -0
  49. package/dist/core/paths.js +26 -0
  50. package/dist/core/project.js +85 -0
  51. package/dist/core/prompt.js +140 -0
  52. package/dist/core/redact.js +45 -0
  53. package/dist/core/secure-fs.js +50 -0
  54. package/dist/core/text-file.js +12 -0
  55. package/dist/core/text.js +27 -0
  56. package/dist/core/time.js +84 -0
  57. package/dist/core/update-notice.js +142 -0
  58. package/dist/core/version.js +16 -0
  59. package/dist/core/warnings.js +10 -0
  60. package/dist/help.js +144 -0
  61. package/dist/modules/dashboard/assets.js +411 -0
  62. package/dist/modules/dashboard/data.js +565 -0
  63. package/dist/modules/dashboard/guard.js +75 -0
  64. package/dist/modules/dashboard/lock.js +93 -0
  65. package/dist/modules/dashboard/server.js +295 -0
  66. package/dist/modules/dashboard/views/html.js +114 -0
  67. package/dist/modules/dashboard/views/layout.js +59 -0
  68. package/dist/modules/dashboard/views/logs.js +122 -0
  69. package/dist/modules/dashboard/views/overview.js +316 -0
  70. package/dist/modules/dashboard/views/runs.js +486 -0
  71. package/dist/modules/dashboard/views/workers.js +157 -0
  72. package/dist/modules/workers/builtin/host-identity.js +59 -0
  73. package/dist/modules/workers/builtin/results-worker.js +37 -0
  74. package/dist/modules/workers/local-server.js +244 -0
  75. package/dist/modules/workers/module.js +98 -0
  76. package/dist/modules/workers/reload-result.js +16 -0
  77. package/dist/modules/workers/repeat-scheduler.js +165 -0
  78. package/dist/modules/workers/results/missing-paths.js +66 -0
  79. package/dist/modules/workers/results/query.js +175 -0
  80. package/dist/modules/workers/results/reader-input.js +134 -0
  81. package/dist/modules/workers/results/reader.js +430 -0
  82. package/dist/modules/workers/results/results-child.js +64 -0
  83. package/dist/modules/workers/results/shape.js +212 -0
  84. package/dist/modules/workers/results/store.js +336 -0
  85. package/dist/modules/workers/results/substitute.js +30 -0
  86. package/dist/modules/workers/runtime/env.js +43 -0
  87. package/dist/modules/workers/runtime/error-chain.js +109 -0
  88. package/dist/modules/workers/runtime/executor.js +289 -0
  89. package/dist/modules/workers/runtime/inspect.js +53 -0
  90. package/dist/modules/workers/runtime/protocol.js +1 -0
  91. package/dist/modules/workers/runtime/resolve.js +30 -0
  92. package/dist/modules/workers/runtime/schema.js +85 -0
  93. package/dist/modules/workers/runtime/thread.js +117 -0
  94. package/dist/modules/workers/start.js +265 -0
  95. package/dist/modules/workers/sync.js +241 -0
  96. package/dist/modules/workers/task-runner.js +892 -0
  97. package/dist/modules/workers/template.js +100 -0
  98. package/dist/modules/workers/worker-set.js +122 -0
  99. package/dist/modules/workers/workers.js +66 -0
  100. package/package.json +48 -5
@@ -0,0 +1,445 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { BIN, CliError, ExitCode, RESULTS_LIST_DEFAULT, RESULTS_QUERY_TIMEOUT_MS, UsageError } from "../core/domain/index.js";
3
+ import { assertWorkerName } from "../core/names.js";
4
+ import { confirm } from "../core/prompt.js";
5
+ import { formatBytes, formatCount, stripControl } from "../core/text.js";
6
+ import { formatLocal, parseDateArg, toIsoUtc } from "../core/time.js";
7
+ import { describeTask, describeWorker, runQuery } from "../modules/workers/results/query.js";
8
+ import { ResultStore, resultsDbPath } from "../modules/workers/results/store.js";
9
+ import { requireProject } from "./common.js";
10
+ const workerOption = {
11
+ type: 'string',
12
+ valueName: 'name',
13
+ description: 'Only this worker'
14
+ };
15
+ const runOption = {
16
+ type: 'string',
17
+ valueName: 'n',
18
+ description: 'A run of a repeating task (default: the latest run)'
19
+ };
20
+ const sinceOption = {
21
+ type: 'string',
22
+ valueName: 'date',
23
+ description: 'From this time — YYYY-MM-DD (local midnight) or ISO 8601'
24
+ };
25
+ export const resultsList = {
26
+ name: 'worker results list',
27
+ summary: 'List the worker results saved in data.db, newest first',
28
+ usage: '[--worker <name>] [--since <date>] [-n <count>]',
29
+ description: 'Every task result this machine ran is saved in the project data.db (table _3minapi_results),\n' +
30
+ 'including results too large to send. Shows time, task id, worker, status, what was sent and size.',
31
+ options: {
32
+ worker: workerOption,
33
+ since: sinceOption,
34
+ lines: {
35
+ type: 'string',
36
+ short: 'n',
37
+ valueName: 'count',
38
+ description: `How many (default ${RESULTS_LIST_DEFAULT})`
39
+ }
40
+ },
41
+ args: { min: 0, max: 0 },
42
+ async run(ctx) {
43
+ const { out } = ctx;
44
+ const filter = filterFrom(ctx);
45
+ const limit = positiveInt(ctx.values.lines, '-n', RESULTS_LIST_DEFAULT);
46
+ const store = openStore(ctx);
47
+ if (!store) {
48
+ out.result({ results: [] }, noResults(ctx));
49
+ return ExitCode.Ok;
50
+ }
51
+ try {
52
+ const rows = store.list(filter, limit);
53
+ const total = store.usage(filter);
54
+ out.result({ results: rows, total: total.count }, rows.length === 0
55
+ ? 'No saved results match.'
56
+ : [
57
+ ...rows.map(rowLine),
58
+ ...(total.count > rows.length
59
+ ? [
60
+ out.out.dim(`… ${formatCount(total.count - rows.length)} more (-n to show more)`)
61
+ ]
62
+ : [])
63
+ ].join('\n'));
64
+ return ExitCode.Ok;
65
+ }
66
+ finally {
67
+ store.close();
68
+ }
69
+ }
70
+ };
71
+ export const resultsShow = {
72
+ name: 'worker results show',
73
+ summary: 'Print one saved result as it was returned (completed result or failed error)',
74
+ usage: '<task_id> [--run <n>]',
75
+ description: 'Prints the original JSON — also for results too large to send, whose task the AI saw as a\n' +
76
+ 'summary. The task id can be shortened to its first characters if that matches only one.\n' +
77
+ "A repeating task saves every run: this shows the latest, --run another ('worker results runs' lists them).",
78
+ options: { run: runOption },
79
+ args: { min: 1, max: 1 },
80
+ async run(ctx) {
81
+ const { out } = ctx;
82
+ const store = requireStore(ctx);
83
+ try {
84
+ const r = findOne(store, ctx.positionals[0], runArg(ctx));
85
+ const payload = JSON.parse(r.payload);
86
+ const { payload: _raw, shape: _shape, ...row } = r;
87
+ if (!out.json) {
88
+ out.info(out.err.dim(rowLine(row)));
89
+ const runs = runsNote(store, r);
90
+ if (runs)
91
+ out.info(out.err.dim(runs));
92
+ }
93
+ out.result({ ...row, payload }, stripControl(JSON.stringify(payload, null, 2)));
94
+ return ExitCode.Ok;
95
+ }
96
+ finally {
97
+ store.close();
98
+ }
99
+ }
100
+ };
101
+ export const resultsDescribe = {
102
+ name: 'worker results describe',
103
+ summary: "Summarize a saved result's structure, or group a worker's results by structure",
104
+ usage: '<task_id> [--run <n>] | --worker <name> [--since <date>]',
105
+ description: 'With a task id: the paths in the result with their types, counts, ranges and sample values.\n' +
106
+ 'A path counted fewer times than its array has items is an optional field.\n' +
107
+ "With --worker: the worker's completed results grouped by structure (shape hash), with how each\n" +
108
+ 'structure differs from the current one (that of the latest result). The current structure is\n' +
109
+ 'summarized over its latest 1,000 results; for values and ranges over all of them, use query.',
110
+ options: { worker: workerOption, since: sinceOption, run: runOption },
111
+ args: { min: 0, max: 1 },
112
+ async run(ctx) {
113
+ const { out, values } = ctx;
114
+ const id = ctx.positionals[0];
115
+ if ((id === undefined) === (values.worker === undefined)) {
116
+ throw new UsageError(`give a task id or --worker <name>, not both.\nusage: ${BIN} worker results describe <task_id> | --worker <name>`);
117
+ }
118
+ if (id !== undefined && values.since !== undefined) {
119
+ throw new UsageError('--since goes with --worker');
120
+ }
121
+ if (id === undefined && values.run !== undefined) {
122
+ throw new UsageError('--run goes with a task id');
123
+ }
124
+ const store = requireStore(ctx);
125
+ try {
126
+ if (id !== undefined) {
127
+ const r = findOne(store, id, runArg(ctx));
128
+ if (!r.shape) {
129
+ throw new CliError(`${r.task_id} is a ${r.status} result — only completed results have a structure. See it with '${BIN} worker results show ${r.task_id}'.`);
130
+ }
131
+ const described = describeTask({ ...r, shape: r.shape });
132
+ const runs = runsNote(store, r);
133
+ out.result(runs ? { run_no: r.run_no, ...described } : described, [
134
+ out.out.dim(rowLine(r)),
135
+ ...(runs ? [out.out.dim(runs)] : []),
136
+ ...shapeLines(described.shape)
137
+ ].join('\n'));
138
+ return ExitCode.Ok;
139
+ }
140
+ const shapes = describeWorker(store, filterFrom(ctx));
141
+ if (shapes.total === 0) {
142
+ out.result(shapes, `No completed results saved for ${values.worker}.`);
143
+ return ExitCode.Ok;
144
+ }
145
+ const lines = [
146
+ `${shapes.worker}: ${formatCount(shapes.total)} completed results, ` +
147
+ `${formatCount(shapes.groups.length + (shapes.others?.groups ?? 0))} structure(s)`
148
+ ];
149
+ for (const g of shapes.groups) {
150
+ lines.push('', `${g.shape_hash} ${formatCount(g.count)} result(s) ${formatLocal(Date.parse(g.first))} → ${formatLocal(Date.parse(g.last))}` +
151
+ (g.shape_hash === shapes.baseline ? ' (current)' : ''), ` latest: ${g.task_ids.join(', ')}`);
152
+ if (g.added.length > 0)
153
+ lines.push(` only here: ${stripControl(g.added.join(', '))}${more(g.added_more)}`);
154
+ if (g.removed.length > 0)
155
+ lines.push(` missing here: ${stripControl(g.removed.join(', '))}${more(g.removed_more)}`);
156
+ }
157
+ if (shapes.others) {
158
+ lines.push('', out.out.dim(`… ${formatCount(shapes.others.groups)} less common structure(s) with ${formatCount(shapes.others.count)} result(s) not shown`));
159
+ }
160
+ if (shapes.shape) {
161
+ lines.push('', `Current structure — values and ranges across the latest ${formatCount(shapes.shape_results)} result(s) ` +
162
+ `since ${formatLocal(Date.parse(shapes.shape_since))}:`, ...shapeLines(shapes.shape));
163
+ if (shapes.note)
164
+ lines.push(out.out.dim(shapes.note));
165
+ }
166
+ out.result(shapes, lines.join('\n'));
167
+ return ExitCode.Ok;
168
+ }
169
+ finally {
170
+ store.close();
171
+ }
172
+ }
173
+ };
174
+ export const resultsQuery = {
175
+ name: 'worker results query',
176
+ summary: 'Run a read-only SQL query on data.db',
177
+ usage: '"<SQL>" [--timeout <seconds>]',
178
+ description: 'For pulling only what you need out of large results, e.g.\n' +
179
+ ` ${BIN} worker results query "SELECT j.value ->> '$.id' FROM _3minapi_results r,\n` +
180
+ " json_each(r.payload, '$.items') j WHERE r.task_id = '…'\"\n" +
181
+ 'Only reading is allowed: writes, ATTACH, PRAGMA and transactions are refused (exit 2).\n' +
182
+ 'It runs in a separate process and is stopped after the timeout (default ' +
183
+ `${RESULTS_QUERY_TIMEOUT_MS / 1000}s).`,
184
+ options: {
185
+ timeout: {
186
+ type: 'string',
187
+ valueName: 'seconds',
188
+ description: `Stop the query after this many seconds (default ${RESULTS_QUERY_TIMEOUT_MS / 1000})`
189
+ }
190
+ },
191
+ args: { min: 1, max: 1 },
192
+ async run(ctx) {
193
+ const { out } = ctx;
194
+ const root = requireProject(ctx.loadConfig());
195
+ if (!existsSync(resultsDbPath(root))) {
196
+ throw new CliError(`${resultsDbPath(root)} does not exist. '${BIN} init' creates it.`);
197
+ }
198
+ const timeoutMs = positiveInt(ctx.values.timeout, '--timeout', RESULTS_QUERY_TIMEOUT_MS / 1000) * 1000;
199
+ ctx.claimInterrupt();
200
+ const r = await runQuery(root, ctx.positionals[0], timeoutMs, ctx.signal);
201
+ if (!r.ok) {
202
+ if (ctx.signal.aborted)
203
+ throw new CliError('cancelled.', ExitCode.Interrupted);
204
+ if (r.kind === 'denied') {
205
+ throw new UsageError('only reading is allowed — this query writes, attaches a database or uses a PRAGMA or transaction.');
206
+ }
207
+ if (r.kind === 'syntax')
208
+ throw new UsageError(r.message);
209
+ throw new CliError(r.message);
210
+ }
211
+ out.result({ columns: r.columns, rows: r.rows }, [r.columns.map(cell).join('\t'), ...r.rows.map((row) => row.map(cell).join('\t'))].join('\n'));
212
+ return ExitCode.Ok;
213
+ }
214
+ };
215
+ export const resultsPrune = {
216
+ name: 'worker results prune',
217
+ summary: 'Delete saved results older than a date',
218
+ usage: '--before <date> [--worker <name>] [--yes]',
219
+ description: 'Deletes saved results from data.db — nothing is deleted automatically otherwise. Shows how\n' +
220
+ 'many and how large first, and asks to confirm. The file does not shrink: the freed space is\n' +
221
+ 'reused for new results (3minapi does not VACUUM data.db, which would renumber rows in your\n' +
222
+ 'own tables).',
223
+ options: {
224
+ before: {
225
+ type: 'string',
226
+ valueName: 'date',
227
+ description: 'Delete results saved before this time — YYYY-MM-DD (local midnight) or ISO 8601'
228
+ },
229
+ worker: workerOption,
230
+ yes: { type: 'boolean', description: 'Delete without asking for confirmation' }
231
+ },
232
+ args: { min: 0, max: 0 },
233
+ async run(ctx) {
234
+ const { out, values } = ctx;
235
+ if (typeof values.before !== 'string') {
236
+ throw new UsageError(`--before is required.\nusage: ${BIN} worker results prune --before <date>`);
237
+ }
238
+ const filter = filterFrom(ctx);
239
+ const root = requireProject(ctx.loadConfig());
240
+ const reader = ResultStore.openReadOnly(root);
241
+ const target = reader ? reader.usage(filter) : { count: 0, bytes: 0 };
242
+ reader?.close();
243
+ if (target.count === 0) {
244
+ out.result({ deleted: 0, bytes: 0 }, 'Nothing to delete.');
245
+ return ExitCode.Ok;
246
+ }
247
+ const what = `${formatCount(target.count)} saved result(s) (${formatBytes(target.bytes)})${filter.worker ? ` of ${filter.worker}` : ''} from before ${formatLocal(Date.parse(filter.before))}`;
248
+ if (!values.yes) {
249
+ if (out.json || !ctx.stdin.isTTY) {
250
+ throw new UsageError(`this deletes ${what} and cannot be undone. Nothing was deleted. Once the user has agreed, run it again with --yes.`);
251
+ }
252
+ if (!(await confirm(ctx.stdin, out, `Delete ${what}? This cannot be undone.`))) {
253
+ throw new CliError('cancelled — nothing was deleted.');
254
+ }
255
+ }
256
+ const { store, warnings } = ResultStore.open(root);
257
+ for (const w of warnings)
258
+ out.warn(w);
259
+ try {
260
+ const deleted = store.prune(filter);
261
+ out.result({ deleted, bytes: target.bytes }, `Deleted ${formatCount(deleted)} result(s). data.db keeps its size; new results reuse the space.`);
262
+ return ExitCode.Ok;
263
+ }
264
+ finally {
265
+ store.close();
266
+ }
267
+ }
268
+ };
269
+ export const resultsRuns = {
270
+ name: 'worker results runs',
271
+ summary: 'List the saved runs of a repeating task, newest first',
272
+ usage: '<task_id> [-n <count>]',
273
+ description: 'A repeating task saves every run as its own result (run 1, 2, …). Shows each run with its time,\n' +
274
+ "status and size. See one with 'worker results show <task_id> --run <n>'. A single task has one run.",
275
+ options: {
276
+ lines: {
277
+ type: 'string',
278
+ short: 'n',
279
+ valueName: 'count',
280
+ description: `How many (default ${RESULTS_LIST_DEFAULT})`
281
+ }
282
+ },
283
+ args: { min: 1, max: 1 },
284
+ async run(ctx) {
285
+ const { out } = ctx;
286
+ const limit = positiveInt(ctx.values.lines, '-n', RESULTS_LIST_DEFAULT);
287
+ const store = requireStore(ctx);
288
+ try {
289
+ const taskId = findOne(store, ctx.positionals[0]).task_id;
290
+ const rows = store.runs(taskId, limit);
291
+ const total = store.runCounts([taskId]).get(taskId)?.runs ?? rows.length;
292
+ out.result({ task_id: taskId, runs: rows, total }, [
293
+ ...rows.map((r) => `run ${r.run_no ?? 1} ${formatLocal(Date.parse(r.created_at))} ${r.status} ${formatBytes(r.bytes)}`),
294
+ ...(total > rows.length
295
+ ? [out.out.dim(`… ${formatCount(total - rows.length)} older run(s) (-n to show more)`)]
296
+ : [])
297
+ ].join('\n'));
298
+ return ExitCode.Ok;
299
+ }
300
+ finally {
301
+ store.close();
302
+ }
303
+ }
304
+ };
305
+ export const workerResults = {
306
+ name: 'worker results',
307
+ summary: 'See the worker results saved in data.db',
308
+ usage: '<command>',
309
+ description: 'Every result this machine ran is saved in the project data.db (table _3minapi_results),\n' +
310
+ 'so nothing is lost when a result is too large to send: a local worker then sends the AI a\n' +
311
+ 'summary of it instead. A repeating task saves every run. Results may contain personal data and\n' +
312
+ "are never deleted automatically — use 'worker results prune'.",
313
+ options: {},
314
+ args: { min: 0, max: 0 },
315
+ subcommands: [resultsList, resultsShow, resultsRuns, resultsDescribe, resultsQuery, resultsPrune],
316
+ async run() {
317
+ return ExitCode.Usage;
318
+ }
319
+ };
320
+ function openStore(ctx) {
321
+ const root = requireProject(ctx.loadConfig());
322
+ try {
323
+ return ResultStore.openReadOnly(root);
324
+ }
325
+ catch (err) {
326
+ throw new CliError(`cannot read ${resultsDbPath(root)}: ${err.message}`);
327
+ }
328
+ }
329
+ function requireStore(ctx) {
330
+ const store = openStore(ctx);
331
+ if (!store)
332
+ throw new CliError(noResults(ctx));
333
+ return store;
334
+ }
335
+ function noResults(ctx) {
336
+ const root = requireProject(ctx.loadConfig());
337
+ return `No results saved yet in ${resultsDbPath(root)} — '${BIN} start' saves every task result there.`;
338
+ }
339
+ function findOne(store, id, runNo) {
340
+ const r = store.find(id);
341
+ if ('found' in r) {
342
+ if (runNo === undefined || r.found.run_no === runNo)
343
+ return r.found;
344
+ const run = store.get(r.found.task_id, runNo);
345
+ if (run)
346
+ return run;
347
+ const saved = store.runCounts([r.found.task_id]).get(r.found.task_id);
348
+ throw new CliError(`no saved run ${runNo} for task ${r.found.task_id}` +
349
+ (saved
350
+ ? ` — ${formatCount(saved.runs)} run(s) saved, the latest is run ${saved.lastRunNo}`
351
+ : '') +
352
+ `. '${BIN} worker results runs ${r.found.task_id}' lists them.`);
353
+ }
354
+ if (r.candidates.length === 0) {
355
+ throw new CliError(`no saved result for task "${id}". '${BIN} worker results list' shows them.`);
356
+ }
357
+ throw new CliError(`"${id}" matches more than one result: ${r.candidates.slice(0, 5).join(', ')}${r.candidates.length > 5 ? ', …' : ''}. Give more of the task id.`);
358
+ }
359
+ function filterFrom(ctx) {
360
+ const { values } = ctx;
361
+ const filter = {};
362
+ if (typeof values.worker === 'string') {
363
+ assertWorkerName(values.worker);
364
+ filter.worker = values.worker;
365
+ }
366
+ for (const key of ['since', 'before']) {
367
+ const v = values[key];
368
+ if (typeof v !== 'string')
369
+ continue;
370
+ const ms = parseDateArg(v);
371
+ if (ms === null) {
372
+ throw new UsageError(`--${key} "${v}" is not a date. Use YYYY-MM-DD (local midnight) or ISO 8601 like 2026-10-01T09:00.`);
373
+ }
374
+ filter[key] = toIsoUtc(ms);
375
+ }
376
+ return filter;
377
+ }
378
+ function positiveInt(value, flag, fallback) {
379
+ if (value === undefined)
380
+ return fallback;
381
+ const n = Number(value);
382
+ if (!Number.isInteger(n) || n < 1)
383
+ throw new UsageError(`${flag} must be a positive whole number`);
384
+ return n;
385
+ }
386
+ function runArg(ctx) {
387
+ return ctx.values.run === undefined ? undefined : positiveInt(ctx.values.run, '--run', 1);
388
+ }
389
+ function runsNote(store, r) {
390
+ const saved = store.runCounts([r.task_id]).get(r.task_id);
391
+ if (!saved || (saved.lastRunNo <= 1 && (r.run_no ?? 1) <= 1))
392
+ return null;
393
+ return (`run ${r.run_no ?? 1} — ${formatCount(saved.runs)} run(s) saved, the latest is run ${saved.lastRunNo} ` +
394
+ `(--run <n> for another · '${BIN} worker results runs ${r.task_id}' lists them)`);
395
+ }
396
+ function rowLine(r) {
397
+ return [
398
+ formatLocal(Date.parse(r.created_at)),
399
+ (r.run_no ?? 1) > 1 || (r.reply === 'none' && r.source !== 'serve')
400
+ ? `${r.task_id} run ${r.run_no ?? 1}`
401
+ : r.task_id,
402
+ r.worker,
403
+ r.status,
404
+ `sent: ${r.reply}`,
405
+ formatBytes(r.bytes)
406
+ ].join(' ');
407
+ }
408
+ function shapeLines(shape) {
409
+ const lines = Object.entries(shape.paths).map(([p, stat]) => ` ${stripControl(p)} ${stripControl(statText(stat))}`);
410
+ const { samples_from_depth: samples, paths_from_depth: paths } = shape.omitted ?? {};
411
+ if (paths !== undefined)
412
+ lines.push(` … paths at depth ${paths} and deeper left out`);
413
+ if (samples !== undefined && samples !== paths) {
414
+ lines.push(` … sample values at depth ${samples} and deeper left out`);
415
+ }
416
+ if (shape.truncated && shape.omitted === undefined) {
417
+ lines.push(' … (summary cut short: too deep or too many paths)');
418
+ }
419
+ return lines;
420
+ }
421
+ function more(n) {
422
+ return n ? ` … and ${formatCount(n)} more` : '';
423
+ }
424
+ function statText(s) {
425
+ const types = Object.entries(s.types)
426
+ .map(([t, n]) => (Object.keys(s.types).length > 1 ? `${t}×${n}` : t))
427
+ .join('|');
428
+ const parts = [types, `×${formatCount(s.count)}`];
429
+ if (s.length)
430
+ parts.push(`length ${s.length.min}–${s.length.max}`);
431
+ if (s.min !== undefined)
432
+ parts.push(`${s.min}–${s.max}`);
433
+ if (s.values)
434
+ parts.push(`values: ${s.values.map((v) => JSON.stringify(v)).join(', ')}`);
435
+ else if (s.sample !== undefined)
436
+ parts.push(`e.g. ${JSON.stringify(s.sample)}`);
437
+ return parts.join(' ');
438
+ }
439
+ function cell(v) {
440
+ if (v === null)
441
+ return 'NULL';
442
+ if (typeof v === 'object')
443
+ return stripControl(JSON.stringify(v));
444
+ return stripControl(String(v).replace(/[\t\n]/g, ' '));
445
+ }