@assethub/cli 0.1.6 → 0.1.8

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 (47) hide show
  1. package/README.md +289 -5
  2. package/dist/canvas.d.ts +27 -0
  3. package/dist/canvas.d.ts.map +1 -0
  4. package/dist/canvas.js +82 -0
  5. package/dist/canvasAssetImport.d.ts +45 -0
  6. package/dist/canvasAssetImport.d.ts.map +1 -0
  7. package/dist/canvasAssetImport.js +154 -0
  8. package/dist/canvasComparison.d.ts +21 -0
  9. package/dist/canvasComparison.d.ts.map +1 -0
  10. package/dist/canvasComparison.js +58 -0
  11. package/dist/execution.d.ts +175 -0
  12. package/dist/execution.d.ts.map +1 -0
  13. package/dist/execution.js +234 -0
  14. package/dist/index.d.ts +94 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1567 -189
  17. package/dist/projectSources.d.ts +65 -0
  18. package/dist/projectSources.d.ts.map +1 -0
  19. package/dist/projectSources.js +369 -0
  20. package/dist/projectSourcesWorkbook.d.ts +3 -0
  21. package/dist/projectSourcesWorkbook.d.ts.map +1 -0
  22. package/dist/projectSourcesWorkbook.js +143 -0
  23. package/dist/runsUpload/artifactGraphModel.d.ts +48 -0
  24. package/dist/runsUpload/artifactGraphModel.d.ts.map +1 -0
  25. package/dist/runsUpload/artifactGraphModel.js +127 -0
  26. package/dist/runsUpload/blobRefs.d.ts +13 -0
  27. package/dist/runsUpload/blobRefs.d.ts.map +1 -0
  28. package/dist/runsUpload/blobRefs.js +73 -0
  29. package/dist/runsUpload/buildRunUpload.d.ts +63 -0
  30. package/dist/runsUpload/buildRunUpload.d.ts.map +1 -0
  31. package/dist/runsUpload/buildRunUpload.js +428 -0
  32. package/dist/runsUpload/canonicalJson.d.ts +5 -0
  33. package/dist/runsUpload/canonicalJson.d.ts.map +1 -0
  34. package/dist/runsUpload/canonicalJson.js +108 -0
  35. package/dist/runsUpload/controlEndpoints.d.ts +63 -0
  36. package/dist/runsUpload/controlEndpoints.d.ts.map +1 -0
  37. package/dist/runsUpload/controlEndpoints.js +64 -0
  38. package/dist/runsUpload/graphFolder.d.ts +25 -0
  39. package/dist/runsUpload/graphFolder.d.ts.map +1 -0
  40. package/dist/runsUpload/graphFolder.js +147 -0
  41. package/dist/runsUpload/runUploadError.d.ts +23 -0
  42. package/dist/runsUpload/runUploadError.d.ts.map +1 -0
  43. package/dist/runsUpload/runUploadError.js +24 -0
  44. package/dist/runsUpload/uploadRun.d.ts +43 -0
  45. package/dist/runsUpload/uploadRun.d.ts.map +1 -0
  46. package/dist/runsUpload/uploadRun.js +217 -0
  47. package/package.json +11 -5
package/README.md CHANGED
@@ -2,6 +2,258 @@
2
2
 
3
3
  Command line interface for AssetHub.
4
4
 
