@assethub/cli 0.1.32 → 0.1.33
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 +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -16
- package/dist/productionBatch.d.ts +2 -0
- package/dist/productionBatch.d.ts.map +1 -1
- package/dist/productionBatch.js +17 -1
- package/dist/progressReport.d.ts +42 -0
- package/dist/progressReport.d.ts.map +1 -0
- package/dist/progressReport.js +201 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -378,12 +378,19 @@ assembly was made. Follow a run started without `--wait` with
|
|
|
378
378
|
While a V4 run works, `--wait`, `runs watch` and `runs get` show where it is.
|
|
379
379
|
The receipt's `progress` field has the phase, a one-line summary, each part's
|
|
380
380
|
state and the Blender assembly rounds, in the words of the canvas Part Composer
|
|
381
|
-
card. `--wait` and `runs watch` print a stderr line
|
|
381
|
+
card. `--wait` and `runs watch` print a stderr line, with the local time, only
|
|
382
|
+
when the run really moves on (a new step, another mesh ready, a part kept with
|
|
383
|
+
an issue, the end); part states that come and go do not print:
|
|
382
384
|
|
|
383
385
|
```text
|
|
384
|
-
|
|
386
|
+
19:52 run 557d2a58 3/4 Making the parts · 3 of 9 meshes ready
|
|
387
|
+
19:58 run 557d2a58 ⚠ kept with an issue: Cropped top · open hem at the lower chest
|
|
388
|
+
20:05 run 557d2a58 ✗ FAILED · at 3/4 Making the parts · Blender assembly failed
|
|
385
389
|
```
|
|
386
390
|
|
|
391
|
+
`production batch --wait` prints the same lines per image, named by letter and
|
|
392
|
+
file name, and a status table every 5 minutes; see `docs/guides/production-batch.md`.
|
|
393
|
+
|
|
387
394
|
```sh
|
|
388
395
|
assethub runs watch <run-id> --timeout-ms 7200000 --download --out-dir ./character
|
|
389
396
|
assethub runs get <run-id> # JSON receipt, progress included
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA2BA,OAAO,EAAsB,KAAK,YAAY,EAAC,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { downloadCanvas } from './canvasDownload.js';
|
|
|
13
13
|
import { downloadCanvasGraphZip } from './canvasGraphDownload.js';
|
|
14
14
|
import { executeNodeMeshBatch, resumeNodeMeshBatch, } from './nodeMeshBatch.js';
|
|
15
15
|
import { writeCanvasComparison } from './canvasComparison.js';
|
|
16
|
-
import { productionBatchLabels, readProductionBatch, runProductionBatch, } from './productionBatch.js';
|
|
16
|
+
import { productionBatchImageName, productionBatchLabels, readProductionBatch, runProductionBatch, } from './productionBatch.js';
|
|
17
17
|
import { importCanvasAssetWithState } from './canvasAssetImport.js';
|
|
18
18
|
import { execFile, spawn } from 'node:child_process';
|
|
19
19
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -27,7 +27,7 @@ import { promisify } from 'node:util';
|
|
|
27
27
|
import { AssetHubApiError, WorkspaceClientError, createWorkspaceClient, createAssetHubClient, } from '@assethub/api-client';
|
|
28
28
|
import { resolveCanvasSelection, saveCanvasSelection } from './canvas.js';
|
|
29
29
|
import { activeExecution, CliExecutionError, executeRecorded, hasRecordedOperation, openExecutionSession, resumeRecorded, waitForExecution, } from './execution.js';
|
|
30
|
-
import {
|
|
30
|
+
import { batchReporter, runReporter } from './progressReport.js';
|
|
31
31
|
import { buildRunUploadPlan, formatBytes } from './runsUpload/buildRunUpload.js';
|
|
32
32
|
import { readGraphFolder } from './runsUpload/graphFolder.js';
|
|
33
33
|
import { uploadRun } from './runsUpload/uploadRun.js';
|
|
@@ -2081,19 +2081,8 @@ const defaultWaitMs = (flags) => {
|
|
|
2081
2081
|
const watchOptions = (flags) => ({
|
|
2082
2082
|
intervalMs: parsePositiveIntegerFlag(flags, 'interval-ms', 5000),
|
|
2083
2083
|
timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', defaultWaitMs(flags)),
|
|
2084
|
-
onProgress:
|
|
2084
|
+
onProgress: runReporter({ write: text => stderr.write(text) }),
|
|
2085
2085
|
});
|
|
2086
|
-
/** Prints a run's line only when it changed since the last poll. */
|
|
2087
|
-
const progressPrinter = (line) => {
|
|
2088
|
-
let last = '';
|
|
2089
|
-
return (run) => {
|
|
2090
|
-
const next = line(run);
|
|
2091
|
-
if (next === last)
|
|
2092
|
-
return;
|
|
2093
|
-
last = next;
|
|
2094
|
-
stderr.write(`${next}\n`);
|
|
2095
|
-
};
|
|
2096
|
-
};
|
|
2097
2086
|
const readJsonArgument = async (value, flag) => {
|
|
2098
2087
|
const text = value === '@-'
|
|
2099
2088
|
? await readStdin()
|
|
@@ -3673,10 +3662,29 @@ const commandProductionBatch = async (ctx) => {
|
|
|
3673
3662
|
if (saved && requestedCanvasId != null && requestedCanvasId !== saved.canvasId)
|
|
3674
3663
|
throw new Error(`Batch ${operationId} runs on canvas ${saved.canvasId}; drop --canvas or pass that canvas`);
|
|
3675
3664
|
const session = await productionSession(ctx, 'production.analyze', saved?.canvasId ?? requestedCanvasId);
|
|
3676
|
-
stderr.write(`[batch] ${operationId} · ${entries.length} run(s) on canvas ${session.canvas.id}. Rerun with --operation-id ${operationId} to resume.\n`);
|
|
3677
3665
|
const wait = hasFlag(ctx.flags, 'wait');
|
|
3678
3666
|
const concurrency = parsePositiveIntegerFlag(ctx.flags, 'concurrency', 2);
|
|
3679
|
-
const
|
|
3667
|
+
const report = batchReporter({
|
|
3668
|
+
write: text => stderr.write(text),
|
|
3669
|
+
operationId,
|
|
3670
|
+
agent: partExtractorOptions.find(option => option.apiValue === options.agentVersion)
|
|
3671
|
+
?.publicName ?? String(options.agentVersion),
|
|
3672
|
+
canvasId: session.canvas.id,
|
|
3673
|
+
...(wait ? { concurrency } : {}),
|
|
3674
|
+
images: entries.map(entry => ({
|
|
3675
|
+
key: entry.key,
|
|
3676
|
+
imageIndex: entry.index,
|
|
3677
|
+
repeat: entry.repeat,
|
|
3678
|
+
name: productionBatchImageName(sources[entry.index]),
|
|
3679
|
+
})),
|
|
3680
|
+
});
|
|
3681
|
+
report.header();
|
|
3682
|
+
const printItem = (item) => report.item(item);
|
|
3683
|
+
// A status table every 5 minutes while waiting; runs take about an hour.
|
|
3684
|
+
const tableTimer = wait
|
|
3685
|
+
? setInterval(() => report.table('still running'), 5 * 60_000)
|
|
3686
|
+
: undefined;
|
|
3687
|
+
tableTimer?.unref();
|
|
3680
3688
|
const result = await runProductionBatch(session, {
|
|
3681
3689
|
operationId,
|
|
3682
3690
|
request: { sources, repeat, options, agent, parentRunId },
|
|
@@ -3714,8 +3722,10 @@ const commandProductionBatch = async (ctx) => {
|
|
|
3714
3722
|
}
|
|
3715
3723
|
: {}),
|
|
3716
3724
|
});
|
|
3725
|
+
clearInterval(tableTimer);
|
|
3717
3726
|
if (!wait)
|
|
3718
3727
|
result.items.forEach(printItem);
|
|
3728
|
+
report.table(!wait ? 'started' : result.timedOut ? 'stopped waiting' : 'done');
|
|
3719
3729
|
const downloads = [];
|
|
3720
3730
|
if (hasFlag(ctx.flags, 'download')) {
|
|
3721
3731
|
const outDir = getFlag(ctx.flags, 'out-dir') ?? getFlag(ctx.flags, 'out');
|
|
@@ -52,6 +52,8 @@ export type ProductionBatchSource = {
|
|
|
52
52
|
* two images must never share one. Files that share a name get their folder.
|
|
53
53
|
*/
|
|
54
54
|
export declare const productionBatchLabels: (sources: readonly ProductionBatchSource[]) => string[];
|
|
55
|
+
/** The image as the user named it, for progress lines: `image (28).png`. */
|
|
56
|
+
export declare const productionBatchImageName: ({ flag, value }: ProductionBatchSource) => string;
|
|
55
57
|
export declare const readProductionBatch: (stateDir: string, operationId: string) => Promise<ProductionBatchState | undefined>;
|
|
56
58
|
export declare const runProductionBatch: (session: Session, options: {
|
|
57
59
|
operationId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"productionBatch.d.ts","sourceRoot":"","sources":["../src/productionBatch.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAGtB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAkB,eAAe,EAAC,MAAM,gBAAgB,CAAA;AAG/D,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAA;AAEpD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;GAIG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,CAAC,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACpF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,CAAA;IACzC,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;
|
|
1
|
+
{"version":3,"file":"productionBatch.d.ts","sourceRoot":"","sources":["../src/productionBatch.ts"],"names":[],"mappings":"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"}
|
package/dist/productionBatch.js
CHANGED
|
@@ -7,6 +7,16 @@ import { activeExecution, executeRecorded } from './execution.js';
|
|
|
7
7
|
import { runProgressLine } from './runProgress.js';
|
|
8
8
|
const stem = (path) => basename(path, extname(path));
|
|
9
9
|
const safeLabel = (label) => label.replace(/[^\w.-]+/g, '_');
|
|
10
|
+
/**
|
|
11
|
+
* A source URL's file name, or its host when the path has none. Never the whole
|
|
12
|
+
* URL: a signed URL carries its credentials in the query string, and this name
|
|
13
|
+
* is printed in progress lines and used as a download folder.
|
|
14
|
+
*/
|
|
15
|
+
const urlFileName = (value, strip = name => name) => {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
const file = basename(url.pathname);
|
|
18
|
+
return file ? strip(file) : url.hostname;
|
|
19
|
+
};
|
|
10
20
|
/**
|
|
11
21
|
* One distinct label per image: it names the order and the download folder, so
|
|
12
22
|
* two images must never share one. Files that share a name get their folder.
|
|
@@ -15,7 +25,7 @@ export const productionBatchLabels = (sources) => {
|
|
|
15
25
|
const base = sources.map(({ flag, value }) => flag === 'file'
|
|
16
26
|
? stem(value)
|
|
17
27
|
: flag === 'source-url'
|
|
18
|
-
?
|
|
28
|
+
? urlFileName(value, stem)
|
|
19
29
|
: value);
|
|
20
30
|
const count = (labels, label) => labels.filter(entry => entry === label).length;
|
|
21
31
|
const withFolder = base.map((label, index) => {
|
|
@@ -30,6 +40,12 @@ export const productionBatchLabels = (sources) => {
|
|
|
30
40
|
: label)
|
|
31
41
|
.map(safeLabel);
|
|
32
42
|
};
|
|
43
|
+
/** The image as the user named it, for progress lines: `image (28).png`. */
|
|
44
|
+
export const productionBatchImageName = ({ flag, value }) => flag === 'file'
|
|
45
|
+
? basename(value)
|
|
46
|
+
: flag === 'source-url'
|
|
47
|
+
? urlFileName(value)
|
|
48
|
+
: value;
|
|
33
49
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
34
50
|
const statePath = (stateDir, operationId) => {
|
|
35
51
|
if (!uuid.test(operationId))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CanvasExecution } from '@assethub/api-client';
|
|
2
|
+
import type { ProductionBatchItemResult } from './productionBatch.js';
|
|
3
|
+
type Clock = () => Date;
|
|
4
|
+
type Write = (text: string) => void;
|
|
5
|
+
/** Where the run is: the summary with `Step 3 of 4` shortened to `3/4`. */
|
|
6
|
+
export declare const stepText: (execution: CanvasExecution) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Why a run stopped, in plain words. The backend sends V4 findings as a
|
|
9
|
+
* Python-style list (`[{'code': ..., 'message': ...}]`); each becomes
|
|
10
|
+
* `message (code)`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const failureText: (execution: CanvasExecution) => string;
|
|
13
|
+
/** `runs watch`, `production analyze --wait` and friends: one run. */
|
|
14
|
+
export declare const runReporter: ({ write, now, }: {
|
|
15
|
+
write: Write;
|
|
16
|
+
now?: Clock;
|
|
17
|
+
}) => (execution: CanvasExecution) => void;
|
|
18
|
+
export type BatchImage = {
|
|
19
|
+
key: string;
|
|
20
|
+
imageIndex: number;
|
|
21
|
+
repeat: number;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `production batch`: a header naming each image by letter (A, B, …; `A#2`
|
|
26
|
+
* for the second repeat), a line per real change, and a status table.
|
|
27
|
+
*/
|
|
28
|
+
export declare const batchReporter: (options: {
|
|
29
|
+
write: Write;
|
|
30
|
+
now?: Clock;
|
|
31
|
+
operationId: string;
|
|
32
|
+
agent: string;
|
|
33
|
+
canvasId: number;
|
|
34
|
+
concurrency?: number;
|
|
35
|
+
images: BatchImage[];
|
|
36
|
+
}) => {
|
|
37
|
+
header: () => void;
|
|
38
|
+
item: (result: ProductionBatchItemResult) => void;
|
|
39
|
+
table: (title: string) => void;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=progressReport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progressReport.d.ts","sourceRoot":"","sources":["../src/progressReport.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAA;AAEzD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAA;AAEnE,KAAK,KAAK,GAAG,MAAM,IAAI,CAAA;AACvB,KAAK,KAAK,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAqBnC,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,GAAI,WAAW,eAAe,KAAG,MAGhC,CAAA;AAEtB;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,WAAW,eAAe,KAAG,MAMtD,CAAA;AAuFH,sEAAsE;AACtE,eAAO,MAAM,WAAW,GAAI,iBAGzB;IACD,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,CAAC,EAAE,KAAK,CAAA;CACZ,MAGS,WAAW,eAAe,SAanC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS;IACrC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,CAAC,EAAE,KAAK,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;;mBA2CuB,yBAAyB;mBAqDzB,MAAM;CAsC7B,CAAA"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// What `--wait` prints while runs work: one line per real change (a new step,
|
|
2
|
+
// another mesh ready, a part kept with an issue, the end of the run), with the
|
|
3
|
+
// local time, and for a batch the image's letter and file name. Part states
|
|
4
|
+
// that flicker (drawing, checking, redoing) never print a line on their own.
|
|
5
|
+
// A stopped run reports phase `done` and step 4 whatever stopped it, so where
|
|
6
|
+
// it stopped is the last step seen while it worked.
|
|
7
|
+
const stepNames = {
|
|
8
|
+
1: 'Planning the parts',
|
|
9
|
+
2: 'Making the base body',
|
|
10
|
+
3: 'Making the parts',
|
|
11
|
+
4: 'Assembling in Blender',
|
|
12
|
+
};
|
|
13
|
+
const pad2 = (value) => String(value).padStart(2, '0');
|
|
14
|
+
const clockTime = (date) => `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
15
|
+
const shortId = (id) => id.slice(0, 8);
|
|
16
|
+
const duration = (ms) => {
|
|
17
|
+
const minutes = Math.max(0, Math.round(ms / 60_000));
|
|
18
|
+
return minutes < 60
|
|
19
|
+
? `${minutes} min`
|
|
20
|
+
: `${Math.floor(minutes / 60)} h ${pad2(minutes % 60)} min`;
|
|
21
|
+
};
|
|
22
|
+
/** Where the run is: the summary with `Step 3 of 4` shortened to `3/4`. */
|
|
23
|
+
export const stepText = (execution) => execution.progress
|
|
24
|
+
? execution.progress.summary.replace(/^Step (\d+) of (\d+) · /, '$1/$2 ')
|
|
25
|
+
: execution.status;
|
|
26
|
+
/**
|
|
27
|
+
* Why a run stopped, in plain words. The backend sends V4 findings as a
|
|
28
|
+
* Python-style list (`[{'code': ..., 'message': ...}]`); each becomes
|
|
29
|
+
* `message (code)`.
|
|
30
|
+
*/
|
|
31
|
+
export const failureText = (execution) => plainError(execution.error?.message ??
|
|
32
|
+
execution.progress?.outcome?.detail ??
|
|
33
|
+
execution.history.error ??
|
|
34
|
+
execution.status);
|
|
35
|
+
const plainError = (message, code) => {
|
|
36
|
+
const findings = [...message.matchAll(/\{([^{}]*)\}/g)].flatMap(([, body]) => {
|
|
37
|
+
const field = (name) => new RegExp(`['"]${name}['"]:\\s*['"]([^'"]*)['"]`).exec(body)?.[1];
|
|
38
|
+
const text = field('message');
|
|
39
|
+
return text
|
|
40
|
+
? [`${text}${field('code') ? ` (${field('code')})` : ''}`]
|
|
41
|
+
: [];
|
|
42
|
+
});
|
|
43
|
+
if (message.trim().startsWith('[') && findings.length)
|
|
44
|
+
return findings.join('; ');
|
|
45
|
+
return code ? `${message} (${code})` : message;
|
|
46
|
+
};
|
|
47
|
+
const isFailure = (status) => status === 'failed' || status === 'cancelled';
|
|
48
|
+
const stoppedAt = (lastStep) => lastStep && stepNames[lastStep]
|
|
49
|
+
? `at ${lastStep}/4 ${stepNames[lastStep]} · `
|
|
50
|
+
: '';
|
|
51
|
+
const terminalText = (execution, status, lastStep) => {
|
|
52
|
+
if (isFailure(status)) {
|
|
53
|
+
return `✗ FAILED · ${stoppedAt(lastStep)}${failureText(execution)}`;
|
|
54
|
+
}
|
|
55
|
+
if (status === 'completed')
|
|
56
|
+
return `✓ done · ${stepText(execution)}`;
|
|
57
|
+
if (status === 'needs_review')
|
|
58
|
+
return `⚠ needs review · ${stepText(execution)}`;
|
|
59
|
+
return `⚠ ${status} · ${stepText(execution)}`;
|
|
60
|
+
};
|
|
61
|
+
const isTerminal = (status) => !['queued', 'running', 'dispatched'].includes(status);
|
|
62
|
+
/** The step a still-working run is on, to say later where it stopped. */
|
|
63
|
+
const workingStep = (execution, status) => !isTerminal(status) ? execution.progress?.step : undefined;
|
|
64
|
+
/** Parts that became kept-with-an-issue or failed since the last report. */
|
|
65
|
+
const newIssues = (previous, next) => {
|
|
66
|
+
const before = new Map(previous?.progress?.parts.map(part => [part.label, part.state]));
|
|
67
|
+
return (next.progress?.parts ?? [])
|
|
68
|
+
.filter(part => (part.state === 'kept' || part.state === 'failed') &&
|
|
69
|
+
before.get(part.label) !== part.state)
|
|
70
|
+
.map(part => `${part.state === 'kept' ? '⚠ kept with an issue' : '✗ part needs a person'}: ${part.label}${part.note ? ` · ${part.note}` : ''}`);
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The lines one run's new receipt adds: its step or its end when either
|
|
74
|
+
* changed, then any part that newly needs attention. `status` is the batch
|
|
75
|
+
* item's status when it differs from the receipt's; `lastStep` the last step
|
|
76
|
+
* seen while the run worked.
|
|
77
|
+
*/
|
|
78
|
+
const changeLines = (previous, next, status = next.status, previousStatus = previous?.status, lastStep) => {
|
|
79
|
+
const text = isTerminal(status)
|
|
80
|
+
? terminalText(next, status, lastStep)
|
|
81
|
+
: stepText(next);
|
|
82
|
+
const before = previous && previousStatus
|
|
83
|
+
? isTerminal(previousStatus)
|
|
84
|
+
? terminalText(previous, previousStatus)
|
|
85
|
+
: stepText(previous)
|
|
86
|
+
: undefined;
|
|
87
|
+
return [...(text !== before ? [text] : []), ...newIssues(previous, next)];
|
|
88
|
+
};
|
|
89
|
+
/** `runs watch`, `production analyze --wait` and friends: one run. */
|
|
90
|
+
export const runReporter = ({ write, now = () => new Date(), }) => {
|
|
91
|
+
let last;
|
|
92
|
+
let lastStep;
|
|
93
|
+
return (execution) => {
|
|
94
|
+
const lines = changeLines(last, execution, execution.status, last?.status, lastStep);
|
|
95
|
+
last = execution;
|
|
96
|
+
lastStep = workingStep(execution, execution.status) ?? lastStep;
|
|
97
|
+
for (const line of lines)
|
|
98
|
+
write(`${clockTime(now())} run ${shortId(execution.runId)} ${line}\n`);
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* `production batch`: a header naming each image by letter (A, B, …; `A#2`
|
|
103
|
+
* for the second repeat), a line per real change, and a status table.
|
|
104
|
+
*/
|
|
105
|
+
export const batchReporter = (options) => {
|
|
106
|
+
const { write, images } = options;
|
|
107
|
+
const now = options.now ?? (() => new Date());
|
|
108
|
+
const repeats = images.some(image => image.repeat > 1);
|
|
109
|
+
const letter = (image) => `${image.imageIndex < 26 ? String.fromCharCode(65 + image.imageIndex) : image.imageIndex + 1}${repeats ? `#${image.repeat}` : ''}`;
|
|
110
|
+
const letterWidth = Math.max(...images.map(image => letter(image).length));
|
|
111
|
+
const nameWidth = Math.max(...images.map(image => image.name.length));
|
|
112
|
+
const who = (key) => {
|
|
113
|
+
const image = images.find(entry => entry.key === key);
|
|
114
|
+
return `${letter(image).padEnd(letterWidth)} ${image.name.padEnd(nameWidth)}`;
|
|
115
|
+
};
|
|
116
|
+
const state = new Map();
|
|
117
|
+
const say = (key, line) => write(`${clockTime(now())} ${who(key)} ${line}\n`);
|
|
118
|
+
const header = () => {
|
|
119
|
+
const runs = `${images.length} run${images.length === 1 ? '' : 's'}${options.concurrency ? `, ${options.concurrency} at a time` : ''}`;
|
|
120
|
+
write([
|
|
121
|
+
`Batch ${shortId(options.operationId)} · ${options.agent} · ${runs} · canvas ${options.canvasId}`,
|
|
122
|
+
...images
|
|
123
|
+
.filter(image => !repeats || image.repeat === 1)
|
|
124
|
+
.map(image => ` ${String.fromCharCode(65 + image.imageIndex).padEnd(1)} ${image.name}`),
|
|
125
|
+
` Resume with: --operation-id ${options.operationId}`,
|
|
126
|
+
'',
|
|
127
|
+
'',
|
|
128
|
+
].join('\n'));
|
|
129
|
+
};
|
|
130
|
+
const item = (result) => {
|
|
131
|
+
const entry = state.get(result.key);
|
|
132
|
+
const previous = entry?.result;
|
|
133
|
+
const lines = [];
|
|
134
|
+
if (result.execution) {
|
|
135
|
+
if (!previous?.execution)
|
|
136
|
+
lines.push(isTerminal(result.status)
|
|
137
|
+
? terminalText(result.execution, result.status)
|
|
138
|
+
: `started run ${shortId(result.execution.runId)} · ${stepText(result.execution)}`, ...newIssues(undefined, result.execution));
|
|
139
|
+
else
|
|
140
|
+
lines.push(...changeLines(previous.execution, result.execution, result.status, previous.status, entry?.lastStep));
|
|
141
|
+
}
|
|
142
|
+
else if (result.status === 'deferred') {
|
|
143
|
+
lines.push(`waiting for a free run slot · ${result.message ?? result.reason ?? ''}`);
|
|
144
|
+
}
|
|
145
|
+
else if (result.status === 'failed') {
|
|
146
|
+
lines.push(`✗ could not start · ${plainError(result.message ?? 'failed', result.code)}`);
|
|
147
|
+
}
|
|
148
|
+
const next = {
|
|
149
|
+
result,
|
|
150
|
+
startedAt: entry?.startedAt ??
|
|
151
|
+
(result.execution ? new Date(result.execution.createdAt) : undefined),
|
|
152
|
+
endedAt: entry?.endedAt ??
|
|
153
|
+
(result.execution && isTerminal(result.status) ? now() : undefined),
|
|
154
|
+
lastLine: entry?.lastLine,
|
|
155
|
+
lastStep: (result.execution && workingStep(result.execution, result.status)) ??
|
|
156
|
+
entry?.lastStep,
|
|
157
|
+
};
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (line === next.lastLine)
|
|
160
|
+
continue;
|
|
161
|
+
next.lastLine = line;
|
|
162
|
+
say(result.key, line);
|
|
163
|
+
}
|
|
164
|
+
state.set(result.key, next);
|
|
165
|
+
};
|
|
166
|
+
/** Every image's state now: printed every few minutes and at the end. */
|
|
167
|
+
const table = (title) => {
|
|
168
|
+
const rows = images.map(image => {
|
|
169
|
+
const entry = state.get(image.key);
|
|
170
|
+
const result = entry?.result;
|
|
171
|
+
const execution = result?.execution;
|
|
172
|
+
const [mark, detail] = !result
|
|
173
|
+
? ['○ not started', '']
|
|
174
|
+
: execution
|
|
175
|
+
? isFailure(result.status)
|
|
176
|
+
? [
|
|
177
|
+
'✗ failed',
|
|
178
|
+
`${entry?.lastStep ? `at ${entry.lastStep}/4 · ` : ''}${failureText(execution)}`,
|
|
179
|
+
]
|
|
180
|
+
: result.status === 'completed'
|
|
181
|
+
? ['✓ done', stepText(execution)]
|
|
182
|
+
: isTerminal(result.status)
|
|
183
|
+
? [`⚠ ${result.status.replace('_', ' ')}`, stepText(execution)]
|
|
184
|
+
: ['● running', stepText(execution)]
|
|
185
|
+
: result.status === 'deferred'
|
|
186
|
+
? ['… waiting', result.message ?? result.reason ?? '']
|
|
187
|
+
: result.status === 'failed'
|
|
188
|
+
? [
|
|
189
|
+
'✗ failed',
|
|
190
|
+
plainError(result.message ?? 'failed', result.code),
|
|
191
|
+
]
|
|
192
|
+
: ['○ not started', ''];
|
|
193
|
+
const took = entry?.startedAt && !Number.isNaN(entry.startedAt.getTime())
|
|
194
|
+
? ` ${duration((entry.endedAt ?? now()).getTime() - entry.startedAt.getTime())}`
|
|
195
|
+
: '';
|
|
196
|
+
return ` ${who(image.key)} ${mark.padEnd(12)}${detail}${took}`;
|
|
197
|
+
});
|
|
198
|
+
write(['', `── ${clockTime(now())} · ${title} ──`, ...rows, '', ''].join('\n'));
|
|
199
|
+
};
|
|
200
|
+
return { header, item, table };
|
|
201
|
+
};
|