@assethub/cli 0.1.25 → 0.1.28
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 +104 -2
- package/dist/agentSetup.d.ts +69 -0
- package/dist/agentSetup.d.ts.map +1 -0
- package/dist/agentSetup.js +311 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +19 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -32
- package/dist/productionOneShot.d.ts +195 -0
- package/dist/productionOneShot.d.ts.map +1 -0
- package/dist/productionOneShot.js +307 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +1 -1
- package/package.json +3 -2
- package/skills/assethub/SKILL.md +151 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, independently-testable pieces of `production run --image ...`
|
|
3
|
+
* (the image -> finished-asset one-shot command). Kept separate from
|
|
4
|
+
* index.ts so the tricky bits — cost estimation, batch-status polling, the
|
|
5
|
+
* V6 compose fallback's mesh<->image pairing, and error hints — can be unit
|
|
6
|
+
* tested without spinning up an HTTP server or spawning the built CLI.
|
|
7
|
+
*
|
|
8
|
+
* This module intentionally does not import from `./index.js`: the CLI
|
|
9
|
+
* command wiring (in index.ts) calls these functions and supplies whatever
|
|
10
|
+
* they need (flags already parsed, client calls already made) rather than
|
|
11
|
+
* this module reaching back into index.ts's private helpers, which would
|
|
12
|
+
* create a circular import between the two files.
|
|
13
|
+
*/
|
|
14
|
+
import type { CanvasExecution, MeshComposerPart, ModelSummary, ProductionAutomationBatchStatusResult, ProductionOrderStatus } from '@assethub/api-client';
|
|
15
|
+
export type OneShotImageInput = {
|
|
16
|
+
kind: 'file';
|
|
17
|
+
path: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'resourceId';
|
|
20
|
+
resourceId: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* `--image <file|assetId>` auto-detection: an existing local path is treated
|
|
24
|
+
* as `--file`; anything else (an asset/resource id) is treated as
|
|
25
|
+
* `--source-id`. `exists` is injectable so this stays a pure function in
|
|
26
|
+
* tests instead of touching the real filesystem.
|
|
27
|
+
*/
|
|
28
|
+
export declare const detectOneShotImageInput: (value: string, exists: (path: string) => boolean) => OneShotImageInput;
|
|
29
|
+
export type ComposeMode = 'v6' | 'none';
|
|
30
|
+
/** Parses `--compose`. Absent stays absent (server default applies untouched). */
|
|
31
|
+
export declare const parseComposeFlag: (value: string | undefined) => ComposeMode | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The confirmed field name (assethub-web PR #8179, `web-cli-ux`) for
|
|
34
|
+
* requesting an explicit Composer V6 pass from `production automation`
|
|
35
|
+
* itself. Not `composeModel` — that was this module's pre-#8179 best guess.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PART_COMPOSER_V6_AGENT_VERSION = "part_composer_v6_auto_assemble";
|
|
38
|
+
/**
|
|
39
|
+
* Builds the `config` object for the `production automation` request.
|
|
40
|
+
*
|
|
41
|
+
* - `--compose` omitted: returns `undefined` so the request carries no
|
|
42
|
+
* `config.autoCompose` at all, leaving the server default in effect.
|
|
43
|
+
* - `--compose none`: disables automation's own compose stage entirely.
|
|
44
|
+
* - `--compose v6`: asks the server to run Composer V6 itself, in the same
|
|
45
|
+
* call, via `partComposerAgentVersion`. `autoCompose` is deliberately left
|
|
46
|
+
* unset (defaults to `true`) — the server 400s with
|
|
47
|
+
* `PART_COMPOSER_VERSION_REQUIRES_AUTO_COMPOSE` if both are sent together,
|
|
48
|
+
* so this function must never combine them. A server that predates #8179
|
|
49
|
+
* ignores the unrecognized field and runs its own default compose instead;
|
|
50
|
+
* `serverSupportsV6Compose` inspects the response afterward so the caller
|
|
51
|
+
* can still fall back to the client-side lineage-pairing compose
|
|
52
|
+
* (`buildComposerPartsFromMeshExecutions`) on that older server.
|
|
53
|
+
*/
|
|
54
|
+
export declare const buildAutomationConfig: (compose: ComposeMode | undefined) => Record<string, unknown> | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* `estimatedCostBreakdown` (added by #8179) only appears on a server new
|
|
57
|
+
* enough to also honor `partComposerAgentVersion` — its presence in the
|
|
58
|
+
* automation response is the signal that the server already did the V6
|
|
59
|
+
* compose server-side in that same call, so no client-side fallback compose
|
|
60
|
+
* is needed. Its absence means an older, currently-deployed server: it
|
|
61
|
+
* silently ignored `partComposerAgentVersion` and ran its own default
|
|
62
|
+
* compose (if any), so the caller must still run the explicit fallback.
|
|
63
|
+
*/
|
|
64
|
+
export declare const serverSupportsV6Compose: (automationResult: Record<string, unknown>) => boolean;
|
|
65
|
+
export declare const DEFAULT_MAX_PARTS = 24;
|
|
66
|
+
export type CostLineItem = {
|
|
67
|
+
total: number | null;
|
|
68
|
+
note?: string;
|
|
69
|
+
};
|
|
70
|
+
export type OneShotCostBreakdown = {
|
|
71
|
+
split: CostLineItem & {
|
|
72
|
+
perImage: number | null;
|
|
73
|
+
};
|
|
74
|
+
meshGeneration: CostLineItem & {
|
|
75
|
+
perPart: number | null;
|
|
76
|
+
maxParts: number;
|
|
77
|
+
};
|
|
78
|
+
compose: CostLineItem & {
|
|
79
|
+
perLane: number | null;
|
|
80
|
+
lanesPerImage: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export type OneShotCostEstimate = {
|
|
84
|
+
breakdown: OneShotCostBreakdown;
|
|
85
|
+
totalCredits: number;
|
|
86
|
+
notes: string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Best-effort cost breakdown for `--estimate`, shaped like the server's own
|
|
90
|
+
* `estimatedCostBreakdown` (#8179: `split`/`meshGeneration`/`compose`, each
|
|
91
|
+
* with a per-unit price and a total). Nothing here calls `production
|
|
92
|
+
* automation`/`analyze` (that would start real, billable work); every
|
|
93
|
+
* number comes from a side-effect-free GET (`models` catalog, `mesh/compose`
|
|
94
|
+
* capabilities). Today's API has no dry-run cost for part extraction/split,
|
|
95
|
+
* so that line is reported as unknown rather than guessed. `maxParts`
|
|
96
|
+
* mirrors the server's own conservative constant for this math (24, per
|
|
97
|
+
* #8179) rather than a locally invented cap; `--max-parts` overrides it.
|
|
98
|
+
*/
|
|
99
|
+
export declare const estimateOneShotCost: (options: {
|
|
100
|
+
meshModel: ModelSummary | undefined;
|
|
101
|
+
maxParts: number;
|
|
102
|
+
composeRequested: boolean;
|
|
103
|
+
composeCredits: number | null | undefined;
|
|
104
|
+
}) => OneShotCostEstimate;
|
|
105
|
+
/**
|
|
106
|
+
* Reads the server's own `estimatedTotalCredits`/`estimatedCostBreakdown`
|
|
107
|
+
* off a real (non-estimate) automation response, when present (#8179).
|
|
108
|
+
* These fields are not yet in `ProductionAutomationResult`'s shipped type,
|
|
109
|
+
* so this reads them optionally/untyped rather than guessing a schema;
|
|
110
|
+
* absent on a server that has not deployed #8179 yet.
|
|
111
|
+
*/
|
|
112
|
+
export declare const readServerCostBreakdown: (automationResult: Record<string, unknown>) => {
|
|
113
|
+
estimatedTotalCredits: number;
|
|
114
|
+
estimatedCostBreakdown: unknown;
|
|
115
|
+
} | undefined;
|
|
116
|
+
/** Whether the estimated (or actual, server-reported) total would exceed `--max-cost`. */
|
|
117
|
+
export declare const exceedsMaxCost: (totalCredits: number, maxCostCredits: number | undefined) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Human stage label for one image's `ProductionOrderStatus`. The API has no
|
|
120
|
+
* distinct "compose" status, so `ready`/`in_progress` are both reported as
|
|
121
|
+
* `mesh` (parts are being generated or are ready to be); there is no way to
|
|
122
|
+
* tell "meshing" apart from "composing" from this field alone.
|
|
123
|
+
*/
|
|
124
|
+
export declare const stageLabelForOrderStatus: (status: ProductionOrderStatus) => string;
|
|
125
|
+
/**
|
|
126
|
+
* One line per per-image stage transition, plus a batch-level completion
|
|
127
|
+
* line when the completed/failed counts change. "mesh k/N" here means
|
|
128
|
+
* images processed out of the batch, not parts within one image — the batch
|
|
129
|
+
* status endpoint does not expose per-part progress.
|
|
130
|
+
*/
|
|
131
|
+
export declare const batchProgressLines: (previous: ProductionAutomationBatchStatusResult | undefined, next: ProductionAutomationBatchStatusResult) => string[];
|
|
132
|
+
/**
|
|
133
|
+
* Polls `getStatus` (expected to be `client.v1.getProductionAutomationStatus`,
|
|
134
|
+
* which already retries 429s in its own request layer) until every image in
|
|
135
|
+
* the batch reaches a terminal outcome, printing one line per stage
|
|
136
|
+
* transition via `onProgress`. Mirrors the deadline/backoff shape of
|
|
137
|
+
* `pollProductionStatus`/`watchProductionRun` in index.ts.
|
|
138
|
+
*/
|
|
139
|
+
export declare const pollAutomationBatch: (options: {
|
|
140
|
+
getStatus: () => Promise<ProductionAutomationBatchStatusResult>;
|
|
141
|
+
sleep: (ms: number) => Promise<void>;
|
|
142
|
+
intervalMs: number;
|
|
143
|
+
timeoutMs: number;
|
|
144
|
+
onProgress?: (lines: string[]) => void;
|
|
145
|
+
}) => Promise<ProductionAutomationBatchStatusResult>;
|
|
146
|
+
/**
|
|
147
|
+
* The V6 compose fallback's mesh<->image pairing. Reads the automation
|
|
148
|
+
* batch's own `mesh.generate` executions (fetched via `listCanvasRuns` and
|
|
149
|
+
* filtered to the target order) for each part's produced mesh asset
|
|
150
|
+
* (`execution.outputs`, `mediaType === 'mesh'`) and the part image it was
|
|
151
|
+
* generated from (`execution.inputAssets`, `mediaType === 'image'`) — the
|
|
152
|
+
* same `{assetId, partImageAssetId}` shape `composer run --part
|
|
153
|
+
* <mesh>:<image>` accepts, so no manual pairing is needed.
|
|
154
|
+
*/
|
|
155
|
+
export declare const buildComposerPartsFromMeshExecutions: (executions: CanvasExecution[], orderId: string) => MeshComposerPart[];
|
|
156
|
+
/**
|
|
157
|
+
* The V6 compose fallback's `fullBodyImageAssetId`. Automation's own result
|
|
158
|
+
* does not return a durable asset id for the original image (only
|
|
159
|
+
* `orderId`/`projectId`/`url`/`runId`), so this reads it back off the part
|
|
160
|
+
* images' own `sourceAssetId` (each part image's own upstream source is the
|
|
161
|
+
* original full-body photo it was cropped from) — the same field
|
|
162
|
+
* `composerInputFromRun`'s mesh.generate part-field allowlist tracks.
|
|
163
|
+
*/
|
|
164
|
+
export declare const resolveFullBodyImageAssetId: (executions: CanvasExecution[], orderId: string) => string | undefined;
|
|
165
|
+
export type KnownErrorLike = {
|
|
166
|
+
status?: number;
|
|
167
|
+
code?: string;
|
|
168
|
+
message: string;
|
|
169
|
+
details?: {
|
|
170
|
+
meshAssetId?: string;
|
|
171
|
+
assetId?: string;
|
|
172
|
+
actualType?: string;
|
|
173
|
+
expectedType?: string;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Actionable next steps appended to (never replacing) a known
|
|
178
|
+
* `AssetHubApiError`'s own message. Matches on `error.code`/`error.details`
|
|
179
|
+
* first — `PART_IMAGE_REQUIRED` and `ASSET_WRONG_MEDIA_TYPE` are confirmed
|
|
180
|
+
* `/mesh/compose` error codes (assethub-web PR #8179) with a structured
|
|
181
|
+
* `details` payload. Falls back to matching on message content for a server
|
|
182
|
+
* that has not shipped those codes yet (e.g. today's deployed API, where
|
|
183
|
+
* the same failures still surface as a plain `404 ASSET_NOT_FOUND` or a
|
|
184
|
+
* generic validation message — "Asset not found" is quoted directly from
|
|
185
|
+
* the originally reported failure). `ASSET_NOT_FOUND` itself (asset does
|
|
186
|
+
* not exist / isn't yours) is left alone; it is not one of these cases.
|
|
187
|
+
*/
|
|
188
|
+
export declare const errorHintFor: (error: KnownErrorLike) => string | undefined;
|
|
189
|
+
/** Appends an error hint to `error.message` in place, if one applies. */
|
|
190
|
+
export declare const withErrorHint: <T extends Error & {
|
|
191
|
+
message: string;
|
|
192
|
+
}>(error: T) => T;
|
|
193
|
+
/** Condensed, human-readable lines for `runs get --summary`. */
|
|
194
|
+
export declare const formatExecutionSummaryLines: (execution: CanvasExecution) => string[];
|
|
195
|
+
//# sourceMappingURL=productionOneShot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"productionOneShot.d.ts","sourceRoot":"","sources":["../src/productionOneShot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--image <file|assetId>` auto-detection: an existing local path is treated
|
|
3
|
+
* as `--file`; anything else (an asset/resource id) is treated as
|
|
4
|
+
* `--source-id`. `exists` is injectable so this stays a pure function in
|
|
5
|
+
* tests instead of touching the real filesystem.
|
|
6
|
+
*/
|
|
7
|
+
export const detectOneShotImageInput = (value, exists) => exists(value) ? { kind: 'file', path: value } : { kind: 'resourceId', resourceId: value };
|
|
8
|
+
/** Parses `--compose`. Absent stays absent (server default applies untouched). */
|
|
9
|
+
export const parseComposeFlag = (value) => {
|
|
10
|
+
if (value == null)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (value === 'v6' || value === 'none')
|
|
13
|
+
return value;
|
|
14
|
+
throw new Error('--compose must be "v6" or "none"');
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The confirmed field name (assethub-web PR #8179, `web-cli-ux`) for
|
|
18
|
+
* requesting an explicit Composer V6 pass from `production automation`
|
|
19
|
+
* itself. Not `composeModel` — that was this module's pre-#8179 best guess.
|
|
20
|
+
*/
|
|
21
|
+
export const PART_COMPOSER_V6_AGENT_VERSION = 'part_composer_v6_auto_assemble';
|
|
22
|
+
/**
|
|
23
|
+
* Builds the `config` object for the `production automation` request.
|
|
24
|
+
*
|
|
25
|
+
* - `--compose` omitted: returns `undefined` so the request carries no
|
|
26
|
+
* `config.autoCompose` at all, leaving the server default in effect.
|
|
27
|
+
* - `--compose none`: disables automation's own compose stage entirely.
|
|
28
|
+
* - `--compose v6`: asks the server to run Composer V6 itself, in the same
|
|
29
|
+
* call, via `partComposerAgentVersion`. `autoCompose` is deliberately left
|
|
30
|
+
* unset (defaults to `true`) — the server 400s with
|
|
31
|
+
* `PART_COMPOSER_VERSION_REQUIRES_AUTO_COMPOSE` if both are sent together,
|
|
32
|
+
* so this function must never combine them. A server that predates #8179
|
|
33
|
+
* ignores the unrecognized field and runs its own default compose instead;
|
|
34
|
+
* `serverSupportsV6Compose` inspects the response afterward so the caller
|
|
35
|
+
* can still fall back to the client-side lineage-pairing compose
|
|
36
|
+
* (`buildComposerPartsFromMeshExecutions`) on that older server.
|
|
37
|
+
*/
|
|
38
|
+
export const buildAutomationConfig = (compose) => {
|
|
39
|
+
if (compose == null)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (compose === 'none')
|
|
42
|
+
return { autoCompose: false };
|
|
43
|
+
return { partComposerAgentVersion: PART_COMPOSER_V6_AGENT_VERSION };
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `estimatedCostBreakdown` (added by #8179) only appears on a server new
|
|
47
|
+
* enough to also honor `partComposerAgentVersion` — its presence in the
|
|
48
|
+
* automation response is the signal that the server already did the V6
|
|
49
|
+
* compose server-side in that same call, so no client-side fallback compose
|
|
50
|
+
* is needed. Its absence means an older, currently-deployed server: it
|
|
51
|
+
* silently ignored `partComposerAgentVersion` and ran its own default
|
|
52
|
+
* compose (if any), so the caller must still run the explicit fallback.
|
|
53
|
+
*/
|
|
54
|
+
export const serverSupportsV6Compose = (automationResult) => automationResult.estimatedCostBreakdown != null;
|
|
55
|
+
export const DEFAULT_MAX_PARTS = 24;
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort cost breakdown for `--estimate`, shaped like the server's own
|
|
58
|
+
* `estimatedCostBreakdown` (#8179: `split`/`meshGeneration`/`compose`, each
|
|
59
|
+
* with a per-unit price and a total). Nothing here calls `production
|
|
60
|
+
* automation`/`analyze` (that would start real, billable work); every
|
|
61
|
+
* number comes from a side-effect-free GET (`models` catalog, `mesh/compose`
|
|
62
|
+
* capabilities). Today's API has no dry-run cost for part extraction/split,
|
|
63
|
+
* so that line is reported as unknown rather than guessed. `maxParts`
|
|
64
|
+
* mirrors the server's own conservative constant for this math (24, per
|
|
65
|
+
* #8179) rather than a locally invented cap; `--max-parts` overrides it.
|
|
66
|
+
*/
|
|
67
|
+
export const estimateOneShotCost = (options) => {
|
|
68
|
+
const notes = [];
|
|
69
|
+
const split = {
|
|
70
|
+
perImage: null,
|
|
71
|
+
total: null,
|
|
72
|
+
note: 'part extraction/split cost has no dry-run price source today; the actual cost is reported after the run (see `production status` or `runs get --summary`)',
|
|
73
|
+
};
|
|
74
|
+
notes.push(split.note);
|
|
75
|
+
const perPart = options.meshModel?.creditCost ?? options.meshModel?.defaultCreditCost ?? null;
|
|
76
|
+
const meshTotal = perPart == null ? null : perPart * options.maxParts;
|
|
77
|
+
const meshGeneration = {
|
|
78
|
+
perPart,
|
|
79
|
+
maxParts: options.maxParts,
|
|
80
|
+
total: meshTotal,
|
|
81
|
+
...(perPart == null
|
|
82
|
+
? {
|
|
83
|
+
note: options.meshModel
|
|
84
|
+
? `mesh generation cost not included: no credit price found for model "${options.meshModel.id}"`
|
|
85
|
+
: 'mesh generation cost not included: mesh model was not found in the catalog',
|
|
86
|
+
}
|
|
87
|
+
: {
|
|
88
|
+
note: `assumes up to ${options.maxParts} parts at ${perPart} credits each; the CLI has no way to know the actual part count before splitting runs`,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
notes.push(meshGeneration.note);
|
|
92
|
+
const lanesPerImage = 1;
|
|
93
|
+
const compose = options.composeRequested
|
|
94
|
+
? options.composeCredits == null
|
|
95
|
+
? {
|
|
96
|
+
perLane: null,
|
|
97
|
+
lanesPerImage,
|
|
98
|
+
total: null,
|
|
99
|
+
note: 'compose cost not included: no quote found for the requested compose model',
|
|
100
|
+
}
|
|
101
|
+
: { perLane: options.composeCredits, lanesPerImage, total: options.composeCredits * lanesPerImage }
|
|
102
|
+
: {
|
|
103
|
+
perLane: null,
|
|
104
|
+
lanesPerImage,
|
|
105
|
+
total: 0,
|
|
106
|
+
note: 'compose disabled by --compose none',
|
|
107
|
+
};
|
|
108
|
+
if (compose.note)
|
|
109
|
+
notes.push(compose.note);
|
|
110
|
+
const totalCredits = (meshGeneration.total ?? 0) + (compose.total ?? 0);
|
|
111
|
+
return { breakdown: { split, meshGeneration, compose }, totalCredits, notes };
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Reads the server's own `estimatedTotalCredits`/`estimatedCostBreakdown`
|
|
115
|
+
* off a real (non-estimate) automation response, when present (#8179).
|
|
116
|
+
* These fields are not yet in `ProductionAutomationResult`'s shipped type,
|
|
117
|
+
* so this reads them optionally/untyped rather than guessing a schema;
|
|
118
|
+
* absent on a server that has not deployed #8179 yet.
|
|
119
|
+
*/
|
|
120
|
+
export const readServerCostBreakdown = (automationResult) => {
|
|
121
|
+
const total = automationResult.estimatedTotalCredits;
|
|
122
|
+
const breakdown = automationResult.estimatedCostBreakdown;
|
|
123
|
+
if (typeof total !== 'number' || breakdown == null)
|
|
124
|
+
return undefined;
|
|
125
|
+
return { estimatedTotalCredits: total, estimatedCostBreakdown: breakdown };
|
|
126
|
+
};
|
|
127
|
+
/** Whether the estimated (or actual, server-reported) total would exceed `--max-cost`. */
|
|
128
|
+
export const exceedsMaxCost = (totalCredits, maxCostCredits) => maxCostCredits != null && totalCredits > maxCostCredits;
|
|
129
|
+
/**
|
|
130
|
+
* Human stage label for one image's `ProductionOrderStatus`. The API has no
|
|
131
|
+
* distinct "compose" status, so `ready`/`in_progress` are both reported as
|
|
132
|
+
* `mesh` (parts are being generated or are ready to be); there is no way to
|
|
133
|
+
* tell "meshing" apart from "composing" from this field alone.
|
|
134
|
+
*/
|
|
135
|
+
export const stageLabelForOrderStatus = (status) => {
|
|
136
|
+
switch (status) {
|
|
137
|
+
case 'draft':
|
|
138
|
+
return 'queued';
|
|
139
|
+
case 'analyzing':
|
|
140
|
+
return 'split';
|
|
141
|
+
case 'ready':
|
|
142
|
+
case 'in_progress':
|
|
143
|
+
return 'mesh';
|
|
144
|
+
case 'completed':
|
|
145
|
+
return 'done';
|
|
146
|
+
case 'failed':
|
|
147
|
+
return 'failed';
|
|
148
|
+
default:
|
|
149
|
+
return status;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* One line per per-image stage transition, plus a batch-level completion
|
|
154
|
+
* line when the completed/failed counts change. "mesh k/N" here means
|
|
155
|
+
* images processed out of the batch, not parts within one image — the batch
|
|
156
|
+
* status endpoint does not expose per-part progress.
|
|
157
|
+
*/
|
|
158
|
+
export const batchProgressLines = (previous, next) => {
|
|
159
|
+
const lines = [];
|
|
160
|
+
const previousStatusByOrder = new Map((previous?.images ?? []).map(image => [image.orderId, image.status]));
|
|
161
|
+
for (const image of next.images) {
|
|
162
|
+
const before = previousStatusByOrder.get(image.orderId);
|
|
163
|
+
if (before !== image.status) {
|
|
164
|
+
lines.push(`[batch] order=${image.orderId} stage=${stageLabelForOrderStatus(image.status)} (${image.status})`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const previousSummary = previous?.summary;
|
|
168
|
+
const done = next.summary.completed + next.summary.failed;
|
|
169
|
+
if (previousSummary == null ||
|
|
170
|
+
previousSummary.completed !== next.summary.completed ||
|
|
171
|
+
previousSummary.failed !== next.summary.failed) {
|
|
172
|
+
lines.push(`[batch] mesh ${done}/${next.summary.total} done`);
|
|
173
|
+
}
|
|
174
|
+
return lines;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Polls `getStatus` (expected to be `client.v1.getProductionAutomationStatus`,
|
|
178
|
+
* which already retries 429s in its own request layer) until every image in
|
|
179
|
+
* the batch reaches a terminal outcome, printing one line per stage
|
|
180
|
+
* transition via `onProgress`. Mirrors the deadline/backoff shape of
|
|
181
|
+
* `pollProductionStatus`/`watchProductionRun` in index.ts.
|
|
182
|
+
*/
|
|
183
|
+
export const pollAutomationBatch = async (options) => {
|
|
184
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
185
|
+
let previous;
|
|
186
|
+
for (;;) {
|
|
187
|
+
const status = await options.getStatus();
|
|
188
|
+
const lines = batchProgressLines(previous, status);
|
|
189
|
+
if (lines.length && options.onProgress)
|
|
190
|
+
options.onProgress(lines);
|
|
191
|
+
previous = status;
|
|
192
|
+
if (status.summary.completed + status.summary.failed >= status.summary.total) {
|
|
193
|
+
return status;
|
|
194
|
+
}
|
|
195
|
+
if (Date.now() >= deadline) {
|
|
196
|
+
throw new Error(`production run --wait timed out after ${options.timeoutMs}ms for batch=${status.batchId}`);
|
|
197
|
+
}
|
|
198
|
+
await options.sleep(Math.min(options.intervalMs, Math.max(0, deadline - Date.now())));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* The V6 compose fallback's mesh<->image pairing. Reads the automation
|
|
203
|
+
* batch's own `mesh.generate` executions (fetched via `listCanvasRuns` and
|
|
204
|
+
* filtered to the target order) for each part's produced mesh asset
|
|
205
|
+
* (`execution.outputs`, `mediaType === 'mesh'`) and the part image it was
|
|
206
|
+
* generated from (`execution.inputAssets`, `mediaType === 'image'`) — the
|
|
207
|
+
* same `{assetId, partImageAssetId}` shape `composer run --part
|
|
208
|
+
* <mesh>:<image>` accepts, so no manual pairing is needed.
|
|
209
|
+
*/
|
|
210
|
+
export const buildComposerPartsFromMeshExecutions = (executions, orderId) => {
|
|
211
|
+
const parts = [];
|
|
212
|
+
for (const execution of executions) {
|
|
213
|
+
if (execution.operation !== 'mesh.generate')
|
|
214
|
+
continue;
|
|
215
|
+
if (!execution.orderIds.includes(orderId))
|
|
216
|
+
continue;
|
|
217
|
+
const meshOutput = execution.outputs.find(output => output.mediaType === 'mesh');
|
|
218
|
+
if (!meshOutput)
|
|
219
|
+
continue;
|
|
220
|
+
const partImage = execution.inputAssets?.find(asset => asset.mediaType === 'image');
|
|
221
|
+
parts.push({
|
|
222
|
+
assetId: meshOutput.assetId,
|
|
223
|
+
...(partImage ? { partImageAssetId: partImage.assetId } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return parts;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* The V6 compose fallback's `fullBodyImageAssetId`. Automation's own result
|
|
230
|
+
* does not return a durable asset id for the original image (only
|
|
231
|
+
* `orderId`/`projectId`/`url`/`runId`), so this reads it back off the part
|
|
232
|
+
* images' own `sourceAssetId` (each part image's own upstream source is the
|
|
233
|
+
* original full-body photo it was cropped from) — the same field
|
|
234
|
+
* `composerInputFromRun`'s mesh.generate part-field allowlist tracks.
|
|
235
|
+
*/
|
|
236
|
+
export const resolveFullBodyImageAssetId = (executions, orderId) => {
|
|
237
|
+
for (const execution of executions) {
|
|
238
|
+
if (execution.operation !== 'mesh.generate')
|
|
239
|
+
continue;
|
|
240
|
+
if (!execution.orderIds.includes(orderId))
|
|
241
|
+
continue;
|
|
242
|
+
const partImage = execution.inputAssets?.find(asset => asset.mediaType === 'image');
|
|
243
|
+
if (partImage?.sourceAssetId)
|
|
244
|
+
return partImage.sourceAssetId;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Actionable next steps appended to (never replacing) a known
|
|
250
|
+
* `AssetHubApiError`'s own message. Matches on `error.code`/`error.details`
|
|
251
|
+
* first — `PART_IMAGE_REQUIRED` and `ASSET_WRONG_MEDIA_TYPE` are confirmed
|
|
252
|
+
* `/mesh/compose` error codes (assethub-web PR #8179) with a structured
|
|
253
|
+
* `details` payload. Falls back to matching on message content for a server
|
|
254
|
+
* that has not shipped those codes yet (e.g. today's deployed API, where
|
|
255
|
+
* the same failures still surface as a plain `404 ASSET_NOT_FOUND` or a
|
|
256
|
+
* generic validation message — "Asset not found" is quoted directly from
|
|
257
|
+
* the originally reported failure). `ASSET_NOT_FOUND` itself (asset does
|
|
258
|
+
* not exist / isn't yours) is left alone; it is not one of these cases.
|
|
259
|
+
*/
|
|
260
|
+
export const errorHintFor = (error) => {
|
|
261
|
+
if (error.code === 'PART_IMAGE_REQUIRED') {
|
|
262
|
+
const meshAssetId = error.details?.meshAssetId ?? '<mesh-asset-id>';
|
|
263
|
+
return `pass --part ${meshAssetId}:<part-image-asset-id>, or regenerate with mesh generate --derived-from-run <split-run-id>`;
|
|
264
|
+
}
|
|
265
|
+
if (error.code === 'ASSET_WRONG_MEDIA_TYPE') {
|
|
266
|
+
const { assetId, actualType, expectedType } = error.details ?? {};
|
|
267
|
+
if (expectedType === 'mesh' && actualType === 'image') {
|
|
268
|
+
return `asset ${assetId ?? '<asset-id>'} is an image; run mesh generate (or production run --image) first`;
|
|
269
|
+
}
|
|
270
|
+
return `asset ${assetId ?? '<asset-id>'} has the wrong type: expected ${expectedType ?? 'unknown'}, got ${actualType ?? 'unknown'}`;
|
|
271
|
+
}
|
|
272
|
+
const message = error.message.toLowerCase();
|
|
273
|
+
if (message.includes('asset not found') || message.includes('not found')) {
|
|
274
|
+
return 'compose expects a mesh part asset id, not an image asset — run `mesh generate` (or `production run --image <file|assetId>`) first, then compose the resulting mesh asset ids';
|
|
275
|
+
}
|
|
276
|
+
if (message.includes('part image') ||
|
|
277
|
+
message.includes('partimageassetid') ||
|
|
278
|
+
message.includes('full body image') ||
|
|
279
|
+
message.includes('fullbodyimageassetid')) {
|
|
280
|
+
return "pass `--part <mesh-asset-id>:<part-image-asset-id>` on `composer run`/`composer refine` to attach each part's source image (or use `production run --image ... --compose v6`, which pairs them automatically)";
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
};
|
|
284
|
+
/** Appends an error hint to `error.message` in place, if one applies. */
|
|
285
|
+
export const withErrorHint = (error) => {
|
|
286
|
+
const hint = errorHintFor(error);
|
|
287
|
+
if (hint)
|
|
288
|
+
error.message = `${error.message} (${hint})`;
|
|
289
|
+
return error;
|
|
290
|
+
};
|
|
291
|
+
/** Condensed, human-readable lines for `runs get --summary`. */
|
|
292
|
+
export const formatExecutionSummaryLines = (execution) => {
|
|
293
|
+
const lines = [
|
|
294
|
+
`runId: ${execution.runId}`,
|
|
295
|
+
`operation: ${execution.operation}`,
|
|
296
|
+
`status: ${execution.status}`,
|
|
297
|
+
`canvas: ${execution.canvas.id}`,
|
|
298
|
+
];
|
|
299
|
+
if (execution.orderIds.length)
|
|
300
|
+
lines.push(`orders: ${execution.orderIds.join(', ')}`);
|
|
301
|
+
lines.push(`credits: reserved=${execution.usage.reservedCredits ?? '-'} charged=${execution.usage.chargedCredits ?? '-'}`);
|
|
302
|
+
const output = execution.outputs[0];
|
|
303
|
+
lines.push(`output: ${output ? output.assetId : '-'}`);
|
|
304
|
+
if (execution.error)
|
|
305
|
+
lines.push(`error: ${execution.error.code}: ${execution.error.message}`);
|
|
306
|
+
return lines;
|
|
307
|
+
};
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export declare const cliVersion: () => Promise<string>;
|
|
3
|
+
export declare const validatedBaseUrl: (baseUrl: string) => string;
|
|
3
4
|
export declare const mcpConfig: (client: string, baseUrl: string, account?: boolean, workspaceId?: string) => string;
|
|
4
5
|
type Check = {
|
|
5
6
|
name: 'api' | 'mcp' | 'configuration';
|
package/dist/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oCAAoC,CAAA;AAE5D,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,MAAM,CAKvC,CAAA;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oCAAoC,CAAA;AAE5D,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,MAAM,CAKvC,CAAA;AAEX,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAelD,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,EAAE,iBAAe,EAAE,cAAc,MAAM,KAAG,MAsBlG,CAAA;AAED,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,eAAe,CAAA;IACrC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CACf,CAAA;AAED,KAAK,IAAI,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AA2DhI,eAAO,MAAM,QAAQ,GAAU,gEAM5B;IACD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;;;;;;;;;;;;;;;;;;;;;;EAkHA,CAAA"}
|
package/dist/setup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { AssetHubApiError, createAssetHubClient, createWorkspaceClient, WorkspaceClientError } from '@assethub/api-client';
|
|
3
3
|
export const cliVersion = async () => JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
4
|
-
const validatedBaseUrl = (baseUrl) => {
|
|
4
|
+
export const validatedBaseUrl = (baseUrl) => {
|
|
5
5
|
const url = new URL(baseUrl);
|
|
6
6
|
if (url.username || url.password || url.search || url.hash)
|
|
7
7
|
throw new Error('Use an API URL without credentials, query, or fragment.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assethub/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Command line interface for AssetHub.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
"skills",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"access": "public"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@assethub/api-client": "^0.1.
|
|
34
|
+
"@assethub/api-client": "^0.1.13",
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
35
36
|
},
|
|
36
37
|
"repository": {
|