5
+ ## Agent generation with canvas history
6
+
7
+ This release uses the Internal `api_canvas_execution` gate. Run `assethub capabilities`
8
+ first. Image, mesh, parts split, and production analyze/execute/automation refuse to dispatch if canvas history is unavailable.
9
+ The current public API remains compatible with clients that omit `executionContext`.
10
+
11
+ ```sh
12
+ assethub image generate --prompt "small stylized robot" --agent my-agent --wait > image.json
13
+ CANVAS_ID=$(jq -r '.execution.canvas.id' image.json)
14
+ IMAGE_ASSET_ID=$(jq -r '.execution.outputs[0].assetId' image.json)
15
+ IMAGE_RUN_ID=$(jq -r '.execution.runId' image.json)
16
+ assethub mesh generate --source-id "$IMAGE_ASSET_ID" --canvas "$CANVAS_ID" \
17
+ --derived-from-run "$IMAGE_RUN_ID" --agent my-agent --wait > mesh.json
18
+ assethub graph show --canvas "$CANVAS_ID"
19
+ assethub runs list --canvas "$CANVAS_ID"
20
+ assethub canvas open "$CANVAS_ID"
21
+ ```
22
+
23
+ Without `--canvas`, the first generation creates one canvas per real working
24
+ directory, API origin and organization. `canvas use <id>` changes the selection.
25
+ An inaccessible selected canvas is an error; the CLI does not silently make another.
26
+ `canvas open` prints JSON and opens a browser only in a terminal.
27
+
28
+ Each generation returns `execution` with the canvas URL, run/job IDs, stable output
29
+ asset IDs, history status, and credit usage when available. History persists while
30
+ all browsers are closed. Uploaded images are promoted to owned durable assets.
31
+ Use `--input-json @request.json` for a complete request, or the existing file/stdin/
32
+ source flags. A `source create` or `files import` JSON response can be passed to
33
+ `--source-json @source.json`; generated asset IDs can be passed to `--source-id`.
34
+
35
+ The CLI saves each request before dispatch under `~/.assethub/state/operations`
36
+ (or `ASSETHUB_CLI_STATE_DIR`). Those files contain inputs, never API keys. Keep the
37
+ state directory private. `--operation-id <uuid>` supplies an operation identity;
38
+ retries and `runs resume <operation-id>` reuse its identical body and key. A new
39
+ candidate needs a new operation ID. `runs watch <run-id>` only reads status.
40
+
41
+ stdout contains final JSON; stderr contains progress. Exit codes are 0 for accepted/
42
+ completed, 1 for terminal failure/partial, 2 for input/auth/capability/budget errors,
43
+ 3 for timeout/history pending/needs review, and 130 for Ctrl-C. Timeout and Ctrl-C
44
+ leave server execution running and return known identities.
45
+
46
+ External agents can evaluate downloaded artifacts with their own tools and store
47
+ an assessment without starting another model call:
48
+
49
+ ```sh
50
+ assethub evaluations submit --canvas "$CANVAS_ID" --artifact "$IMAGE_ASSET_ID" \
51
+ --report ./evaluation.json --agent my-agent --require-pass
52
+ assethub evaluations list --canvas "$CANVAS_ID"
53
+ assethub graph lineage --canvas "$CANVAS_ID" --artifact "$IMAGE_ASSET_ID"
54
+ assethub graph export --canvas "$CANVAS_ID" --out ./graph.json
55
+ ```
56
+
57
+ `evaluation.json` requires the strict report fields `schemaVersion`,
58
+ `rubric`, `verdict: pass|fail|needs_review`, `criteria`, `referenceAssetIds`, and
59
+ `evidenceAssetIds` for comparable reports. The server records `agent_submission`
60
+ and authenticated actor provenance. This never counts as human approval.
61
+ `--require-pass` returns 1 for fail and 3 for missing/needs-review verdicts.
62
+
63
+ For example, an agent that has not inspected the output yet can submit:
64
+
65
+ ```json
66
+ {
67
+ "schemaVersion": "assethub.evaluation-submission.v1",
68
+ "rubric": {"id": "asset-quality", "version": "1"},
69
+ "verdict": "needs_review",
70
+ "criteria": [
71
+ {
72
+ "id": "visual-quality",
73
+ "verdict": "needs_review",
74
+ "reason": "Artifact inspection is pending."
75
+ }
76
+ ],
77
+ "referenceAssetIds": [],
78
+ "evidenceAssetIds": []
79
+ }
80
+ ```
81
+
82
+ Replace the verdict and reason with the actual assessment after inspecting the
83
+ artifact; reference and evidence IDs must belong to the authenticated organization.
84
+
85
+ Existing parts commands use the same canvas and durable operation storage:
86
+
87
+ ```sh
88
+ assethub parts split --source-id "$IMAGE_ASSET_ID" --canvas "$CANVAS_ID" --part-extractor V1.5 --wait
89
+ # Select only the parts to execute; --all-ready selects all ready tasks.
90
+ assethub parts split --source-id "$IMAGE_ASSET_ID" --canvas "$CANVAS_ID" --part-extractor V1.5 --all-ready --wait
91
+ # Internal V3.6.1 runs the complete graph split through one analyze receipt.
92
+ assethub production agents
93
+ assethub parts split --source-id "$IMAGE_ASSET_ID" --canvas "$CANVAS_ID" --part-extractor V3.6.1 --all-ready --wait
94
+ assethub production automation --canvas "$CANVAS_ID" --input-json @batch.json --wait
95
+ ```
96
+
97
+ `parts split` keeps separate analysis and selected-part execution phases. Each phase
98
+ has its own saved operation ID, with the execution linked to the analysis run.
99
+ For Internal `V3.6.1` (`V3.6.1 Primary Images First`), analysis runs the complete
100
+ Artifact Graph split. `--all-ready` waits for that one receipt and does not start a
101
+ second production execution. Resume an interrupted graph split with
102
+ `runs resume <operation-id>`; `--task-id`, `--mission-id`,
103
+ `--part-extraction-mode`, and classic `--order-id` continuation are unsupported.
104
+ Automation runs the existing complete batch pipeline, including mesh generation;
105
+ its `images[]` share the selected canvas. `batch.json` uses the existing automation
106
+ request shape, including `agentVersion`, `images` (owned `imageAssetId`, `uploadId`,
107
+ or an `imageUrl` to import), `config`, and optional `maxCostCredits`.
108
+ An uncertain Trigger acknowledgement remains recoverable as `needs_review` with
109
+ pending history; retrying the same operation never silently launches another run.
110
+
111
+ `evaluate list` discovers server evaluators. Server evaluation dispatch is currently
112
+ unavailable. Graph exports are bounded to 100 runs/evaluations and 1,000 nodes;
113
+ truncated exports require `--allow-truncated`. Other commands, including production run/intervene, rig and retopology,
114
+ retain their existing API behavior and do not yet create these receipts.
115
+ `autopilot --demo` is an experimental demo requiring the AssetHub monorepo and its unpublished development dependency `@assethub/autopilot-core`. Standalone project workflow commands do not require or install it. Autonomous budgeted improvement is a later stage.
116
+
117
+ ## Storyboard project workflow
118
+
119
+ These API-key commands work from the standalone CLI. Canvas/context commands use
120
+ `api_canvas_execution`; native moodboards additionally require an internal actor
121
+ with `moodboard_style_reference_enabled`. Select a key belonging to the intended
122
+ workspace before uploading source material. API keys are never part of context.
123
+
124
+ ```sh
125
+ # No API key is needed for local extraction. Optional dependencies are reported.
126
+ assethub project ingest ./client-sources --out-dir ./extracted --exclude-dir generated
127
+ assethub canvas create --name "Storyboard scene study" > canvas.json
128
+ CANVAS_ID=$(jq -r .id canvas.json)
129
+ assethub canvas import --canvas "$CANVAS_ID" --file ./extracted/storyboard.png > storyboard.json
130
+ assethub canvas import --canvas "$CANVAS_ID" --file ./character-face.png > character.json
131
+ assethub canvas import --canvas "$CANVAS_ID" --file ./primary-style.png > style.json
132
+
133
+ # Supply prompt + source text + imageAssetIds in analysis-request.json.
134
+ # Keep facts, uncertain identities and proposed backgrounds separate.
135
+ assethub language vision --input-json @analysis-request.json \
136
+ --operation-id 61e3a99c-30b7-4f41-8d57-2789ccdc7c92 --out analysis.json
137
+
138
+ # Prepare context.json from the inspected source facts and analysis.
139
+ assethub canvas context put --canvas "$CANVAS_ID" --file ./context.json --if-version 0
140
+ assethub canvas context get --canvas "$CANVAS_ID" --out ./recovered-context.json
141
+ assethub moodboard create --input-json @moodboard.json > board.json
142
+ BOARD_ID=$(jq -r .id board.json)
143
+ assethub moodboard analyze "$BOARD_ID" --wait > revision.json
144
+ REVISION_ID=$(jq -r .id revision.json)
145
+ assethub image generate --canvas "$CANVAS_ID" --context "$CANVAS_ID" \
146
+ --moodboard-revision "$REVISION_ID" --prompt "Reconstruct this storyboard as a 3D scene" \
147
+ --wait --download --out-dir ./generated > generated.json
148
+ SOURCE_ID=$(jq -r .assetId storyboard.json)
149
+ OUTPUT_ID=$(jq -r '.execution.outputs[0].assetId' generated.json)
150
+ assethub canvas compare --canvas "$CANVAS_ID" --source-id "$SOURCE_ID" \
151
+ --asset-id "$OUTPUT_ID" --out ./comparison.html
152
+ assethub evaluations submit --canvas "$CANVAS_ID" --artifact "$OUTPUT_ID" \
153
+ --report ./review.json --agent my-agent
154
+ ```
155
+
156
+ `project ingest` extracts PDF text/pages, workbook cells and embedded images,
157
+ representative video frames, and image/text sources. It writes `summary.json`,
158
+ `text.md` and `images.json` with source paths, hashes and extraction metadata.
159
+ Dependencies (Poppler, ffmpeg and Python 3) are detected, never installed. Partial
160
+ extraction exits 3 and reports missing dependencies; filenames do not establish
161
+ whether a picture is a storyboard or a 3D reference. Original files stay unchanged.
162
+
163
+ A context document is an extensible JSON object:
164
+
165
+ ```json
166
+ {
167
+ "schemaVersion": 1,
168
+ "title": "EP013 s150",
169
+ "instructions": {
170
+ "must": ["Preserve the original character face and pose"],
171
+ "avoid": ["Generic replacement characters"]
172
+ },
173
+ "sources": [
174
+ {
175
+ "key": "storyboard",
176
+ "remoteAssetId": "<owned-image-uuid>",
177
+ "generationUse": "required"
178
+ }
179
+ ],
180
+ "generationPlan": {"requiredSourceKeys": ["storyboard"]},
181
+ "sourceFacts": [
182
+ "Facts established by the original storyboard and story text"
183
+ ],
184
+ "inferences": [
185
+ {"proposal": "Background inferred from story", "status": "proposed"}
186
+ ],
187
+ "reviewState": {"status": "awaiting_creator_review"}
188
+ }
189
+ ```
190
+
191
+ `context get --out` writes the document for editing and reuse; stdout contains its
192
+ version/hash envelope. Updating changed content requires `--if-version N` from the
193
+ last read. Image generation pins the current version by default; `--context-version`
194
+ and repeated `--context-source` explicitly select a version and source keys. Required
195
+ references, labelled instructions and source facts are added by the server, and their
196
+ IDs/version/hash are recorded on the run. Retry an uncertain generation with
197
+ `runs resume <operation-id>` to preserve the original pinned inputs.
198
+
199
+ `moodboard.json` contains `name`, `assetIds` (maximum 12),
200
+ `representativeAssetIds` (maximum 3, all included in assetIds), and optional
201
+ `userNote`. `update <board-id> --input-json @file` creates a new immutable revision;
202
+ `get`, `list`, and `archive` manage existing boards. Analysis uses the existing
203
+ 8-credit vision plan for a new attempt, resumes an active job, and reuses a ready
204
+ revision. The combined generation limit is **6 content and style images**: five identity/storyboard references leave room
205
+ for one style representative.
206
+
207
+ Image import persists upload identity before registration and recovers the same
208
+ asset after an uncertain response; `--upload-id` resumes a known completed upload.
209
+ Identical moodboard creation input uses the same operation identity; supply a fresh
210
+ `--operation-id` when intentionally creating another identical board. Language
211
+ commands require a UUID operation ID: reuse it with the same input after a timeout.
212
+
213
+ Comparisons are portable HTML with embedded image bytes and a SHA-256 provenance
214
+ sidecar. They remain `awaiting_creator_review`; an agent evaluation is not creator
215
+ approval. Native project assets and generation history are saved in AssetHub.
216
+ `canvas layout` appends native image nodes and source/output edges through the
217
+ authoritative canvas room (Internal rollout). It preserves matching existing nodes
218
+ and rejects identity collisions. `canvas import` alone preserves an asset without
219
+ changing layout. Use the owned asset IDs returned by import or generation, including `image_...` IDs.
220
+ For example, create `layout.json` with those IDs:
221
+
222
+ ```json
223
+ {
224
+ "images": [
225
+ {
226
+ "assetId": "<storyboard-asset-id>",
227
+ "role": "source",
228
+ "x": 0,
229
+ "y": 0,
230
+ "name": "Original storyboard"
231
+ },
232
+ {
233
+ "assetId": "<generated-asset-id>",
234
+ "role": "output",
235
+ "x": 600,
236
+ "y": 0,
237
+ "name": "Generated candidate — review pending"
238
+ }
239
+ ],
240
+ "connections": [
241
+ {
242
+ "sourceAssetId": "<storyboard-asset-id>",
243
+ "targetAssetId": "<generated-asset-id>"
244
+ }
245
+ ]
246
+ }
247
+ ```
248
+
249
+ ```sh
250
+ assethub canvas layout --canvas "$CANVAS_ID" --input-json @layout.json
251
+ ```
252
+
253
+ Legacy `workspace --internal` commands and the experimental autopilot demo are
254
+ not available in the standalone distribution. Use the authenticated public
255
+ `canvas`, `moodboard`, and `canvas context` commands described above.
256
+
5
257
  ```sh
6
258
  export ASSETHUB_API_KEY=ah_live_xxx
7
259
 
@@ -13,8 +265,39 @@ assethub mesh generate --file ./input.png --wait
13
265
  assethub jobs watch job_123 --download --out-dir ./out/job
14
266
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
15
267
  assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
268
+ assethub runs upload ./out/pluffy-run-2026-07-30 --dry-run
16
269
  ```
