@just-every/design 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,21 +5,22 @@
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 { spawn, spawnSync } from 'node:child_process';
8
+ import { spawnSync } from 'node:child_process';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { fileURLToPath } from 'node:url';
11
+ import { fileURLToPath, pathToFileURL } 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';
16
15
  import { clearConfig, readConfig, resolveConfigPath, writeConfig } from './config.js';
17
16
  import { DesignAppClient } from './design-client.js';
18
17
  import { parseJsonOrThrow } from './json.js';
19
18
  import { checkIntegrations, detectClients, detectDefaultClients, ensureLocalLauncher, resolveLocalInstallLayout, runInstall, runRemove, } from './install.js';
20
19
  import { startMcpServer } from './server.js';
21
20
  import { buildCreateRunRequest, NOT_AUTHENTICATED_HELP, watchRun } from './tool-logic.js';
22
- import { wrapToolResponse } from './response-guidance.js';
21
+ import { formatSyncCompletionMarkdown, formatSyncStartMarkdown, formatToolResponseMarkdown, wrapToolResponse, } from './response-guidance.js';
22
+ import { createSyncProgressTracker, getProgressPct } from './progress.js';
23
+ import { runScreenshotCommand, which } from './screenshot.js';
23
24
  function parseArgs(argv) {
24
25
  const flags = {};
25
26
  const command = [];
@@ -60,25 +61,9 @@ function printHelp() {
60
61
  console.error(' (default) Start the MCP server (stdio)');
61
62
  console.error(' install Interactive setup (auth + client config)');
62
63
  console.error(' remove Remove Every Design from detected clients');
63
- console.error(' create Create a design run (CLI helper)');
64
- console.error(' upload-target Upload an image and set it as the run target (CLI helper)');
65
- console.error(' refine-draft Refine a specific draft image in the same run (CLI helper)');
66
- console.error(' set-target Select a target draft for a run (CLI helper)');
67
- console.error(' clear-target Clear the explicit target for a run (CLI helper)');
68
- console.error(' extract-assets Trigger asset extraction for a run (CLI helper)');
69
- console.error(' generate-html Trigger HTML generation for a run (CLI helper)');
70
- console.error(' screenshot <url> Screenshot a URL (CLI helper)');
71
- console.error(' iterate Iterate on a run using screenshot feedback (CLI helper)');
72
- console.error(' critique Alias for `iterate`');
73
- console.error(' watch Watch a design run (CLI helper)');
74
- console.error(' share status Get run share link status (CLI helper)');
75
- console.error(' share enable Enable (or rotate) a share link (CLI helper)');
76
- console.error(' share revoke Revoke the active share link (CLI helper)');
77
- console.error(' get Fetch a run by id (CLI helper)');
78
- console.error(' list List recent runs (CLI helper)');
79
- console.error(' events Fetch run events (CLI helper)');
80
- console.error(' artifacts list <runId> List run artifacts (CLI helper)');
81
- console.error(' artifacts download <runId>/<artifactId> Download an artifact (CLI helper)');
64
+ console.error(' create Create a new design (CLI helper)');
65
+ console.error(' sync Wait for completion, extract assets, and sync artifacts (CLI helper)');
66
+ console.error(' check Screenshot and compare implementation against a run (CLI helper)');
82
67
  console.error(' auth login Login via approval-link flow and save config');
83
68
  console.error(' auth status Check current auth against /api/me');
84
69
  console.error(' auth logout Delete the saved config file');
@@ -90,10 +75,6 @@ function printHelp() {
90
75
  console.error(` --config <path> Config path (default: ${configPath})`);
91
76
  console.error(' --no-open Do not open the approval URL in a browser (auth login)');
92
77
  console.error(' --json <string> JSON args payload for CLI helpers (or pipe JSON via stdin)');
93
- console.error(' --out <path> Output path (screenshot)');
94
- console.error(' --width <px> Viewport width (screenshot, default 1696)');
95
- console.error(' --height <px> Viewport height (screenshot, default 2528)');
96
- console.error(' --wait-ms <ms> Extra wait budget before screenshot (screenshot, default 4000)');
97
78
  console.error(' --client <name[,name...]> Install target(s): code,codex,claude-desktop,claude-code,cursor,gemini,qwen,all,auto');
98
79
  console.error(' --name <serverName> MCP server name key (default: every-design)');
99
80
  console.error(' --launcher <npx|local> How clients launch the MCP server (default for install: local)');
@@ -119,221 +100,6 @@ async function resolvePackageVersion() {
119
100
  return '0.0.0';
120
101
  }
121
102
  }
122
- function which(bin) {
123
- try {
124
- const tool = process.platform === 'win32' ? 'where' : 'which';
125
- const res = spawnSync(tool, [bin], { encoding: 'utf8' });
126
- if (res.status !== 0)
127
- return null;
128
- const first = String(res.stdout || '').trim().split(/\r?\n/)[0];
129
- return first ? first.trim() : null;
130
- }
131
- catch {
132
- return null;
133
- }
134
- }
135
- function resolveHeadlessBrowserBinary() {
136
- const explicit = process.env.CHROME_PATH?.trim();
137
- if (explicit && existsSync(explicit))
138
- return explicit;
139
- const candidates = [
140
- 'google-chrome',
141
- 'google-chrome-stable',
142
- 'chromium',
143
- 'chromium-browser',
144
- 'chrome',
145
- 'msedge',
146
- 'microsoft-edge',
147
- ];
148
- for (const c of candidates) {
149
- const found = which(c);
150
- if (found)
151
- return found;
152
- }
153
- if (process.platform === 'darwin') {
154
- const macCandidates = [
155
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
156
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
157
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
158
- ];
159
- for (const c of macCandidates) {
160
- if (existsSync(c))
161
- return c;
162
- }
163
- }
164
- return null;
165
- }
166
- async function runScreenshotCommand(args) {
167
- const url = args.url.trim();
168
- if (!url)
169
- throw new Error('Missing URL. Usage: every-design screenshot http://127.0.0.1:3000/path');
170
- const outPath = path.resolve(args.outPath);
171
- await mkdir(path.dirname(outPath), { recursive: true });
172
- const browser = resolveHeadlessBrowserBinary();
173
- if (!browser) {
174
- throw new Error('No headless Chrome/Chromium/Edge binary found. Install a Chromium-based browser or set CHROME_PATH to an executable path.');
175
- }
176
- const tmpProfile = await (async () => {
177
- const base = path.join(os.tmpdir(), 'every-design-screenshot-');
178
- const { mkdtemp } = await import('node:fs/promises');
179
- return mkdtemp(base);
180
- })();
181
- try {
182
- const waitMs = Number.isFinite(args.waitMs) ? Math.max(0, Math.min(120_000, Math.round(args.waitMs))) : 4000;
183
- const timeoutMs = Math.max(15_000, Math.min(180_000, waitMs + 30_000));
184
- const argsList = [
185
- '--headless=new',
186
- '--disable-gpu',
187
- '--hide-scrollbars',
188
- '--mute-audio',
189
- '--no-first-run',
190
- '--no-default-browser-check',
191
- '--disable-background-networking',
192
- '--disable-sync',
193
- '--disable-extensions',
194
- '--metrics-recording-only',
195
- '--force-device-scale-factor=1',
196
- `--window-size=${args.width},${args.height}`,
197
- `--user-data-dir=${tmpProfile}`,
198
- `--virtual-time-budget=${waitMs || 0}`,
199
- `--screenshot=${outPath}`,
200
- url,
201
- ];
202
- if (typeof process.getuid === 'function' && process.getuid() === 0) {
203
- // Chromium requires no-sandbox flags when running as root.
204
- argsList.unshift('--disable-setuid-sandbox', '--no-sandbox');
205
- }
206
- const screenshotOk = () => {
207
- try {
208
- return statSync(outPath).size > 0;
209
- }
210
- catch {
211
- return false;
212
- }
213
- };
214
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
215
- const waitForScreenshotStable = async (signal) => {
216
- const start = Date.now();
217
- // Chrome writes the screenshot file as a single operation most of the time, but we've seen
218
- // occasional flakiness where the file exists briefly at size=0. Wait for a stable >0 size.
219
- let lastSize = -1;
220
- let stableTicks = 0;
221
- while (Date.now() - start < timeoutMs) {
222
- if (signal.aborted)
223
- return false;
224
- let size = -1;
225
- try {
226
- size = statSync(outPath).size;
227
- }
228
- catch {
229
- size = -1;
230
- }
231
- if (size > 0) {
232
- if (size === lastSize)
233
- stableTicks += 1;
234
- else
235
- stableTicks = 0;
236
- lastSize = size;
237
- if (stableTicks >= 2)
238
- return true;
239
- }
240
- await sleep(100);
241
- }
242
- if (signal.aborted)
243
- return false;
244
- throw new Error('Timed out waiting for screenshot file to be produced.');
245
- };
246
- // Use an isolated process group on Unix so we can reliably kill Chrome + its helpers.
247
- const child = spawn(browser, argsList, {
248
- stdio: ['ignore', 'pipe', 'pipe'],
249
- detached: process.platform !== 'win32',
250
- });
251
- let stdout = '';
252
- let stderr = '';
253
- child.stdout?.setEncoding('utf8');
254
- child.stderr?.setEncoding('utf8');
255
- child.stdout?.on('data', (chunk) => {
256
- stdout += String(chunk);
257
- });
258
- child.stderr?.on('data', (chunk) => {
259
- stderr += String(chunk);
260
- });
261
- const killChild = (signal) => {
262
- if (!child.pid)
263
- return;
264
- try {
265
- if (process.platform !== 'win32') {
266
- // Kill the whole process group (negative PID) when detached.
267
- process.kill(-child.pid, signal);
268
- }
269
- else {
270
- process.kill(child.pid, signal);
271
- }
272
- }
273
- catch {
274
- // ignore
275
- }
276
- };
277
- const exitPromise = new Promise((resolve, reject) => {
278
- child.once('error', reject);
279
- child.once('exit', (code, signal) => resolve({ code, signal }));
280
- });
281
- const abort = new AbortController();
282
- const screenshotPromise = waitForScreenshotStable(abort.signal)
283
- .then((ok) => ({ type: 'screenshot', ok }))
284
- .catch((error) => ({ type: 'screenshot_error', error }));
285
- const exitPromiseHandled = exitPromise
286
- .then((r) => ({ type: 'exit', ...r }))
287
- .catch((error) => ({ type: 'exit_error', error }));
288
- // Hard-kill after the timeout budget; Chrome can hang even after producing a screenshot.
289
- const timeout = setTimeout(() => {
290
- killChild('SIGKILL');
291
- }, timeoutMs);
292
- try {
293
- const first = await Promise.race([screenshotPromise, exitPromiseHandled]);
294
- abort.abort();
295
- if (first.type === 'exit_error') {
296
- const message = first.error instanceof Error ? first.error.message : String(first.error);
297
- throw new Error(`Screenshot failed (spawn error): ${message}`);
298
- }
299
- if (first.type === 'screenshot_error') {
300
- // Still allow a best-effort success if Chrome produced a screenshot before hanging.
301
- killChild('SIGKILL');
302
- await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
303
- }
304
- // If the screenshot is ready but Chrome is still running, kill it immediately instead of
305
- // waiting for the timeout (which is often ~waitMs+30s).
306
- if (first.type === 'screenshot' && first.ok) {
307
- killChild('SIGKILL');
308
- // Don't await indefinitely; cleanup is best-effort.
309
- await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
310
- }
311
- const ok = screenshotOk();
312
- if (!ok) {
313
- // Chrome may exit without producing a screenshot; include output to help debug.
314
- const detail = (stderr || stdout).trim();
315
- const exit = first.type === 'exit' ? first : await exitPromise.catch(() => ({ code: null, signal: null }));
316
- throw new Error(`Screenshot failed (exit ${exit.code ?? 'unknown'}, signal ${exit.signal ?? 'none'}): ${detail || 'no output'}`);
317
- }
318
- }
319
- finally {
320
- clearTimeout(timeout);
321
- // Ensure Chrome isn't left running in the background.
322
- killChild('SIGKILL');
323
- }
324
- return {
325
- screenshotPath: outPath,
326
- width: args.width,
327
- height: args.height,
328
- engine: 'chrome-headless',
329
- binary: browser,
330
- };
331
- }
332
- finally {
333
- const { rm } = await import('node:fs/promises');
334
- await rm(tmpProfile, { recursive: true, force: true }).catch(() => undefined);
335
- }
336
- }
337
103
  function isTtyInteractive() {
338
104
  return Boolean(process.stdin.isTTY && process.stderr.isTTY);
339
105
  }
@@ -385,76 +151,6 @@ async function readJsonArgs(remainingArgs, flags) {
385
151
  }
386
152
  return parsed;
387
153
  }
388
- async function readJsonArgsOptional(remainingArgs, flags) {
389
- const jsonFlag = resolveFlagString(flags, 'json');
390
- const hasInline = remainingArgs.some((t) => t.trim().length > 0);
391
- const hasStdin = !process.stdin.isTTY;
392
- if (!jsonFlag && !hasInline && !hasStdin)
393
- return {};
394
- return readJsonArgs(remainingArgs, flags);
395
- }
396
- function pretty(value) {
397
- return JSON.stringify(value, null, 2);
398
- }
399
- function createWatchProgressDisplay(stream, enabled) {
400
- let lastRendered = '';
401
- let lastSnapshot = null;
402
- let spinner = 0;
403
- function clearLine() {
404
- stream.write('\r\x1b[2K');
405
- }
406
- function render(snapshot) {
407
- if (!enabled)
408
- return;
409
- lastSnapshot = snapshot;
410
- spinner = (spinner + 1) % 4;
411
- const stage = snapshot.stage ? String(snapshot.stage) : '';
412
- const status = snapshot.status ? String(snapshot.status) : '';
413
- const progress = typeof snapshot.progress === 'number' ? snapshot.progress : null;
414
- const pct = typeof progress === 'number' ? Math.max(0, Math.min(100, Math.round(progress * 100))) : null;
415
- const barWidth = 24;
416
- const filled = pct === null ? 0 : Math.round((pct / 100) * barWidth);
417
- const bar = `[${'#'.repeat(Math.max(0, Math.min(barWidth, filled)))}${'-'.repeat(Math.max(0, barWidth - filled))}]`;
418
- const spinChar = ['|', '/', '-', '\\'][spinner] ?? '|';
419
- const parts = [];
420
- if (pct === null) {
421
- parts.push(`${spinChar} ${bar}`);
422
- }
423
- else {
424
- parts.push(`${bar} ${String(pct).padStart(3, ' ')}%`);
425
- }
426
- if (stage)
427
- parts.push(`stage=${stage}`);
428
- if (status)
429
- parts.push(`status=${status}`);
430
- const line = parts.join(' ');
431
- if (line === lastRendered)
432
- return;
433
- lastRendered = line;
434
- clearLine();
435
- stream.write(line);
436
- }
437
- function log(line) {
438
- if (!enabled) {
439
- stream.write(`${line}\n`);
440
- return;
441
- }
442
- clearLine();
443
- stream.write(`${line}\n`);
444
- if (lastSnapshot) {
445
- lastRendered = '';
446
- render(lastSnapshot);
447
- }
448
- }
449
- function finish() {
450
- if (!enabled)
451
- return;
452
- clearLine();
453
- lastRendered = '';
454
- lastSnapshot = null;
455
- }
456
- return { render, log, finish, enabled };
457
- }
458
154
  async function promptYesNo(question, defaultYes = false) {
459
155
  if (!isTtyInteractive())
460
156
  return defaultYes;
@@ -927,32 +623,6 @@ async function main() {
927
623
  const loginOrigin = resolveLoginOrigin(flags);
928
624
  const verb = command[0] ?? '';
929
625
  const sub = command[1] ?? '';
930
- if (verb === 'screenshot') {
931
- const url = String(command[1] ?? '').trim();
932
- const width = Number.parseInt(String(resolveFlagString(flags, 'width') ?? '1696'), 10);
933
- const height = Number.parseInt(String(resolveFlagString(flags, 'height') ?? '2528'), 10);
934
- const waitMs = Number.parseInt(String(resolveFlagString(flags, 'wait-ms') ?? '4000'), 10);
935
- const out = resolveFlagString(flags, 'out')?.trim();
936
- const outPath = out
937
- ? out
938
- : path.join(process.cwd(), `every-design-screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`);
939
- try {
940
- const result = await runScreenshotCommand({
941
- url,
942
- outPath,
943
- width: Number.isFinite(width) && width > 0 ? width : 1696,
944
- height: Number.isFinite(height) && height > 0 ? height : 2528,
945
- waitMs: Number.isFinite(waitMs) ? waitMs : 4000,
946
- });
947
- console.log(pretty(result));
948
- process.exit(0);
949
- }
950
- catch (error) {
951
- const message = error instanceof Error ? error.message : String(error);
952
- console.error(message);
953
- process.exit(1);
954
- }
955
- }
956
626
  if (verb === 'install') {
957
627
  const yes = Boolean(flags.yes);
958
628
  const dryRun = Boolean(flags['dry-run']);
@@ -1259,22 +929,8 @@ async function main() {
1259
929
  }
1260
930
  process.exit(0);
1261
931
  }
1262
- // CLI helpers (non-MCP). These are mostly useful for Skills/playbooks and scripting.
1263
- if (verb === 'create'
1264
- || verb === 'upload-target'
1265
- || verb === 'refine-draft'
1266
- || verb === 'set-target'
1267
- || verb === 'clear-target'
1268
- || verb === 'extract-assets'
1269
- || verb === 'generate-html'
1270
- || verb === 'iterate'
1271
- || verb === 'critique'
1272
- || verb === 'watch'
1273
- || verb === 'share'
1274
- || verb === 'get'
1275
- || verb === 'list'
1276
- || verb === 'events'
1277
- || verb === 'artifacts') {
932
+ // CLI helpers (non-MCP). CLIs must follow: create -> sync -> check.
933
+ if (verb === 'create' || verb === 'sync' || verb === 'check') {
1278
934
  const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
1279
935
  const sessionToken = (process.env.DESIGN_MCP_SESSION_TOKEN ?? cfg.sessionToken ?? '').trim();
1280
936
  if (!sessionToken) {
@@ -1292,139 +948,116 @@ async function main() {
1292
948
  const input = await readJsonArgs(command.slice(1), flags);
1293
949
  const { prompt, config, parentRunId } = await buildCreateRunRequest(client, input);
1294
950
  const created = await client.createRun({ prompt, config, ...(parentRunId ? { parentRunId } : {}) });
1295
- console.log(pretty(wrap('design.create', created)));
1296
- process.exit(0);
1297
- }
1298
- if (verb === 'upload-target') {
1299
- const input = await readJsonArgs(command.slice(1), flags);
1300
- const runId = String(input.runId ?? '').trim();
1301
- if (!runId)
1302
- throw new Error('Missing required argument: runId');
1303
- const sourceImage = input.sourceImage && typeof input.sourceImage === 'object' ? input.sourceImage : null;
1304
- if (!sourceImage) {
1305
- throw new Error('Missing required argument: sourceImage');
1306
- }
1307
- const res = await client.uploadRunTarget(runId, sourceImage);
1308
- console.log(pretty(wrap('design.uploadTarget', res, { runId })));
1309
- process.exit(0);
1310
- }
1311
- if (verb === 'refine-draft') {
1312
- const input = await readJsonArgs(command.slice(1), flags);
1313
- const runId = String(input.runId ?? '').trim();
1314
- const artifactId = String(input.artifactId ?? '').trim();
1315
- const prompt = String(input.prompt ?? '').trim();
1316
- if (!runId)
1317
- throw new Error('Missing required argument: runId');
1318
- if (!artifactId)
1319
- throw new Error('Missing required argument: artifactId');
1320
- if (!prompt)
1321
- throw new Error('Missing required argument: prompt');
1322
- const res = await client.refineDraft(runId, artifactId, prompt);
1323
- console.log(pretty(wrap('design.refineDraft', res, { runId, artifactId })));
1324
- process.exit(0);
1325
- }
1326
- if (verb === 'set-target') {
1327
- const input = await readJsonArgs(command.slice(1), flags);
1328
- const runId = String(input.runId ?? '').trim();
1329
- const artifactId = String(input.artifactId ?? '').trim();
1330
- if (!runId)
1331
- throw new Error('Missing required argument: runId');
1332
- if (!artifactId)
1333
- throw new Error('Missing required argument: artifactId');
1334
- const res = await client.setRunTarget(runId, artifactId);
1335
- console.log(pretty(wrap('design.setTarget', res, { runId, artifactId })));
1336
- process.exit(0);
1337
- }
1338
- if (verb === 'clear-target') {
1339
- const input = await readJsonArgs(command.slice(1), flags);
1340
- const runId = String(input.runId ?? '').trim();
1341
- if (!runId)
1342
- throw new Error('Missing required argument: runId');
1343
- const res = await client.clearRunTarget(runId);
1344
- console.log(pretty(wrap('design.clearTarget', res, { runId })));
951
+ console.log(formatToolResponseMarkdown(wrap('design.create', created)));
1345
952
  process.exit(0);
1346
953
  }
1347
- if (verb === 'extract-assets') {
954
+ if (verb === 'sync') {
1348
955
  const input = await readJsonArgs(command.slice(1), flags);
1349
956
  const runId = String(input.runId ?? '').trim();
1350
957
  if (!runId)
1351
958
  throw new Error('Missing required argument: runId');
1352
- const targetArtifactId = typeof input.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
1353
- const rawKind = typeof input.designKind === 'string' ? input.designKind.trim().toLowerCase() : '';
1354
- const designKind = rawKind === 'interface' || rawKind === 'html' ? 'interface' : undefined;
1355
- if (rawKind && !designKind) {
1356
- throw new Error('Invalid designKind: expected "interface"');
959
+ const syncDirInput = typeof input.syncDir === 'string' ? input.syncDir.trim() : '';
960
+ const syncDir = syncDirInput || `./every-design/${runId}`;
961
+ const progressLines = [];
962
+ console.log(formatSyncStartMarkdown());
963
+ const tracker = createSyncProgressTracker();
964
+ const emitProgress = (pct) => {
965
+ const line = `Progress: ${pct}%`;
966
+ progressLines.push(line);
967
+ console.log(line);
968
+ };
969
+ const initialPct = tracker.next(0);
970
+ if (initialPct !== null) {
971
+ emitProgress(initialPct);
1357
972
  }
1358
- if (targetArtifactId) {
1359
- await client.setRunTarget(runId, targetArtifactId);
973
+ const run = await watchRun(client, runId, {
974
+ timeoutSeconds: 1800,
975
+ intervalSeconds: 5,
976
+ onUpdate: (snapshot) => {
977
+ const pct = getProgressPct(snapshot.run);
978
+ const mapped = tracker.next(pct);
979
+ if (mapped !== null) {
980
+ emitProgress(mapped);
981
+ }
982
+ },
983
+ onExtractStart: () => {
984
+ const mapped = tracker.markExtractStart();
985
+ if (mapped !== null) {
986
+ emitProgress(mapped);
987
+ }
988
+ },
989
+ syncDir,
990
+ ensureAssets: true,
991
+ });
992
+ const finalPct = getProgressPct(run);
993
+ const mappedFinal = tracker.finalize(finalPct);
994
+ if (mappedFinal !== null) {
995
+ emitProgress(mappedFinal);
1360
996
  }
1361
- const progressed = await client.progressRun(runId, { action: 'extract_assets', ...(designKind ? { designKind } : {}) });
1362
- console.log(pretty(wrap('design.extractAssets', progressed, {
1363
- runId,
1364
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
1365
- })));
997
+ const payload = typeof run === 'object' && run ? { ...run, progress: progressLines } : { run, progress: progressLines };
998
+ console.log('');
999
+ console.log(formatSyncCompletionMarkdown(wrap('design.sync', payload, { runId, syncDir })));
1366
1000
  process.exit(0);
1367
1001
  }
1368
- if (verb === 'generate-html') {
1002
+ if (verb === 'check') {
1369
1003
  const input = await readJsonArgs(command.slice(1), flags);
1370
1004
  const runId = String(input.runId ?? '').trim();
1371
1005
  if (!runId)
1372
1006
  throw new Error('Missing required argument: runId');
1373
- const targetArtifactId = typeof input.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
1374
- if (targetArtifactId) {
1375
- await client.setRunTarget(runId, targetArtifactId);
1376
- }
1377
- const progressed = await client.progressRun(runId, { action: 'generate_html' });
1378
- console.log(pretty(wrap('design.generateHtml', progressed, {
1379
- runId,
1380
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
1381
- })));
1382
- process.exit(0);
1383
- }
1384
- if (verb === 'iterate' || verb === 'critique') {
1385
- const input = await readJsonArgs(command.slice(1), flags);
1386
- if (verb === 'critique') {
1387
- console.error('[every-design] `critique` is deprecated; use `iterate` instead.');
1007
+ const urlInput = typeof input.url === 'string' ? input.url.trim() : '';
1008
+ const pathInput = typeof input.path === 'string' ? input.path.trim() : '';
1009
+ if (!urlInput && !pathInput)
1010
+ throw new Error('Missing required argument: url or path');
1011
+ if (urlInput && pathInput)
1012
+ throw new Error('Provide only one of url or path');
1013
+ let targetUrl = urlInput;
1014
+ if (pathInput) {
1015
+ const resolved = path.resolve(pathInput);
1016
+ let stat;
1017
+ try {
1018
+ stat = statSync(resolved);
1019
+ }
1020
+ catch {
1021
+ throw new Error(`File not found: ${resolved}`);
1022
+ }
1023
+ if (!stat.isFile()) {
1024
+ throw new Error(`Path must be a file: ${resolved}`);
1025
+ }
1026
+ targetUrl = pathToFileURL(resolved).toString();
1388
1027
  }
1389
- const runId = String(input.runId ?? '').trim();
1390
- if (!runId)
1391
- throw new Error('Missing required argument: runId');
1392
- const concerns = typeof input.concerns === 'string' ? input.concerns.trim() : '';
1393
- const render = Array.isArray(input.render) ? input.render : [];
1394
- if (render.length === 0)
1395
- throw new Error('Missing required argument: render (array of images)');
1396
- const target = Array.isArray(input.target) ? input.target : [];
1397
- const viewportInput = input.viewport && typeof input.viewport === 'object' ? input.viewport : null;
1398
- const viewport = {
1399
- width: Number(viewportInput?.width) || 1696,
1400
- height: Number(viewportInput?.height) || 2528,
1401
- };
1402
- const renderRefs = await client.uploadSourceImages(render);
1403
- const targetRefs = target.length > 0 ? await client.uploadSourceImages(target) : [];
1028
+ const screenshotOut = path.join(os.tmpdir(), `every-design-check-${runId}-${Date.now()}.png`);
1029
+ const screenshot = await runScreenshotCommand({
1030
+ url: targetUrl,
1031
+ outPath: screenshotOut,
1032
+ width: 1696,
1033
+ height: 2528,
1034
+ waitMs: 4000,
1035
+ });
1036
+ const renderRefs = await client.uploadSourceImages([{ type: 'path', path: screenshot.screenshotPath }]);
1404
1037
  const created = await client.createRunOperation(runId, {
1405
1038
  type: 'iterate',
1406
- ...(concerns ? { concerns } : {}),
1407
- viewport,
1039
+ viewport: { width: screenshot.width, height: screenshot.height },
1408
1040
  render: renderRefs,
1409
- ...(targetRefs.length > 0 ? { target: targetRefs } : {}),
1410
1041
  });
1411
1042
  const operationId = String(created?.operation?.id ?? '').trim();
1412
1043
  if (!operationId)
1413
1044
  throw new Error('Iterate operation created but missing operation id');
1414
- const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 3600;
1415
- const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
1416
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1417
- const deadlineMs = Date.now() + Math.max(1, timeoutSeconds) * 1000;
1045
+ const deadlineMs = Date.now() + 900 * 1000;
1046
+ let terminalStatus = '';
1418
1047
  while (true) {
1419
1048
  const op = await client.getOperation(operationId);
1420
1049
  const status = String(op?.operation?.status ?? '').trim().toLowerCase();
1421
1050
  if (status === 'completed' || status === 'failed' || status === 'cancelled') {
1051
+ terminalStatus = status;
1422
1052
  break;
1423
1053
  }
1424
1054
  if (Date.now() > deadlineMs) {
1425
1055
  throw new Error(`Timed out waiting for iterate operation (${operationId})`);
1426
1056
  }
1427
- await sleep(Math.max(1, intervalSeconds) * 1000);
1057
+ await new Promise((r) => setTimeout(r, 5_000));
1058
+ }
1059
+ if (terminalStatus !== 'completed') {
1060
+ throw new Error(`Iterate operation ${operationId} ended with status ${terminalStatus || 'unknown'}`);
1428
1061
  }
1429
1062
  const detail = await client.getRun(runId);
1430
1063
  const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
@@ -1447,12 +1080,16 @@ async function main() {
1447
1080
  runId,
1448
1081
  operationId,
1449
1082
  status: 'completed',
1083
+ screenshot: {
1084
+ filePath: screenshot.screenshotPath,
1085
+ width: screenshot.width,
1086
+ height: screenshot.height,
1087
+ },
1450
1088
  iterate: null,
1451
- critique: null,
1452
1089
  generatedAssets,
1453
1090
  error: 'Missing critique artifact for iterate operation',
1454
1091
  };
1455
- console.log(pretty(wrap('design.iterate', payload, { runId })));
1092
+ console.log(formatToolResponseMarkdown(wrap('design.check', payload, { runId })));
1456
1093
  process.exit(0);
1457
1094
  }
1458
1095
  const url = typeof critiqueArtifact?.url === 'string' ? String(critiqueArtifact.url) : '';
@@ -1460,213 +1097,27 @@ async function main() {
1460
1097
  throw new Error('Critique artifact is missing a download url');
1461
1098
  const downloaded = await client.downloadUrlToCache(runId, critiqueArtifact.id, url, { fileNameHint: 'iterate.json' });
1462
1099
  const raw = await readFile(downloaded.filePath, 'utf8');
1463
- const parsed = parseJsonOrThrow(raw);
1100
+ let parsed;
1101
+ try {
1102
+ parsed = JSON.parse(raw);
1103
+ }
1104
+ catch {
1105
+ parsed = raw;
1106
+ }
1464
1107
  const payload = {
1465
1108
  runId,
1466
- operationId,
1109
+ screenshot: {
1110
+ filePath: screenshot.screenshotPath,
1111
+ width: screenshot.width,
1112
+ height: screenshot.height,
1113
+ },
1467
1114
  iterate: parsed,
1468
- critique: parsed,
1469
1115
  artifact: { id: critiqueArtifact.id, filePath: downloaded.filePath },
1470
1116
  generatedAssets,
1471
1117
  };
1472
- console.log(pretty(wrap('design.iterate', payload, { runId })));
1473
- process.exit(0);
1474
- }
1475
- if (verb === 'watch') {
1476
- const input = await readJsonArgs(command.slice(1), flags);
1477
- const runId = String(input.runId ?? '').trim();
1478
- if (!runId)
1479
- throw new Error('Missing required argument: runId');
1480
- const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
1481
- const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
1482
- const syncDir = typeof input.syncDir === 'string' && input.syncDir.trim() ? input.syncDir.trim() : undefined;
1483
- const ensureAssets = typeof input.ensureAssets === 'boolean' ? input.ensureAssets : undefined;
1484
- const targetArtifactId = typeof input.targetArtifactId === 'string' && input.targetArtifactId.trim()
1485
- ? input.targetArtifactId.trim()
1486
- : undefined;
1487
- const progressUi = createWatchProgressDisplay(process.stderr, Boolean(process.stderr.isTTY));
1488
- const log = (line) => {
1489
- if (progressUi.enabled && line.startsWith('[design.watch] status='))
1490
- return;
1491
- progressUi.log(line);
1492
- };
1493
- const run = await watchRun(client, runId, {
1494
- timeoutSeconds,
1495
- intervalSeconds,
1496
- log,
1497
- onUpdate: (snapshot) => {
1498
- progressUi.render(snapshot);
1499
- },
1500
- ...(syncDir ? { syncDir } : {}),
1501
- ...(ensureAssets !== undefined ? { ensureAssets } : {}),
1502
- ...(targetArtifactId ? { targetArtifactId } : {}),
1503
- });
1504
- progressUi.finish();
1505
- if (process.stderr.isTTY) {
1506
- const synced = run?.synced;
1507
- if (synced && typeof synced === 'object' && typeof synced.dir === 'string') {
1508
- const dir = String(synced.dir);
1509
- const downloaded = typeof synced.downloaded === 'number' ? synced.downloaded : 0;
1510
- const skipped = typeof synced.skipped === 'number' ? synced.skipped : 0;
1511
- const mapPath = typeof synced.mapPath === 'string' ? String(synced.mapPath) : '';
1512
- console.error(`Synced: downloaded=${downloaded} skipped=${skipped} dir=${dir}`);
1513
- if (mapPath)
1514
- console.error(`Map: ${mapPath}`);
1515
- try {
1516
- const mapRaw = mapPath ? await readFile(mapPath, 'utf8') : '';
1517
- const map = mapRaw ? parseJsonOrThrow(mapRaw) : null;
1518
- const files = Array.isArray(map?.files) ? map.files : [];
1519
- const rels = new Set(files
1520
- .map((f) => (typeof f?.relPath === 'string' ? f.relPath : ''))
1521
- .filter((p) => Boolean(p)));
1522
- const hasPrefix = (prefix) => Array.from(rels).some((p) => p === prefix || p.startsWith(prefix + '/'));
1523
- const keyFiles = [];
1524
- const sorted = Array.from(rels).sort();
1525
- const targets = sorted.filter((p) => p.startsWith('target/'));
1526
- const assetPlan = sorted.filter((p) => p === 'assets/asset-plan.json');
1527
- const assets = sorted.filter((p) => p.startsWith('assets/') && p !== 'assets/asset-plan.json');
1528
- const refined = sorted.filter((p) => p.startsWith('refined/'));
1529
- const drafts = sorted.filter((p) => p.startsWith('drafts/'));
1530
- if (targets.length)
1531
- keyFiles.push(targets[0]);
1532
- if (assetPlan.length)
1533
- keyFiles.push(assetPlan[0]);
1534
- if (assets.length)
1535
- keyFiles.push(...assets.slice(0, 3));
1536
- if (refined.length)
1537
- keyFiles.push(...refined.slice(0, 3));
1538
- if (drafts.length)
1539
- keyFiles.push(...drafts.slice(0, 3));
1540
- if (keyFiles.length) {
1541
- const unique = Array.from(new Set(keyFiles));
1542
- console.error(`Key files: ${unique.join(', ')}${sorted.length > unique.length ? ` (+${sorted.length - unique.length} more)` : ''}`);
1543
- }
1544
- }
1545
- catch {
1546
- // Ignore summary failures.
1547
- }
1548
- }
1549
- }
1550
- console.log(pretty(wrap('design.watch', run, { runId, ...(syncDir ? { syncDir } : {}) })));
1551
- process.exit(0);
1552
- }
1553
- if (verb === 'share') {
1554
- const action = (sub || 'status').trim().toLowerCase();
1555
- const input = await readJsonArgs(command.slice(2), flags);
1556
- const runId = String(input.runId ?? '').trim();
1557
- if (!runId)
1558
- throw new Error('Missing required argument: runId');
1559
- if (action === 'status') {
1560
- const res = await client.getRunShareStatus(runId);
1561
- console.log(pretty(res));
1562
- process.exit(0);
1563
- }
1564
- if (action === 'enable' || action === 'create') {
1565
- const rotate = input.rotate === true;
1566
- const res = await client.createRunShare(runId, rotate ? { rotate: true } : {});
1567
- console.log(pretty(res));
1568
- process.exit(0);
1569
- }
1570
- if (action === 'revoke') {
1571
- const res = await client.revokeRunShare(runId);
1572
- console.log(pretty(res));
1573
- process.exit(0);
1574
- }
1575
- throw new Error(`Unknown share action: ${action}. Expected status|enable|revoke.`);
1576
- }
1577
- if (verb === 'get') {
1578
- const input = await readJsonArgs(command.slice(1), flags);
1579
- const runId = String(input.runId ?? '').trim();
1580
- if (!runId)
1581
- throw new Error('Missing required argument: runId');
1582
- const run = await client.getRun(runId);
1583
- console.log(pretty(wrap('design.get', run, { runId })));
1584
- process.exit(0);
1585
- }
1586
- if (verb === 'events') {
1587
- const input = await readJsonArgs(command.slice(1), flags);
1588
- const runId = String(input.runId ?? '').trim();
1589
- if (!runId)
1590
- throw new Error('Missing required argument: runId');
1591
- const events = await client.getRunEvents(runId);
1592
- console.log(pretty(wrap('design.events', events, { runId })));
1118
+ console.log(formatToolResponseMarkdown(wrap('design.check', payload, { runId })));
1593
1119
  process.exit(0);
1594
1120
  }
1595
- if (verb === 'list') {
1596
- const input = await readJsonArgsOptional(command.slice(1), flags);
1597
- const page = typeof input.page === 'number' ? input.page : undefined;
1598
- const limit = typeof input.limit === 'number' ? input.limit : undefined;
1599
- const runs = await client.listRuns({ page, limit });
1600
- console.log(pretty(wrap('design.list', runs)));
1601
- process.exit(0);
1602
- }
1603
- if (verb === 'artifacts' && sub === 'list') {
1604
- const positional = parseArtifactsListPositional(command.slice(2));
1605
- const input = positional ? { runId: positional.runId } : await readJsonArgs(command.slice(2), flags);
1606
- const runId = String(input.runId ?? '').trim();
1607
- if (!runId)
1608
- throw new Error('Missing required argument: runId');
1609
- const detail = await client.getRun(runId);
1610
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
1611
- console.log(pretty(wrap('design.artifacts.list', { artifacts: outputs }, { runId })));
1612
- process.exit(0);
1613
- }
1614
- if (verb === 'artifacts' && sub === 'download') {
1615
- const positional = parseArtifactsDownloadPositional(command.slice(2));
1616
- const input = positional ? { runId: positional.runId, artifactId: positional.artifactId } : await readJsonArgs(command.slice(2), flags);
1617
- const runId = String(input.runId ?? '').trim();
1618
- const artifactId = String(input.artifactId ?? '').trim();
1619
- if (!runId)
1620
- throw new Error('Missing required argument: runId');
1621
- if (!artifactId)
1622
- throw new Error('Missing required argument: artifactId');
1623
- const detail = await client.getRun(runId);
1624
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
1625
- const found = outputs.find((entry) => String(entry?.id ?? '') === artifactId) || null;
1626
- const hint = await (async () => {
1627
- const url = typeof found?.url === 'string' ? found.url.trim() : '';
1628
- if (url) {
1629
- try {
1630
- const u = url.startsWith('http://') || url.startsWith('https://')
1631
- ? new URL(url)
1632
- : new URL(url, 'https://example.invalid');
1633
- return u.pathname.split('/').filter(Boolean).pop() || undefined;
1634
- }
1635
- catch {
1636
- return url.split('/').filter(Boolean).pop() || undefined;
1637
- }
1638
- }
1639
- try {
1640
- const artifacts = await client.listArtifacts(runId);
1641
- const record = Array.isArray(artifacts?.artifacts)
1642
- ? artifacts.artifacts.find((a) => String(a?.id ?? '') === artifactId)
1643
- : null;
1644
- const storageKey = typeof record?.storageKey === 'string' ? record.storageKey.trim() : '';
1645
- return storageKey ? storageKey.split('/').filter(Boolean).pop() || undefined : undefined;
1646
- }
1647
- catch {
1648
- return undefined;
1649
- }
1650
- })();
1651
- const downloaded = await (async () => {
1652
- try {
1653
- return await client.downloadArtifactToCache(runId, artifactId, { fileNameHint: hint });
1654
- }
1655
- catch (error) {
1656
- const url = typeof found?.url === 'string' ? found.url.trim() : '';
1657
- if (url) {
1658
- return await client.downloadUrlToCache(runId, artifactId, url, { fileNameHint: hint });
1659
- }
1660
- throw error;
1661
- }
1662
- })();
1663
- console.log(pretty(wrap('design.artifacts.download', downloaded, { runId, artifactId })));
1664
- process.exit(0);
1665
- }
1666
- if (verb === 'artifacts') {
1667
- throw new Error('Usage: artifacts list <runId> | artifacts download <runId>/<artifactId>\n' +
1668
- 'Tip: you can still use --json or pipe a JSON object via stdin.');
1669
- }
1670
1121
  // Should be unreachable due to the verb guard.
1671
1122
  throw new Error(`Unknown command: ${verb}`);
1672
1123
  }
@@ -1759,6 +1210,11 @@ async function main() {
1759
1210
  console.log(JSON.stringify(safe, null, 2));
1760
1211
  process.exit(0);
1761
1212
  }
1213
+ if (verb) {
1214
+ console.error(`Unknown command: ${verb}`);
1215
+ printHelp();
1216
+ process.exit(1);
1217
+ }
1762
1218
  // Default: run MCP server.
1763
1219
  const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
1764
1220
  const sessionToken = process.env.DESIGN_MCP_SESSION_TOKEN?.trim();