@just-every/design 0.1.21 → 0.1.23
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 +2 -1
- package/dist/artifacts-cli.d.ts +11 -0
- package/dist/artifacts-cli.d.ts.map +1 -0
- package/dist/artifacts-cli.js +53 -0
- package/dist/artifacts-cli.js.map +1 -0
- package/dist/cli.js +210 -64
- package/dist/cli.js.map +1 -1
- package/dist/design-client.d.ts +22 -0
- package/dist/design-client.d.ts.map +1 -1
- package/dist/design-client.js +6 -0
- package/dist/design-client.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +5 -8
- package/dist/install.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +161 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ Current tools are focused on runs + artifacts:
|
|
|
122
122
|
- `design.watch`
|
|
123
123
|
- `design.extractAssets` (opt-in: extract assets for interface/html runs)
|
|
124
124
|
- `design.generateHtml` (opt-in: generate HTML/CSS + assets for html runs)
|
|
125
|
+
- `design.iterate` (iterate on a run using screenshot feedback)
|
|
125
126
|
- `design.refineDraft` (non-fork: refine a specific draft within a run)
|
|
126
127
|
- `design.uploadTarget` (upload an image and set it as the run target)
|
|
127
128
|
- `design.setTarget` (select which draft should be treated as the run target)
|
|
@@ -146,7 +147,7 @@ CLI helper commands (non-MCP):
|
|
|
146
147
|
- `every-design set-target --json '{...}'`
|
|
147
148
|
- `every-design clear-target --json '{...}'`
|
|
148
149
|
- `every-design screenshot <url>`
|
|
149
|
-
- `every-design
|
|
150
|
+
- `every-design iterate --json '{"runId":"<run-id>","render":[...],"concerns":"..."}'`
|
|
150
151
|
|
|
151
152
|
## Output kinds (CLI/MCP)
|
|
152
153
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type RunArtifactRef = {
|
|
2
|
+
runId: string;
|
|
3
|
+
artifactId: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function parseRunArtifactRef(value: string): RunArtifactRef | null;
|
|
6
|
+
export declare function parseArtifactsListPositional(args: string[]): {
|
|
7
|
+
runId: string;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare function parseArtifactsDownloadPositional(args: string[]): RunArtifactRef | null;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=artifacts-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts-cli.d.ts","sourceRoot":"","sources":["../src/artifacts-cli.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAkBF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAgBxE;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUrF;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,GAAG,IAAI,CAUtF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const UUID_RE = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
|
|
2
|
+
function extractUuids(value) {
|
|
3
|
+
const matches = value.match(UUID_RE);
|
|
4
|
+
return matches ? matches.map((m) => m) : [];
|
|
5
|
+
}
|
|
6
|
+
function normalizeToken(value) {
|
|
7
|
+
return String(value ?? '').trim();
|
|
8
|
+
}
|
|
9
|
+
function looksLikeJsonObject(value) {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
return trimmed.startsWith('{');
|
|
12
|
+
}
|
|
13
|
+
export function parseRunArtifactRef(value) {
|
|
14
|
+
const raw = normalizeToken(value);
|
|
15
|
+
if (!raw)
|
|
16
|
+
return null;
|
|
17
|
+
if (looksLikeJsonObject(raw))
|
|
18
|
+
return null;
|
|
19
|
+
const uuids = extractUuids(raw);
|
|
20
|
+
if (uuids.length >= 2) {
|
|
21
|
+
return { runId: uuids[0], artifactId: uuids[1] };
|
|
22
|
+
}
|
|
23
|
+
const parts = raw.split('/').map((p) => p.trim()).filter(Boolean);
|
|
24
|
+
if (parts.length === 2) {
|
|
25
|
+
return { runId: parts[0], artifactId: parts[1] };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function parseArtifactsListPositional(args) {
|
|
30
|
+
const tokens = args.map(normalizeToken).filter(Boolean);
|
|
31
|
+
if (tokens.length !== 1)
|
|
32
|
+
return null;
|
|
33
|
+
const token = tokens[0];
|
|
34
|
+
if (looksLikeJsonObject(token))
|
|
35
|
+
return null;
|
|
36
|
+
const uuids = extractUuids(token);
|
|
37
|
+
if (uuids.length >= 1)
|
|
38
|
+
return { runId: uuids[0] };
|
|
39
|
+
return { runId: token };
|
|
40
|
+
}
|
|
41
|
+
export function parseArtifactsDownloadPositional(args) {
|
|
42
|
+
const tokens = args.map(normalizeToken).filter(Boolean);
|
|
43
|
+
if (tokens.length === 1) {
|
|
44
|
+
return parseRunArtifactRef(tokens[0]);
|
|
45
|
+
}
|
|
46
|
+
if (tokens.length === 2) {
|
|
47
|
+
if (looksLikeJsonObject(tokens[0]) || looksLikeJsonObject(tokens[1]))
|
|
48
|
+
return null;
|
|
49
|
+
return { runId: tokens[0], artifactId: tokens[1] };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=artifacts-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts-cli.js","sourceRoot":"","sources":["../src/artifacts-cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
7
|
import { existsSync, statSync } from 'node:fs';
|
|
8
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { runApprovalLinkLoginFlow } from './auth.js';
|
|
13
13
|
import { refreshLoginSession } from './auth.js';
|
|
14
14
|
import { pickAccountSlug } from './account-picker.js';
|
|
15
|
+
import { parseArtifactsDownloadPositional, parseArtifactsListPositional } from './artifacts-cli.js';
|
|
15
16
|
import { clearConfig, readConfig, resolveConfigPath, writeConfig } from './config.js';
|
|
16
17
|
import { DesignAppClient } from './design-client.js';
|
|
17
18
|
import { parseJsonOrThrow } from './json.js';
|
|
@@ -66,7 +67,8 @@ function printHelp() {
|
|
|
66
67
|
console.error(' extract-assets Trigger asset extraction for a run (CLI helper)');
|
|
67
68
|
console.error(' generate-html Trigger HTML generation for a run (CLI helper)');
|
|
68
69
|
console.error(' screenshot <url> Screenshot a URL (CLI helper)');
|
|
69
|
-
console.error('
|
|
70
|
+
console.error(' iterate Iterate on a run using screenshot feedback (CLI helper)');
|
|
71
|
+
console.error(' critique Alias for `iterate`');
|
|
70
72
|
console.error(' watch Watch a design run (CLI helper)');
|
|
71
73
|
console.error(' share status Get run share link status (CLI helper)');
|
|
72
74
|
console.error(' share enable Enable (or rotate) a share link (CLI helper)');
|
|
@@ -74,8 +76,8 @@ function printHelp() {
|
|
|
74
76
|
console.error(' get Fetch a run by id (CLI helper)');
|
|
75
77
|
console.error(' list List recent runs (CLI helper)');
|
|
76
78
|
console.error(' events Fetch run events (CLI helper)');
|
|
77
|
-
console.error(' artifacts list
|
|
78
|
-
console.error(' artifacts download
|
|
79
|
+
console.error(' artifacts list <runId> List run artifacts (CLI helper)');
|
|
80
|
+
console.error(' artifacts download <runId>/<artifactId> Download an artifact (CLI helper)');
|
|
79
81
|
console.error(' auth login Login via approval-link flow and save config');
|
|
80
82
|
console.error(' auth status Check current auth against /api/me');
|
|
81
83
|
console.error(' auth logout Delete the saved config file');
|
|
@@ -175,7 +177,7 @@ async function runScreenshotCommand(args) {
|
|
|
175
177
|
try {
|
|
176
178
|
const waitMs = Number.isFinite(args.waitMs) ? Math.max(0, Math.min(120_000, Math.round(args.waitMs))) : 4000;
|
|
177
179
|
const timeoutMs = Math.max(15_000, Math.min(180_000, waitMs + 30_000));
|
|
178
|
-
const
|
|
180
|
+
const argsList = [
|
|
179
181
|
'--headless=new',
|
|
180
182
|
'--disable-gpu',
|
|
181
183
|
'--hide-scrollbars',
|
|
@@ -192,26 +194,124 @@ async function runScreenshotCommand(args) {
|
|
|
192
194
|
`--virtual-time-budget=${waitMs || 0}`,
|
|
193
195
|
`--screenshot=${outPath}`,
|
|
194
196
|
url,
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
timeout: timeoutMs,
|
|
198
|
-
// Hard kill; some Chrome headless runs can hang after producing a screenshot.
|
|
199
|
-
killSignal: 'SIGKILL',
|
|
200
|
-
});
|
|
201
|
-
const screenshotOk = (() => {
|
|
197
|
+
];
|
|
198
|
+
const screenshotOk = () => {
|
|
202
199
|
try {
|
|
203
200
|
return statSync(outPath).size > 0;
|
|
204
201
|
}
|
|
205
202
|
catch {
|
|
206
203
|
return false;
|
|
207
204
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
205
|
+
};
|
|
206
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
207
|
+
const waitForScreenshotStable = async (signal) => {
|
|
208
|
+
const start = Date.now();
|
|
209
|
+
// Chrome writes the screenshot file as a single operation most of the time, but we've seen
|
|
210
|
+
// occasional flakiness where the file exists briefly at size=0. Wait for a stable >0 size.
|
|
211
|
+
let lastSize = -1;
|
|
212
|
+
let stableTicks = 0;
|
|
213
|
+
while (Date.now() - start < timeoutMs) {
|
|
214
|
+
if (signal.aborted)
|
|
215
|
+
return false;
|
|
216
|
+
let size = -1;
|
|
217
|
+
try {
|
|
218
|
+
size = statSync(outPath).size;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
size = -1;
|
|
222
|
+
}
|
|
223
|
+
if (size > 0) {
|
|
224
|
+
if (size === lastSize)
|
|
225
|
+
stableTicks += 1;
|
|
226
|
+
else
|
|
227
|
+
stableTicks = 0;
|
|
228
|
+
lastSize = size;
|
|
229
|
+
if (stableTicks >= 2)
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
await sleep(100);
|
|
233
|
+
}
|
|
234
|
+
if (signal.aborted)
|
|
235
|
+
return false;
|
|
236
|
+
throw new Error('Timed out waiting for screenshot file to be produced.');
|
|
237
|
+
};
|
|
238
|
+
// Use an isolated process group on Unix so we can reliably kill Chrome + its helpers.
|
|
239
|
+
const child = spawn(browser, argsList, {
|
|
240
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
241
|
+
detached: process.platform !== 'win32',
|
|
242
|
+
});
|
|
243
|
+
let stdout = '';
|
|
244
|
+
let stderr = '';
|
|
245
|
+
child.stdout?.setEncoding('utf8');
|
|
246
|
+
child.stderr?.setEncoding('utf8');
|
|
247
|
+
child.stdout?.on('data', (chunk) => {
|
|
248
|
+
stdout += String(chunk);
|
|
249
|
+
});
|
|
250
|
+
child.stderr?.on('data', (chunk) => {
|
|
251
|
+
stderr += String(chunk);
|
|
252
|
+
});
|
|
253
|
+
const killChild = (signal) => {
|
|
254
|
+
if (!child.pid)
|
|
255
|
+
return;
|
|
256
|
+
try {
|
|
257
|
+
if (process.platform !== 'win32') {
|
|
258
|
+
// Kill the whole process group (negative PID) when detached.
|
|
259
|
+
process.kill(-child.pid, signal);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
process.kill(child.pid, signal);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// ignore
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
270
|
+
child.once('error', reject);
|
|
271
|
+
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
272
|
+
});
|
|
273
|
+
const abort = new AbortController();
|
|
274
|
+
const screenshotPromise = waitForScreenshotStable(abort.signal)
|
|
275
|
+
.then((ok) => ({ type: 'screenshot', ok }))
|
|
276
|
+
.catch((error) => ({ type: 'screenshot_error', error }));
|
|
277
|
+
const exitPromiseHandled = exitPromise
|
|
278
|
+
.then((r) => ({ type: 'exit', ...r }))
|
|
279
|
+
.catch((error) => ({ type: 'exit_error', error }));
|
|
280
|
+
// Hard-kill after the timeout budget; Chrome can hang even after producing a screenshot.
|
|
281
|
+
const timeout = setTimeout(() => {
|
|
282
|
+
killChild('SIGKILL');
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
try {
|
|
285
|
+
const first = await Promise.race([screenshotPromise, exitPromiseHandled]);
|
|
286
|
+
abort.abort();
|
|
287
|
+
if (first.type === 'exit_error') {
|
|
288
|
+
const message = first.error instanceof Error ? first.error.message : String(first.error);
|
|
289
|
+
throw new Error(`Screenshot failed (spawn error): ${message}`);
|
|
290
|
+
}
|
|
291
|
+
if (first.type === 'screenshot_error') {
|
|
292
|
+
// Still allow a best-effort success if Chrome produced a screenshot before hanging.
|
|
293
|
+
killChild('SIGKILL');
|
|
294
|
+
await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
|
|
295
|
+
}
|
|
296
|
+
// If the screenshot is ready but Chrome is still running, kill it immediately instead of
|
|
297
|
+
// waiting for the timeout (which is often ~waitMs+30s).
|
|
298
|
+
if (first.type === 'screenshot' && first.ok) {
|
|
299
|
+
killChild('SIGKILL');
|
|
300
|
+
// Don't await indefinitely; cleanup is best-effort.
|
|
301
|
+
await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
|
|
302
|
+
}
|
|
303
|
+
const ok = screenshotOk();
|
|
304
|
+
if (!ok) {
|
|
305
|
+
// Chrome may exit without producing a screenshot; include output to help debug.
|
|
306
|
+
const detail = (stderr || stdout).trim();
|
|
307
|
+
const exit = first.type === 'exit' ? first : await exitPromise.catch(() => ({ code: null, signal: null }));
|
|
308
|
+
throw new Error(`Screenshot failed (exit ${exit.code ?? 'unknown'}, signal ${exit.signal ?? 'none'}): ${detail || 'no output'}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
// Ensure Chrome isn't left running in the background.
|
|
314
|
+
killChild('SIGKILL');
|
|
215
315
|
}
|
|
216
316
|
return {
|
|
217
317
|
screenshotPath: outPath,
|
|
@@ -866,6 +966,7 @@ async function main() {
|
|
|
866
966
|
|| verb === 'clear-target'
|
|
867
967
|
|| verb === 'extract-assets'
|
|
868
968
|
|| verb === 'generate-html'
|
|
969
|
+
|| verb === 'iterate'
|
|
869
970
|
|| verb === 'critique'
|
|
870
971
|
|| verb === 'watch'
|
|
871
972
|
|| verb === 'share'
|
|
@@ -972,69 +1073,89 @@ async function main() {
|
|
|
972
1073
|
console.log(pretty(progressed));
|
|
973
1074
|
process.exit(0);
|
|
974
1075
|
}
|
|
975
|
-
if (verb === 'critique') {
|
|
1076
|
+
if (verb === 'iterate' || verb === 'critique') {
|
|
976
1077
|
const input = await readJsonArgs(command.slice(1), flags);
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1078
|
+
if (verb === 'critique') {
|
|
1079
|
+
console.error('[every-design] `critique` is deprecated; use `iterate` instead.');
|
|
1080
|
+
}
|
|
1081
|
+
const runId = String(input.runId ?? '').trim();
|
|
1082
|
+
if (!runId)
|
|
1083
|
+
throw new Error('Missing required argument: runId');
|
|
980
1084
|
const concerns = typeof input.concerns === 'string' ? input.concerns.trim() : '';
|
|
981
|
-
const target = Array.isArray(input.target) ? input.target : [];
|
|
982
1085
|
const render = Array.isArray(input.render) ? input.render : [];
|
|
983
|
-
if (target.length === 0)
|
|
984
|
-
throw new Error('Missing required argument: target (array of images)');
|
|
985
1086
|
if (render.length === 0)
|
|
986
1087
|
throw new Error('Missing required argument: render (array of images)');
|
|
1088
|
+
const target = Array.isArray(input.target) ? input.target : [];
|
|
987
1089
|
const viewportInput = input.viewport && typeof input.viewport === 'object' ? input.viewport : null;
|
|
988
1090
|
const viewport = {
|
|
989
1091
|
width: Number(viewportInput?.width) || 1696,
|
|
990
1092
|
height: Number(viewportInput?.height) || 2528,
|
|
991
1093
|
};
|
|
992
|
-
const targetRefs = await client.uploadSourceImages(target);
|
|
993
1094
|
const renderRefs = await client.uploadSourceImages(render);
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
target: targetRefs,
|
|
1002
|
-
render: renderRefs,
|
|
1003
|
-
},
|
|
1004
|
-
},
|
|
1095
|
+
const targetRefs = target.length > 0 ? await client.uploadSourceImages(target) : [];
|
|
1096
|
+
const created = await client.createRunOperation(runId, {
|
|
1097
|
+
type: 'iterate',
|
|
1098
|
+
...(concerns ? { concerns } : {}),
|
|
1099
|
+
viewport,
|
|
1100
|
+
render: renderRefs,
|
|
1101
|
+
...(targetRefs.length > 0 ? { target: targetRefs } : {}),
|
|
1005
1102
|
});
|
|
1006
|
-
const
|
|
1007
|
-
if (!
|
|
1008
|
-
throw new Error('
|
|
1103
|
+
const operationId = String(created?.operation?.id ?? '').trim();
|
|
1104
|
+
if (!operationId)
|
|
1105
|
+
throw new Error('Iterate operation created but missing operation id');
|
|
1009
1106
|
const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
|
|
1010
1107
|
const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1108
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1109
|
+
const deadlineMs = Date.now() + Math.max(1, timeoutSeconds) * 1000;
|
|
1110
|
+
while (true) {
|
|
1111
|
+
const op = await client.getOperation(operationId);
|
|
1112
|
+
const status = String(op?.operation?.status ?? '').trim().toLowerCase();
|
|
1113
|
+
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
if (Date.now() > deadlineMs) {
|
|
1117
|
+
throw new Error(`Timed out waiting for iterate operation (${operationId})`);
|
|
1118
|
+
}
|
|
1119
|
+
await sleep(Math.max(1, intervalSeconds) * 1000);
|
|
1120
|
+
}
|
|
1016
1121
|
const detail = await client.getRun(runId);
|
|
1017
1122
|
const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1123
|
+
const getOperationId = (artifact) => {
|
|
1124
|
+
const meta = artifact?.metadata;
|
|
1125
|
+
if (meta && typeof meta === 'object') {
|
|
1126
|
+
const candidate = meta.operationId ?? meta.operation_id;
|
|
1127
|
+
return typeof candidate === 'string' ? candidate.trim() : '';
|
|
1128
|
+
}
|
|
1129
|
+
return '';
|
|
1130
|
+
};
|
|
1131
|
+
const critiqueArtifacts = outputs
|
|
1132
|
+
.filter((a) => a?.artifactType === 'critique')
|
|
1133
|
+
.sort((a, b) => String(a?.createdAt || '').localeCompare(String(b?.createdAt || '')));
|
|
1134
|
+
const critiqueArtifact = critiqueArtifacts.find((a) => getOperationId(a) === operationId)
|
|
1135
|
+
?? (critiqueArtifacts.length ? critiqueArtifacts[critiqueArtifacts.length - 1] : null);
|
|
1136
|
+
const generatedAssets = outputs.filter((a) => a?.artifactType === 'critique-asset' && getOperationId(a) === operationId);
|
|
1020
1137
|
if (!critiqueArtifact) {
|
|
1021
1138
|
console.log(pretty({
|
|
1022
1139
|
runId,
|
|
1140
|
+
operationId,
|
|
1023
1141
|
status: 'completed',
|
|
1142
|
+
iterate: null,
|
|
1024
1143
|
critique: null,
|
|
1025
1144
|
generatedAssets,
|
|
1026
|
-
error: 'Missing critique artifact',
|
|
1145
|
+
error: 'Missing critique artifact for iterate operation',
|
|
1027
1146
|
}));
|
|
1028
1147
|
process.exit(0);
|
|
1029
1148
|
}
|
|
1030
1149
|
const url = typeof critiqueArtifact?.url === 'string' ? String(critiqueArtifact.url) : '';
|
|
1031
1150
|
if (!url)
|
|
1032
|
-
throw new Error('Critique
|
|
1033
|
-
const downloaded = await client.downloadUrlToCache(runId, critiqueArtifact.id, url, { fileNameHint: '
|
|
1151
|
+
throw new Error('Critique artifact is missing a download url');
|
|
1152
|
+
const downloaded = await client.downloadUrlToCache(runId, critiqueArtifact.id, url, { fileNameHint: 'iterate.json' });
|
|
1034
1153
|
const raw = await readFile(downloaded.filePath, 'utf8');
|
|
1035
1154
|
const parsed = parseJsonOrThrow(raw);
|
|
1036
1155
|
console.log(pretty({
|
|
1037
1156
|
runId,
|
|
1157
|
+
operationId,
|
|
1158
|
+
iterate: parsed,
|
|
1038
1159
|
critique: parsed,
|
|
1039
1160
|
artifact: { id: critiqueArtifact.id, filePath: downloaded.filePath },
|
|
1040
1161
|
generatedAssets,
|
|
@@ -1191,7 +1312,8 @@ async function main() {
|
|
|
1191
1312
|
process.exit(0);
|
|
1192
1313
|
}
|
|
1193
1314
|
if (verb === 'artifacts' && sub === 'list') {
|
|
1194
|
-
const
|
|
1315
|
+
const positional = parseArtifactsListPositional(command.slice(2));
|
|
1316
|
+
const input = positional ? { runId: positional.runId } : await readJsonArgs(command.slice(2), flags);
|
|
1195
1317
|
const runId = String(input.runId ?? '').trim();
|
|
1196
1318
|
if (!runId)
|
|
1197
1319
|
throw new Error('Missing required argument: runId');
|
|
@@ -1201,7 +1323,8 @@ async function main() {
|
|
|
1201
1323
|
process.exit(0);
|
|
1202
1324
|
}
|
|
1203
1325
|
if (verb === 'artifacts' && sub === 'download') {
|
|
1204
|
-
const
|
|
1326
|
+
const positional = parseArtifactsDownloadPositional(command.slice(2));
|
|
1327
|
+
const input = positional ? { runId: positional.runId, artifactId: positional.artifactId } : await readJsonArgs(command.slice(2), flags);
|
|
1205
1328
|
const runId = String(input.runId ?? '').trim();
|
|
1206
1329
|
const artifactId = String(input.artifactId ?? '').trim();
|
|
1207
1330
|
if (!runId)
|
|
@@ -1211,26 +1334,49 @@ async function main() {
|
|
|
1211
1334
|
const detail = await client.getRun(runId);
|
|
1212
1335
|
const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
|
|
1213
1336
|
const found = outputs.find((entry) => String(entry?.id ?? '') === artifactId) || null;
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1337
|
+
const hint = await (async () => {
|
|
1338
|
+
const url = typeof found?.url === 'string' ? found.url.trim() : '';
|
|
1339
|
+
if (url) {
|
|
1340
|
+
try {
|
|
1341
|
+
const u = url.startsWith('http://') || url.startsWith('https://')
|
|
1342
|
+
? new URL(url)
|
|
1343
|
+
: new URL(url, 'https://example.invalid');
|
|
1344
|
+
return u.pathname.split('/').filter(Boolean).pop() || undefined;
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
return url.split('/').filter(Boolean).pop() || undefined;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1218
1350
|
try {
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1351
|
+
const artifacts = await client.listArtifacts(runId);
|
|
1352
|
+
const record = Array.isArray(artifacts?.artifacts)
|
|
1353
|
+
? artifacts.artifacts.find((a) => String(a?.id ?? '') === artifactId)
|
|
1354
|
+
: null;
|
|
1355
|
+
const storageKey = typeof record?.storageKey === 'string' ? record.storageKey.trim() : '';
|
|
1356
|
+
return storageKey ? storageKey.split('/').filter(Boolean).pop() || undefined : undefined;
|
|
1223
1357
|
}
|
|
1224
1358
|
catch {
|
|
1225
|
-
return
|
|
1359
|
+
return undefined;
|
|
1360
|
+
}
|
|
1361
|
+
})();
|
|
1362
|
+
const downloaded = await (async () => {
|
|
1363
|
+
try {
|
|
1364
|
+
return await client.downloadArtifactToCache(runId, artifactId, { fileNameHint: hint });
|
|
1365
|
+
}
|
|
1366
|
+
catch (error) {
|
|
1367
|
+
const url = typeof found?.url === 'string' ? found.url.trim() : '';
|
|
1368
|
+
if (url) {
|
|
1369
|
+
return await client.downloadUrlToCache(runId, artifactId, url, { fileNameHint: hint });
|
|
1370
|
+
}
|
|
1371
|
+
throw error;
|
|
1226
1372
|
}
|
|
1227
1373
|
})();
|
|
1228
|
-
const downloaded = await client.downloadUrlToCache(runId, artifactId, url, { fileNameHint: hint });
|
|
1229
1374
|
console.log(pretty(downloaded));
|
|
1230
1375
|
process.exit(0);
|
|
1231
1376
|
}
|
|
1232
1377
|
if (verb === 'artifacts') {
|
|
1233
|
-
throw new Error('Usage: artifacts list|download'
|
|
1378
|
+
throw new Error('Usage: artifacts list <runId> | artifacts download <runId>/<artifactId>\n' +
|
|
1379
|
+
'Tip: you can still use --json or pipe a JSON object via stdin.');
|
|
1234
1380
|
}
|
|
1235
1381
|
// Should be unreachable due to the verb guard.
|
|
1236
1382
|
throw new Error(`Unknown command: ${verb}`);
|