17
270
 
271
+ Uploading a local artifact-graph run:
272
+
273
+ ```sh
274
+ # Validate the run folder without sending anything.
275
+ assethub runs upload ./out/pluffy-run-2026-07-30 --dry-run
276
+
277
+ # Upload it. Blobs go first, one at a time, then the snapshot.
278
+ assethub runs upload ./out/pluffy-run-2026-07-30 --tag pluffy --description "V3 humanoid"
279
+ ```
280
+
281
+ `<path>` is an `ag.graph-folder.v1` folder — the directory holding
282
+ `manifest.json`, `nodes.jsonl`, `edges.jsonl` and `blobs/`. The wire format is
283
+ `ag.registry.v1`, the same document the artifact-graph registry accepts, so one
284
+ folder can be published to either destination without conversion.
285
+
286
+ - `--dry-run` runs every local check and prints what _would_ be sent. A folder
287
+ whose manifest no longer matches its own `nodes.jsonl`, whose blob table
288
+ disagrees with the graph, or which exceeds the per-request size ceilings is
289
+ rejected here rather than halfway through an upload.
290
+ - Uploads are resumable. Each blob is checked against the server before it is
291
+ sent, and blob storage is content-addressed, so re-running the identical
292
+ command after a failure sends only what is still missing.
293
+ - `--graph-id` / `--stream-id` / `--rev` override the identity derived from the
294
+ folder name and `manifest.source`. Re-uploading the same revision with
295
+ different content is refused; bump `--rev` to publish a new revision.
296
+ - `--skip-register` skips the graph-registration call for a graph that already
297
+ exists.
298
+ - Requires an internal account whose org has the Production Control
299
+ entitlement; other keys get "cannot upload runs to Production Control".
300
+
18
301
  Configuration:
