@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 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 critique --json '{...}'`
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(' critique Critique a target vs current screenshot (CLI helper)');
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 List run artifacts (CLI helper)');
78
- console.error(' artifacts download Download an artifact (CLI helper)');
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 res = spawnSync(browser, [
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
- encoding: 'utf8',
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
- // `spawnSync` returns `error.code === 'ETIMEDOUT'` when the timeout triggers.
210
- // Chrome may still have produced a valid screenshot before hanging, so treat that as success.
211
- if (!screenshotOk) {
212
- const detail = (res.stderr || res.stdout || '').trim();
213
- const errorText = res.error instanceof Error ? res.error.message : '';
214
- throw new Error(`Screenshot failed (exit ${res.status ?? 'unknown'}, signal ${res.signal ?? 'none'}): ${detail || errorText || 'no output'}`);
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
- const originalPrompt = String(input.originalPrompt ?? '').trim();
978
- if (!originalPrompt)
979
- throw new Error('Missing required argument: originalPrompt');
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 created = await client.createRun({
995
- prompt: originalPrompt,
996
- config: {
997
- output: { designKind: 'interface', render: { viewport } },
998
- critique: {
999
- originalPrompt,
1000
- concerns,
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 runId = String(created?.run?.id ?? '').trim();
1007
- if (!runId)
1008
- throw new Error('Critique run created but missing run id');
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
- await watchRun(client, runId, {
1012
- timeoutSeconds,
1013
- intervalSeconds,
1014
- log: (line) => console.error(line),
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 critiqueArtifact = outputs.find((a) => a?.artifactType === 'critique') ?? null;
1019
- const generatedAssets = outputs.filter((a) => a?.artifactType === 'critique-asset');
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 output is missing a download url');
1033
- const downloaded = await client.downloadUrlToCache(runId, critiqueArtifact.id, url, { fileNameHint: 'critique.json' });
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 input = await readJsonArgs(command.slice(2), flags);
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 input = await readJsonArgs(command.slice(2), flags);
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 url = typeof found?.url === 'string' ? found.url : '';
1215
- if (!url)
1216
- throw new Error(`Artifact not found in standard outputs: ${artifactId}`);
1217
- const hint = (() => {
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 u = url.startsWith('http://') || url.startsWith('https://')
1220
- ? new URL(url)
1221
- : new URL(url, 'https://example.invalid');
1222
- return u.pathname.split('/').filter(Boolean).pop() || undefined;
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 url.split('/').filter(Boolean).pop() || undefined;
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}`);