19
302
 
20
303
  - `ASSETHUB_API_KEY`: optional when a profile was saved with `auth login`.
@@ -79,12 +362,13 @@ is preferred. Use `--all-ready` rather than explicit `--task-id` values when
79
362
  preprocessing is enabled, because the generated-image split creates a separate
80
363
  production order.
81
364
 
82
- Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
83
- only when the `v2` image endpoint is available in the target environment.
365
+ Image generation uses the `v2` endpoint with mandatory canvas history.
366
+ `--api-version v1` is rejected; run `assethub capabilities` to check availability.
84
367
 
85
- Part extraction uses the same public names shown in the AssetHub product:
86
- `V1`, `V1.5 alpha`, `V1.5`, `V2.0 alpha`, and `V2.1 alpha`. Internal agent
87
- IDs are intentionally not part of the public CLI interface.
368
+ Part extraction uses the same names shown in the AssetHub product: `V1.5`,
369
+ `V2.0 alpha`, `V2.1 alpha`, and the Internal-only `V3.6.1` alias for
370
+ `V3.6.1 Primary Images First`. Retired names are no longer accepted by the CLI.
371
+ Internal agent IDs are intentionally not part of the CLI interface.
88
372
 
89
373
  The CLI only uses AssetHub's public API surface and is designed so this package
90
374
  can be published independently from the private application monorepo.
@@ -0,0 +1,27 @@
1
+ import type { Canvas } from '@assethub/api-client';
2
+ type CanvasScope = {
3
+ stateDir: string;
4
+ cwd: string;
5
+ baseUrl: string;
6
+ ownerId: string;
7
+ };
8
+ type CanvasClient = {
9
+ v2: {
10
+ createCanvas: (body: {
11
+ name: string;
12
+ }, options: {
13
+ idempotencyKey: string;
14
+ }) => Promise<Canvas>;
15
+ getCanvas: (id: number) => Promise<Canvas>;
16
+ };
17
+ };
18
+ export declare const writeState: (path: string, value: unknown) => Promise<void>;
19
+ export declare const readState: (path: string) => Promise<unknown | undefined>;
20
+ export declare const saveCanvasSelection: (scope: CanvasScope, canvasId: number) => Promise<void>;
21
+ export declare const resolveCanvasSelection: (options: CanvasScope & {
22
+ client: CanvasClient;
23
+ canvasId?: number;
24
+ create?: boolean;
25
+ }) => Promise<Canvas>;
26
+ export {};
27
+ //# sourceMappingURL=canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,sBAAsB,CAAA;AAEhD,KAAK,WAAW,GAAG;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACD,KAAK,YAAY,GAAG;IAClB,EAAE,EAAE;QACF,YAAY,EAAE,CACZ,IAAI,EAAE;YAAC,IAAI,EAAE,MAAM,CAAA;SAAC,EACpB,OAAO,EAAE;YAAC,cAAc,EAAE,MAAM,CAAA;SAAC,KAC9B,OAAO,CAAC,MAAM,CAAC,CAAA;QACpB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;KAC3C,CAAA;CACF,CAAA;AAED,eAAO,MAAM,UAAU,GACrB,MAAM,MAAM,EACZ,OAAO,OAAO,KACb,OAAO,CAAC,IAAI,CAWd,CAAA;AAED,eAAO,MAAM,SAAS,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAazE,CAAA;AAiBD,eAAO,MAAM,mBAAmB,GAC9B,OAAO,WAAW,EAClB,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAOd,CAAA;AAED,eAAO,MAAM,sBAAsB,GACjC,SAAS,WAAW,GAAG;IACrB,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,KACA,OAAO,CAAC,MAAM,CA8ChB,CAAA"}
package/dist/canvas.js ADDED
@@ -0,0 +1,82 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, realpath, rename, unlink, writeFile, } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
4
+ export const writeState = async (path, value) => {
5
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
6
+ const temporary = `${path}.${randomUUID()}.tmp`;
7
+ try {
8
+ await writeFile(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
9
+ await rename(temporary, path);
10
+ }
11
+ finally {
12
+ await unlink(temporary).catch(error => {
13
+ if (error.code !== 'ENOENT')
14
+ throw error;
15
+ });
16
+ }
17
+ };
18
+ export const readState = async (path) => {
19
+ try {
20
+ return JSON.parse(await readFile(path, 'utf8'));
21
+ }
22
+ catch (error) {
23
+ if (error &&
24
+ typeof error === 'object' &&
25
+ 'code' in error &&
26
+ error.code === 'ENOENT')
27
+ return undefined;
28
+ throw error;
29
+ }
30
+ };
31
+ const scopeHash = async (scope) => createHash('sha256')
32
+ .update(JSON.stringify([
33
+ scope.baseUrl.replace(/\/+$/, ''),
34
+ scope.ownerId,
35
+ await realpath(scope.cwd),
36
+ ]))
37
+ .digest('hex');
38
+ /** A stable operation identity lets the server deduplicate even a crashed first invocation. */
39
+ const creationId = (hash) => `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
40
+ export const saveCanvasSelection = async (scope, canvasId) => {
41
+ if (!Number.isSafeInteger(canvasId) || canvasId <= 0)
42
+ throw new Error('Canvas ID must be a positive integer');
43
+ await writeState(join(scope.stateDir, 'canvases', `${await scopeHash(scope)}.json`), { canvasId });
44
+ };
45
+ export const resolveCanvasSelection = async (options) => {
46
+ const { client } = options;
47
+ let canvasId = options.canvasId;
48
+ const hash = await scopeHash(options);
49
+ if (canvasId == null) {
50
+ const stored = await readState(join(options.stateDir, 'canvases', `${hash}.json`));
51
+ if (stored != null) {
52
+ if (typeof stored !== 'object' ||
53
+ !('canvasId' in stored) ||
54
+ !Number.isSafeInteger(stored.canvasId)) {
55
+ throw new Error('Invalid saved canvas selection; select one with canvas use <id>');
56
+ }
57
+ canvasId = Number(stored.canvasId);
58
+ }
59
+ }
60
+ if (canvasId != null) {
61
+ if (!Number.isSafeInteger(canvasId) || canvasId <= 0)
62
+ throw new Error('Canvas ID must be a positive integer');
63
+ const canvas = await client.v2.getCanvas(canvasId);
64
+ if (canvas.ownerId !== options.ownerId)
65
+ throw new Error('Canvas belongs to a different organization');
66
+ return canvas;
67
+ }
68
+ if (options.create === false)
69
+ throw new Error('No canvas selected; use --canvas <id> or canvas use <id>');
70
+ // Check local persistence before starting a remote mutation. The same scope
71
+ // always posts the same name and key, including after a lost response.
72
+ const directory = join(options.stateDir, 'canvases');
73
+ await mkdir(directory, { recursive: true, mode: 0o700 });
74
+ const probe = join(directory, `${hash}.${randomUUID()}.probe`);
75
+ await writeFile(probe, '', { mode: 0o600, flag: 'wx' });
76
+ await unlink(probe);
77
+ const canvas = await client.v2.createCanvas({ name: `CLI ${basename(await realpath(options.cwd))} ${hash.slice(0, 8)}` }, { idempotencyKey: creationId(hash) });
78
+ if (canvas.ownerId !== options.ownerId)
79
+ throw new Error('Created canvas belongs to a different organization');
80
+ await saveCanvasSelection(options, canvas.id);
81
+ return canvas;
82
+ };
@@ -0,0 +1,45 @@
1
+ import type { Source } from '@assethub/api-client';
2
+ type DurableSource = Extract<Source, {
3
+ uploadId: string;
4
+ } | {
5
+ resourceId: string;
6
+ }>;
7
+ type ImportedAsset = {
8
+ assetId: string;
9
+ mediaType: 'image';
10
+ canvasId: number;
11
+ };
12
+ type ImportClient = {
13
+ v2: {
14
+ uploadFile: (input: {
15
+ file: Blob;
16
+ fileName: string;
17
+ mediaType: 'image';
18
+ }) => Promise<{
19
+ uploadId?: string;
20
+ }>;
21
+ importCanvasAsset: (canvasId: number, input: {
22
+ source: DurableSource;
23
+ name?: string;
24
+ }) => Promise<ImportedAsset>;
25
+ };
26
+ };
27
+ type ImportOptions = {
28
+ client: ImportClient;
29
+ baseUrl: string;
30
+ ownerId: string;
31
+ canvasId: number;
32
+ stateDir: string;
33
+ filePath?: string;
34
+ contentType?: string;
35
+ source?: DurableSource;
36
+ name?: string;
37
+ };
38
+ /** Keep staging/import receipts separate from paid generation operation state. */
39
+ export declare function importCanvasAssetWithState(options: ImportOptions): Promise<ImportedAsset & {
40
+ statePath: string;
41
+ sha256?: string;
42
+ uploadId?: string;
43
+ }>;
44
+ export {};
45
+ //# sourceMappingURL=canvasAssetImport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasAssetImport.d.ts","sourceRoot":"","sources":["../src/canvasAssetImport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,sBAAsB,CAAA;AAGhD,KAAK,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,GAAG;IAAC,UAAU,EAAE,MAAM,CAAA;CAAC,CAAC,CAAA;AAC/E,KAAK,aAAa,GAAG;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAA;AAC5E,KAAK,YAAY,GAAG;IAClB,EAAE,EAAE;QACF,UAAU,EAAE,CAAC,KAAK,EAAE;YAClB,IAAI,EAAE,IAAI,CAAA;YACV,QAAQ,EAAE,MAAM,CAAA;YAChB,SAAS,EAAE,OAAO,CAAA;SACnB,KAAK,OAAO,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAC,CAAC,CAAA;QAClC,iBAAiB,EAAE,CACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE;YAAC,MAAM,EAAE,aAAa,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAC,KAC1C,OAAO,CAAC,aAAa,CAAC,CAAA;KAC5B,CAAA;CACF,CAAA;AAaD,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAcD,kFAAkF;AAClF,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,aAAa,GACrB,OAAO,CACR,aAAa,GAAG;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,CACxE,CAwJA"}
@@ -0,0 +1,154 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, open, readFile, unlink } from 'node:fs/promises';
3
+ import { basename, dirname, extname, join, resolve } from 'node:path';
4
+ import { readState, writeState } from './canvas.js';
5
+ const imageContentTypes = {
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.webp': 'image/webp',
10
+ '.gif': 'image/gif',
11
+ '.avif': 'image/avif',
12
+ '.bmp': 'image/bmp',
13
+ '.tif': 'image/tiff',
14
+ '.tiff': 'image/tiff',
15
+ };
16
+ /** Keep staging/import receipts separate from paid generation operation state. */
17
+ export async function importCanvasAssetWithState(options) {
18
+ if (!Number.isSafeInteger(options.canvasId) ||
19
+ options.canvasId <= 0 ||
20
+ !options.ownerId)
21
+ throw new Error('An owned canvas is required for image import');
22
+ if (Boolean(options.filePath) === Boolean(options.source))
23
+ throw new Error('Provide exactly one file, upload ID, or resource ID');
24
+ const name = options.name?.trim();
25
+ if (name !== undefined && (!name || name.length > 200))
26
+ throw new Error('Import name must be between 1 and 200 characters');
27
+ const bytes = options.filePath
28
+ ? await readFile(resolve(options.filePath))
29
+ : undefined;
30
+ const sha256 = bytes
31
+ ? createHash('sha256').update(bytes).digest('hex')
32
+ : undefined;
33
+ const sourceIdentity = sha256
34
+ ? `sha256:${sha256}`
35
+ : options.source?.uploadId
36
+ ? `upload:${options.source.uploadId}`
37
+ : options.source?.resourceId
38
+ ? `resource:${options.source.resourceId}`
39
+ : undefined;
40
+ if (!sourceIdentity)
41
+ throw new Error('Import source ID is required');
42
+ const scope = {
43
+ baseUrl: options.baseUrl.replace(/\/+$/, ''),
44
+ ownerId: options.ownerId,
45
+ canvasId: options.canvasId,
46
+ sourceIdentity,
47
+ };
48
+ const key = createHash('sha256').update(JSON.stringify(scope)).digest('hex');
49
+ const statePath = join(options.stateDir, 'canvas-imports', `${key}.json`);
50
+ const readReceipt = async () => {
51
+ const value = await readState(statePath);
52
+ if (value === undefined)
53
+ return undefined;
54
+ if (!value ||
55
+ typeof value !== 'object' ||
56
+ !('schemaVersion' in value) ||
57
+ value.schemaVersion !== 'assethub.canvas-import.v1' ||
58
+ !('baseUrl' in value) ||
59
+ value.baseUrl !== scope.baseUrl ||
60
+ !('ownerId' in value) ||
61
+ value.ownerId !== scope.ownerId ||
62
+ !('canvasId' in value) ||
63
+ value.canvasId !== scope.canvasId ||
64
+ !('sourceIdentity' in value) ||
65
+ value.sourceIdentity !== sourceIdentity)
66
+ throw new Error(`Invalid or differently scoped import receipt: ${statePath}`);
67
+ const saved = value;
68
+ if ((saved.uploadId !== undefined &&
69
+ (typeof saved.uploadId !== 'string' || !saved.uploadId)) ||
70
+ (saved.resourceId !== undefined &&
71
+ (typeof saved.resourceId !== 'string' || !saved.resourceId)) ||
72
+ (sha256 !== undefined && saved.sha256 !== sha256))
73
+ throw new Error(`Invalid import source in receipt: ${statePath}`);
74
+ if (name !== undefined && saved.name !== name)
75
+ throw new Error(`This source has an import receipt with a different name. Reuse its original name: ${statePath}`);
76
+ return saved;
77
+ };
78
+ let saved = await readReceipt();
79
+ if (!saved?.uploadId && !saved?.resourceId) {
80
+ await mkdir(dirname(statePath), { recursive: true, mode: 0o700 });
81
+ const lockPath = `${statePath}.upload-lock`;
82
+ const lock = await open(lockPath, 'wx', 0o600).catch(error => {
83
+ if (error &&
84
+ typeof error === 'object' &&
85
+ 'code' in error &&
86
+ error.code === 'EEXIST')
87
+ throw new Error(`An upload for this source is already in progress. Retry the same command after it finishes. If its process was terminated, verify the PID in ${lockPath} is stopped before removing that lock.`);
88
+ throw error;
89
+ });
90
+ try {
91
+ await lock.writeFile(JSON.stringify({ pid: process.pid }));
92
+ // Another process may have completed its upload between our initial read and lock acquisition.
93
+ saved = await readReceipt();
94
+ if (!saved?.uploadId && !saved?.resourceId) {
95
+ saved ??= {
96
+ schemaVersion: 'assethub.canvas-import.v1',
97
+ ...scope,
98
+ ...(sha256 ? { sha256 } : {}),
99
+ ...(name === undefined ? {} : { name }),
100
+ };
101
+ await writeState(statePath, saved);
102
+ if (bytes && options.filePath) {
103
+ const uploaded = await options.client.v2.uploadFile({
104
+ file: new Blob([bytes], {
105
+ type: options.contentType ??
106
+ imageContentTypes[extname(options.filePath).toLowerCase()] ??
107
+ 'application/octet-stream',
108
+ }),
109
+ fileName: basename(options.filePath),
110
+ mediaType: 'image',
111
+ });
112
+ if (!uploaded.uploadId)
113
+ throw new Error('Server did not return a durable upload ID');
114
+ saved.uploadId = uploaded.uploadId;
115
+ }
116
+ else if (options.source?.uploadId)
117
+ saved.uploadId = options.source.uploadId;
118
+ else if (options.source?.resourceId)
119
+ saved.resourceId = options.source.resourceId;
120
+ // Never call durable import before its upload identity has been persisted.
121
+ await writeState(statePath, saved);
122
+ }
123
+ }
124
+ finally {
125
+ await lock.close();
126
+ await unlink(lockPath);
127
+ }
128
+ }
129
+ const source = saved?.uploadId
130
+ ? { uploadId: saved.uploadId }
131
+ : saved?.resourceId
132
+ ? { resourceId: saved.resourceId }
133
+ : (() => {
134
+ throw new Error('Import receipt has no durable source');
135
+ })();
136
+ // Revalidate on the server even after a successful cached result. Promotion
137
+ // recognizes this same upload after staging cleanup and never creates a new asset.
138
+ const result = await options.client.v2.importCanvasAsset(options.canvasId, {
139
+ source,
140
+ name: saved.name,
141
+ });
142
+ if (result.canvasId !== options.canvasId ||
143
+ result.mediaType !== 'image' ||
144
+ !result.assetId ||
145
+ (saved.result && saved.result.assetId !== result.assetId))
146
+ throw new Error(`Server returned an inconsistent imported asset; inspect ${statePath}`);
147
+ await writeState(statePath, { ...saved, result });
148
+ return {
149
+ ...result,
150
+ statePath,
151
+ ...(sha256 ? { sha256 } : {}),
152
+ ...(saved.uploadId ? { uploadId: saved.uploadId } : {}),
153
+ };
154
+ }
@@ -0,0 +1,21 @@
1
+ import type { AssetHubClient } from '@assethub/api-client';
2
+ /** A portable review artifact; downloading uses signed URLs, never the API key. */
3
+ export declare function writeCanvasComparison(input: {
4
+ client: AssetHubClient;
5
+ canvasId: number;
6
+ sourceId: string;
7
+ assetId: string;
8
+ out: string;
9
+ title?: string;
10
+ }): Promise<{
11
+ canvasId: number;
12
+ canvasUrl: string;
13
+ reviewStatus: string;
14
+ images: {
15
+ assetId: string;
16
+ sha256: string;
17
+ }[];
18
+ path: string;
19
+ provenancePath: string;
20
+ }>;
21
+ //# sourceMappingURL=canvasComparison.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasComparison.d.ts","sourceRoot":"","sources":["../src/canvasComparison.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAA;AAWxD,mFAAmF;AACnF,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;;;;;;;;;;GAuDA"}