@just-every/design 0.1.31 → 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,217 +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
- const screenshotOk = () => {
203
- try {
204
- return statSync(outPath).size > 0;
205
- }
206
- catch {
207
- return false;
208
- }
209
- };
210
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
211
- const waitForScreenshotStable = async (signal) => {
212
- const start = Date.now();
213
- // Chrome writes the screenshot file as a single operation most of the time, but we've seen
214
- // occasional flakiness where the file exists briefly at size=0. Wait for a stable >0 size.
215
- let lastSize = -1;
216
- let stableTicks = 0;
217
- while (Date.now() - start < timeoutMs) {
218
- if (signal.aborted)
219
- return false;
220
- let size = -1;
221
- try {
222
- size = statSync(outPath).size;
223
- }
224
- catch {
225
- size = -1;
226
- }
227
- if (size > 0) {
228
- if (size === lastSize)
229
- stableTicks += 1;
230
- else
231
- stableTicks = 0;
232
- lastSize = size;
233
- if (stableTicks >= 2)
234
- return true;
235
- }
236
- await sleep(100);
237
- }
238
- if (signal.aborted)
239
- return false;
240
- throw new Error('Timed out waiting for screenshot file to be produced.');
241
- };
242
- // Use an isolated process group on Unix so we can reliably kill Chrome + its helpers.
243
- const child = spawn(browser, argsList, {
244
- stdio: ['ignore', 'pipe', 'pipe'],
245
- detached: process.platform !== 'win32',
246
- });
247
- let stdout = '';
248
- let stderr = '';
249
- child.stdout?.setEncoding('utf8');
250
- child.stderr?.setEncoding('utf8');
251
- child.stdout?.on('data', (chunk) => {
252
- stdout += String(chunk);
253
- });
254
- child.stderr?.on('data', (chunk) => {
255
- stderr += String(chunk);
256
- });
257
- const killChild = (signal) => {
258
- if (!child.pid)
259
- return;
260
- try {
261
- if (process.platform !== 'win32') {
262
- // Kill the whole process group (negative PID) when detached.
263
- process.kill(-child.pid, signal);
264
- }
265
- else {
266
- process.kill(child.pid, signal);
267
- }
268
- }
269
- catch {
270
- // ignore
271
- }
272
- };
273
- const exitPromise = new Promise((resolve, reject) => {
274
- child.once('error', reject);
275
- child.once('exit', (code, signal) => resolve({ code, signal }));
276
- });
277
- const abort = new AbortController();
278
- const screenshotPromise = waitForScreenshotStable(abort.signal)
279
- .then((ok) => ({ type: 'screenshot', ok }))
280
- .catch((error) => ({ type: 'screenshot_error', error }));
281
- const exitPromiseHandled = exitPromise
282
- .then((r) => ({ type: 'exit', ...r }))
283
- .catch((error) => ({ type: 'exit_error', error }));
284
- // Hard-kill after the timeout budget; Chrome can hang even after producing a screenshot.
285
- const timeout = setTimeout(() => {
286
- killChild('SIGKILL');
287
- }, timeoutMs);
288
- try {
289
- const first = await Promise.race([screenshotPromise, exitPromiseHandled]);
290
- abort.abort();
291
- if (first.type === 'exit_error') {
292
- const message = first.error instanceof Error ? first.error.message : String(first.error);
293
- throw new Error(`Screenshot failed (spawn error): ${message}`);
294
- }
295
- if (first.type === 'screenshot_error') {
296
- // Still allow a best-effort success if Chrome produced a screenshot before hanging.
297
- killChild('SIGKILL');
298
- await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
299
- }
300
- // If the screenshot is ready but Chrome is still running, kill it immediately instead of
301
- // waiting for the timeout (which is often ~waitMs+30s).
302
- if (first.type === 'screenshot' && first.ok) {
303
- killChild('SIGKILL');
304
- // Don't await indefinitely; cleanup is best-effort.
305
- await Promise.race([exitPromise.catch(() => undefined), sleep(1_500)]);
306
- }
307
- const ok = screenshotOk();
308
- if (!ok) {
309
- // Chrome may exit without producing a screenshot; include output to help debug.
310
- const detail = (stderr || stdout).trim();
311
- const exit = first.type === 'exit' ? first : await exitPromise.catch(() => ({ code: null, signal: null }));
312
- throw new Error(`Screenshot failed (exit ${exit.code ?? 'unknown'}, signal ${exit.signal ?? 'none'}): ${detail || 'no output'}`);
313
- }
314
- }
315
- finally {
316
- clearTimeout(timeout);
317
- // Ensure Chrome isn't left running in the background.
318
- killChild('SIGKILL');
319
- }
320
- return {
321
- screenshotPath: outPath,
322
- width: args.width,
323
- height: args.height,
324
- engine: 'chrome-headless',
325
- binary: browser,
326
- };
327
- }
328
- finally {
329
- const { rm } = await import('node:fs/promises');
330
- await rm(tmpProfile, { recursive: true, force: true }).catch(() => undefined);
331
- }
332
- }
333
103
  function isTtyInteractive() {
334
104
  return Boolean(process.stdin.isTTY && process.stderr.isTTY);
335
105
  }
@@ -381,76 +151,6 @@ async function readJsonArgs(remainingArgs, flags) {
381
151
  }
382
152
  return parsed;
383
153
  }
384
- async function readJsonArgsOptional(remainingArgs, flags) {
385
- const jsonFlag = resolveFlagString(flags, 'json');
386
- const hasInline = remainingArgs.some((t) => t.trim().length > 0);
387
- const hasStdin = !process.stdin.isTTY;
388
- if (!jsonFlag && !hasInline && !hasStdin)
389
- return {};
390
- return readJsonArgs(remainingArgs, flags);
391
- }
392
- function pretty(value) {
393
- return JSON.stringify(value, null, 2);
394
- }
395
- function createWatchProgressDisplay(stream, enabled) {
396
- let lastRendered = '';
397
- let lastSnapshot = null;
398
- let spinner = 0;
399
- function clearLine() {
400
- stream.write('\r\x1b[2K');
401
- }
402
- function render(snapshot) {
403
- if (!enabled)
404
- return;
405
- lastSnapshot = snapshot;
406
- spinner = (spinner + 1) % 4;
407
- const stage = snapshot.stage ? String(snapshot.stage) : '';
408
- const status = snapshot.status ? String(snapshot.status) : '';
409
- const progress = typeof snapshot.progress === 'number' ? snapshot.progress : null;
410
- const pct = typeof progress === 'number' ? Math.max(0, Math.min(100, Math.round(progress * 100))) : null;
411
- const barWidth = 24;
412
- const filled = pct === null ? 0 : Math.round((pct / 100) * barWidth);
413
- const bar = `[${'#'.repeat(Math.max(0, Math.min(barWidth, filled)))}${'-'.repeat(Math.max(0, barWidth - filled))}]`;
414
- const spinChar = ['|', '/', '-', '\\'][spinner] ?? '|';
415
- const parts = [];
416
- if (pct === null) {
417
- parts.push(`${spinChar} ${bar}`);
418
- }
419
- else {
420
- parts.push(`${bar} ${String(pct).padStart(3, ' ')}%`);
421
- }
422
- if (stage)
423
- parts.push(`stage=${stage}`);
424
- if (status)
425
- parts.push(`status=${status}`);
426
- const line = parts.join(' ');
427
- if (line === lastRendered)
428
- return;
429
- lastRendered = line;
430
- clearLine();
431
- stream.write(line);
432
- }
433
- function log(line) {
434
- if (!enabled) {
435
- stream.write(`${line}\n`);
436
- return;
437
- }
438
- clearLine();
439
- stream.write(`${line}\n`);
440
- if (lastSnapshot) {
441
- lastRendered = '';
442
- render(lastSnapshot);
443
- }
444
- }
445
- function finish() {
446
- if (!enabled)
447
- return;
448
- clearLine();
449
- lastRendered = '';
450
- lastSnapshot = null;
451
- }
452
- return { render, log, finish, enabled };
453
- }
454
154
  async function promptYesNo(question, defaultYes = false) {
455
155
  if (!isTtyInteractive())
456
156
  return defaultYes;
@@ -923,32 +623,6 @@ async function main() {
923
623
  const loginOrigin = resolveLoginOrigin(flags);
924
624
  const verb = command[0] ?? '';
925
625
  const sub = command[1] ?? '';
926
- if (verb === 'screenshot') {
927
- const url = String(command[1] ?? '').trim();
928
- const width = Number.parseInt(String(resolveFlagString(flags, 'width') ?? '1696'), 10);
929
- const height = Number.parseInt(String(resolveFlagString(flags, 'height') ?? '2528'), 10);
930
- const waitMs = Number.parseInt(String(resolveFlagString(flags, 'wait-ms') ?? '4000'), 10);
931
- const out = resolveFlagString(flags, 'out')?.trim();
932
- const outPath = out
933
- ? out
934
- : path.join(process.cwd(), `every-design-screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`);
935
- try {
936
- const result = await runScreenshotCommand({
937
- url,
938
- outPath,
939
- width: Number.isFinite(width) && width > 0 ? width : 1696,
940
- height: Number.isFinite(height) && height > 0 ? height : 2528,
941
- waitMs: Number.isFinite(waitMs) ? waitMs : 4000,
942
- });
943
- console.log(pretty(result));
944
- process.exit(0);
945
- }
946
- catch (error) {
947
- const message = error instanceof Error ? error.message : String(error);
948
- console.error(message);
949
- process.exit(1);
950
- }
951
- }
952
626
  if (verb === 'install') {
953
627
  const yes = Boolean(flags.yes);
954
628
  const dryRun = Boolean(flags['dry-run']);
@@ -1255,22 +929,8 @@ async function main() {
1255
929
  }
1256
930
  process.exit(0);
1257
931
  }
1258
- // CLI helpers (non-MCP). These are mostly useful for Skills/playbooks and scripting.
1259
- if (verb === 'create'
1260
- || verb === 'upload-target'
1261
- || verb === 'refine-draft'
1262
- || verb === 'set-target'
1263
- || verb === 'clear-target'
1264
- || verb === 'extract-assets'
1265
- || verb === 'generate-html'
1266
- || verb === 'iterate'
1267
- || verb === 'critique'
1268
- || verb === 'watch'
1269
- || verb === 'share'
1270
- || verb === 'get'
1271
- || verb === 'list'
1272
- || verb === 'events'
1273
- || verb === 'artifacts') {
932
+ // CLI helpers (non-MCP). CLIs must follow: create -> sync -> check.
933
+ if (verb === 'create' || verb === 'sync' || verb === 'check') {
1274
934
  const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
1275
935
  const sessionToken = (process.env.DESIGN_MCP_SESSION_TOKEN ?? cfg.sessionToken ?? '').trim();
1276
936
  if (!sessionToken) {
@@ -1288,139 +948,116 @@ async function main() {
1288
948
  const input = await readJsonArgs(command.slice(1), flags);
1289
949
  const { prompt, config, parentRunId } = await buildCreateRunRequest(client, input);
1290
950
  const created = await client.createRun({ prompt, config, ...(parentRunId ? { parentRunId } : {}) });
1291
- console.log(pretty(wrap('design.create', created)));
951
+ console.log(formatToolResponseMarkdown(wrap('design.create', created)));
1292
952
  process.exit(0);
1293
953
  }
1294
- if (verb === 'upload-target') {
954
+ if (verb === 'sync') {
1295
955
  const input = await readJsonArgs(command.slice(1), flags);
1296
956
  const runId = String(input.runId ?? '').trim();
1297
957
  if (!runId)
1298
958
  throw new Error('Missing required argument: runId');
1299
- const sourceImage = input.sourceImage && typeof input.sourceImage === 'object' ? input.sourceImage : null;
1300
- if (!sourceImage) {
1301
- throw new Error('Missing required argument: sourceImage');
1302
- }
1303
- const res = await client.uploadRunTarget(runId, sourceImage);
1304
- console.log(pretty(wrap('design.uploadTarget', res, { runId })));
1305
- process.exit(0);
1306
- }
1307
- if (verb === 'refine-draft') {
1308
- const input = await readJsonArgs(command.slice(1), flags);
1309
- const runId = String(input.runId ?? '').trim();
1310
- const artifactId = String(input.artifactId ?? '').trim();
1311
- const prompt = String(input.prompt ?? '').trim();
1312
- if (!runId)
1313
- throw new Error('Missing required argument: runId');
1314
- if (!artifactId)
1315
- throw new Error('Missing required argument: artifactId');
1316
- if (!prompt)
1317
- throw new Error('Missing required argument: prompt');
1318
- const res = await client.refineDraft(runId, artifactId, prompt);
1319
- console.log(pretty(wrap('design.refineDraft', res, { runId, artifactId })));
1320
- process.exit(0);
1321
- }
1322
- if (verb === 'set-target') {
1323
- const input = await readJsonArgs(command.slice(1), flags);
1324
- const runId = String(input.runId ?? '').trim();
1325
- const artifactId = String(input.artifactId ?? '').trim();
1326
- if (!runId)
1327
- throw new Error('Missing required argument: runId');
1328
- if (!artifactId)
1329
- throw new Error('Missing required argument: artifactId');
1330
- const res = await client.setRunTarget(runId, artifactId);
1331
- console.log(pretty(wrap('design.setTarget', res, { runId, artifactId })));
1332
- process.exit(0);
1333
- }
1334
- if (verb === 'clear-target') {
1335
- const input = await readJsonArgs(command.slice(1), flags);
1336
- const runId = String(input.runId ?? '').trim();
1337
- if (!runId)
1338
- throw new Error('Missing required argument: runId');
1339
- const res = await client.clearRunTarget(runId);
1340
- console.log(pretty(wrap('design.clearTarget', res, { runId })));
1341
- process.exit(0);
1342
- }
1343
- if (verb === 'extract-assets') {
1344
- const input = await readJsonArgs(command.slice(1), flags);
1345
- const runId = String(input.runId ?? '').trim();
1346
- if (!runId)
1347
- throw new Error('Missing required argument: runId');
1348
- const targetArtifactId = typeof input.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
1349
- const rawKind = typeof input.designKind === 'string' ? input.designKind.trim().toLowerCase() : '';
1350
- const designKind = rawKind === 'interface' || rawKind === 'html' ? 'interface' : undefined;
1351
- if (rawKind && !designKind) {
1352
- 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);
1353
972
  }
1354
- if (targetArtifactId) {
1355
- 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);
1356
996
  }
1357
- const progressed = await client.progressRun(runId, { action: 'extract_assets', ...(designKind ? { designKind } : {}) });
1358
- console.log(pretty(wrap('design.extractAssets', progressed, {
1359
- runId,
1360
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
1361
- })));
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 })));
1362
1000
  process.exit(0);
1363
1001
  }
1364
- if (verb === 'generate-html') {
1002
+ if (verb === 'check') {
1365
1003
  const input = await readJsonArgs(command.slice(1), flags);
1366
1004
  const runId = String(input.runId ?? '').trim();
1367
1005
  if (!runId)
1368
1006
  throw new Error('Missing required argument: runId');
1369
- const targetArtifactId = typeof input.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
1370
- if (targetArtifactId) {
1371
- await client.setRunTarget(runId, targetArtifactId);
1372
- }
1373
- const progressed = await client.progressRun(runId, { action: 'generate_html' });
1374
- console.log(pretty(wrap('design.generateHtml', progressed, {
1375
- runId,
1376
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
1377
- })));
1378
- process.exit(0);
1379
- }
1380
- if (verb === 'iterate' || verb === 'critique') {
1381
- const input = await readJsonArgs(command.slice(1), flags);
1382
- if (verb === 'critique') {
1383
- 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();
1384
1027
  }
1385
- const runId = String(input.runId ?? '').trim();
1386
- if (!runId)
1387
- throw new Error('Missing required argument: runId');
1388
- const concerns = typeof input.concerns === 'string' ? input.concerns.trim() : '';
1389
- const render = Array.isArray(input.render) ? input.render : [];
1390
- if (render.length === 0)
1391
- throw new Error('Missing required argument: render (array of images)');
1392
- const target = Array.isArray(input.target) ? input.target : [];
1393
- const viewportInput = input.viewport && typeof input.viewport === 'object' ? input.viewport : null;
1394
- const viewport = {
1395
- width: Number(viewportInput?.width) || 1696,
1396
- height: Number(viewportInput?.height) || 2528,
1397
- };
1398
- const renderRefs = await client.uploadSourceImages(render);
1399
- 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 }]);
1400
1037
  const created = await client.createRunOperation(runId, {
1401
1038
  type: 'iterate',
1402
- ...(concerns ? { concerns } : {}),
1403
- viewport,
1039
+ viewport: { width: screenshot.width, height: screenshot.height },
1404
1040
  render: renderRefs,
1405
- ...(targetRefs.length > 0 ? { target: targetRefs } : {}),
1406
1041
  });
1407
1042
  const operationId = String(created?.operation?.id ?? '').trim();
1408
1043
  if (!operationId)
1409
1044
  throw new Error('Iterate operation created but missing operation id');
1410
- const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 3600;
1411
- const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
1412
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1413
- const deadlineMs = Date.now() + Math.max(1, timeoutSeconds) * 1000;
1045
+ const deadlineMs = Date.now() + 900 * 1000;
1046
+ let terminalStatus = '';
1414
1047
  while (true) {
1415
1048
  const op = await client.getOperation(operationId);
1416
1049
  const status = String(op?.operation?.status ?? '').trim().toLowerCase();
1417
1050
  if (status === 'completed' || status === 'failed' || status === 'cancelled') {
1051
+ terminalStatus = status;
1418
1052
  break;
1419
1053
  }
1420
1054
  if (Date.now() > deadlineMs) {
1421
1055
  throw new Error(`Timed out waiting for iterate operation (${operationId})`);
1422
1056
  }
1423
- 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'}`);
1424
1061
  }
1425
1062
  const detail = await client.getRun(runId);
1426
1063
  const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
@@ -1443,12 +1080,16 @@ async function main() {
1443
1080
  runId,
1444
1081
  operationId,
1445
1082
  status: 'completed',
1083
+ screenshot: {
1084
+ filePath: screenshot.screenshotPath,
1085
+ width: screenshot.width,
1086
+ height: screenshot.height,
1087
+ },
1446
1088
  iterate: null,
1447
- critique: null,
1448
1089
  generatedAssets,
1449
1090
  error: 'Missing critique artifact for iterate operation',
1450
1091
  };
1451
- console.log(pretty(wrap('design.iterate', payload, { runId })));
1092
+ console.log(formatToolResponseMarkdown(wrap('design.check', payload, { runId })));
1452
1093
  process.exit(0);
1453
1094
  }
1454
1095
  const url = typeof critiqueArtifact?.url === 'string' ? String(critiqueArtifact.url) : '';
@@ -1456,213 +1097,27 @@ async function main() {
1456
1097
  throw new Error('Critique artifact is missing a download url');
1457
1098
  const downloaded = await client.downloadUrlToCache(runId, critiqueArtifact.id, url, { fileNameHint: 'iterate.json' });
1458
1099
  const raw = await readFile(downloaded.filePath, 'utf8');
1459
- const parsed = parseJsonOrThrow(raw);
1100
+ let parsed;
1101
+ try {
1102
+ parsed = JSON.parse(raw);
1103
+ }
1104
+ catch {
1105
+ parsed = raw;
1106
+ }
1460
1107
  const payload = {
1461
1108
  runId,
1462
- operationId,
1109
+ screenshot: {
1110
+ filePath: screenshot.screenshotPath,
1111
+ width: screenshot.width,
1112
+ height: screenshot.height,
1113
+ },
1463
1114
  iterate: parsed,
1464
- critique: parsed,
1465
1115
  artifact: { id: critiqueArtifact.id, filePath: downloaded.filePath },
1466
1116
  generatedAssets,
1467
1117
  };
1468
- console.log(pretty(wrap('design.iterate', payload, { runId })));
1118
+ console.log(formatToolResponseMarkdown(wrap('design.check', payload, { runId })));
1469
1119
  process.exit(0);
1470
1120
  }
1471
- if (verb === 'watch') {
1472
- const input = await readJsonArgs(command.slice(1), flags);
1473
- const runId = String(input.runId ?? '').trim();
1474
- if (!runId)
1475
- throw new Error('Missing required argument: runId');
1476
- const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
1477
- const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
1478
- const syncDir = typeof input.syncDir === 'string' && input.syncDir.trim() ? input.syncDir.trim() : undefined;
1479
- const ensureAssets = typeof input.ensureAssets === 'boolean' ? input.ensureAssets : undefined;
1480
- const targetArtifactId = typeof input.targetArtifactId === 'string' && input.targetArtifactId.trim()
1481
- ? input.targetArtifactId.trim()
1482
- : undefined;
1483
- const progressUi = createWatchProgressDisplay(process.stderr, Boolean(process.stderr.isTTY));
1484
- const log = (line) => {
1485
- if (progressUi.enabled && line.startsWith('[design.watch] status='))
1486
- return;
1487
- progressUi.log(line);
1488
- };
1489
- const run = await watchRun(client, runId, {
1490
- timeoutSeconds,
1491
- intervalSeconds,
1492
- log,
1493
- onUpdate: (snapshot) => {
1494
- progressUi.render(snapshot);
1495
- },
1496
- ...(syncDir ? { syncDir } : {}),
1497
- ...(ensureAssets !== undefined ? { ensureAssets } : {}),
1498
- ...(targetArtifactId ? { targetArtifactId } : {}),
1499
- });
1500
- progressUi.finish();
1501
- if (process.stderr.isTTY) {
1502
- const synced = run?.synced;
1503
- if (synced && typeof synced === 'object' && typeof synced.dir === 'string') {
1504
- const dir = String(synced.dir);
1505
- const downloaded = typeof synced.downloaded === 'number' ? synced.downloaded : 0;
1506
- const skipped = typeof synced.skipped === 'number' ? synced.skipped : 0;
1507
- const mapPath = typeof synced.mapPath === 'string' ? String(synced.mapPath) : '';
1508
- console.error(`Synced: downloaded=${downloaded} skipped=${skipped} dir=${dir}`);
1509
- if (mapPath)
1510
- console.error(`Map: ${mapPath}`);
1511
- try {
1512
- const mapRaw = mapPath ? await readFile(mapPath, 'utf8') : '';
1513
- const map = mapRaw ? parseJsonOrThrow(mapRaw) : null;
1514
- const files = Array.isArray(map?.files) ? map.files : [];
1515
- const rels = new Set(files
1516
- .map((f) => (typeof f?.relPath === 'string' ? f.relPath : ''))
1517
- .filter((p) => Boolean(p)));
1518
- const hasPrefix = (prefix) => Array.from(rels).some((p) => p === prefix || p.startsWith(prefix + '/'));
1519
- const keyFiles = [];
1520
- const sorted = Array.from(rels).sort();
1521
- const targets = sorted.filter((p) => p.startsWith('target/'));
1522
- const assetPlan = sorted.filter((p) => p === 'assets/asset-plan.json');
1523
- const assets = sorted.filter((p) => p.startsWith('assets/') && p !== 'assets/asset-plan.json');
1524
- const refined = sorted.filter((p) => p.startsWith('refined/'));
1525
- const drafts = sorted.filter((p) => p.startsWith('drafts/'));
1526
- if (targets.length)
1527
- keyFiles.push(targets[0]);
1528
- if (assetPlan.length)
1529
- keyFiles.push(assetPlan[0]);
1530
- if (assets.length)
1531
- keyFiles.push(...assets.slice(0, 3));
1532
- if (refined.length)
1533
- keyFiles.push(...refined.slice(0, 3));
1534
- if (drafts.length)
1535
- keyFiles.push(...drafts.slice(0, 3));
1536
- if (keyFiles.length) {
1537
- const unique = Array.from(new Set(keyFiles));
1538
- console.error(`Key files: ${unique.join(', ')}${sorted.length > unique.length ? ` (+${sorted.length - unique.length} more)` : ''}`);
1539
- }
1540
- }
1541
- catch {
1542
- // Ignore summary failures.
1543
- }
1544
- }
1545
- }
1546
- console.log(pretty(wrap('design.watch', run, { runId, ...(syncDir ? { syncDir } : {}) })));
1547
- process.exit(0);
1548
- }
1549
- if (verb === 'share') {
1550
- const action = (sub || 'status').trim().toLowerCase();
1551
- const input = await readJsonArgs(command.slice(2), flags);
1552
- const runId = String(input.runId ?? '').trim();
1553
- if (!runId)
1554
- throw new Error('Missing required argument: runId');
1555
- if (action === 'status') {
1556
- const res = await client.getRunShareStatus(runId);
1557
- console.log(pretty(res));
1558
- process.exit(0);
1559
- }
1560
- if (action === 'enable' || action === 'create') {
1561
- const rotate = input.rotate === true;
1562
- const res = await client.createRunShare(runId, rotate ? { rotate: true } : {});
1563
- console.log(pretty(res));
1564
- process.exit(0);
1565
- }
1566
- if (action === 'revoke') {
1567
- const res = await client.revokeRunShare(runId);
1568
- console.log(pretty(res));
1569
- process.exit(0);
1570
- }
1571
- throw new Error(`Unknown share action: ${action}. Expected status|enable|revoke.`);
1572
- }
1573
- if (verb === 'get') {
1574
- const input = await readJsonArgs(command.slice(1), flags);
1575
- const runId = String(input.runId ?? '').trim();
1576
- if (!runId)
1577
- throw new Error('Missing required argument: runId');
1578
- const run = await client.getRun(runId);
1579
- console.log(pretty(wrap('design.get', run, { runId })));
1580
- process.exit(0);
1581
- }
1582
- if (verb === 'events') {
1583
- const input = await readJsonArgs(command.slice(1), flags);
1584
- const runId = String(input.runId ?? '').trim();
1585
- if (!runId)
1586
- throw new Error('Missing required argument: runId');
1587
- const events = await client.getRunEvents(runId);
1588
- console.log(pretty(wrap('design.events', events, { runId })));
1589
- process.exit(0);
1590
- }
1591
- if (verb === 'list') {
1592
- const input = await readJsonArgsOptional(command.slice(1), flags);
1593
- const page = typeof input.page === 'number' ? input.page : undefined;
1594
- const limit = typeof input.limit === 'number' ? input.limit : undefined;
1595
- const runs = await client.listRuns({ page, limit });
1596
- console.log(pretty(wrap('design.list', runs)));
1597
- process.exit(0);
1598
- }
1599
- if (verb === 'artifacts' && sub === 'list') {
1600
- const positional = parseArtifactsListPositional(command.slice(2));
1601
- const input = positional ? { runId: positional.runId } : await readJsonArgs(command.slice(2), flags);
1602
- const runId = String(input.runId ?? '').trim();
1603
- if (!runId)
1604
- throw new Error('Missing required argument: runId');
1605
- const detail = await client.getRun(runId);
1606
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
1607
- console.log(pretty(wrap('design.artifacts.list', { artifacts: outputs }, { runId })));
1608
- process.exit(0);
1609
- }
1610
- if (verb === 'artifacts' && sub === 'download') {
1611
- const positional = parseArtifactsDownloadPositional(command.slice(2));
1612
- const input = positional ? { runId: positional.runId, artifactId: positional.artifactId } : await readJsonArgs(command.slice(2), flags);
1613
- const runId = String(input.runId ?? '').trim();
1614
- const artifactId = String(input.artifactId ?? '').trim();
1615
- if (!runId)
1616
- throw new Error('Missing required argument: runId');
1617
- if (!artifactId)
1618
- throw new Error('Missing required argument: artifactId');
1619
- const detail = await client.getRun(runId);
1620
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
1621
- const found = outputs.find((entry) => String(entry?.id ?? '') === artifactId) || null;
1622
- const hint = await (async () => {
1623
- const url = typeof found?.url === 'string' ? found.url.trim() : '';
1624
- if (url) {
1625
- try {
1626
- const u = url.startsWith('http://') || url.startsWith('https://')
1627
- ? new URL(url)
1628
- : new URL(url, 'https://example.invalid');
1629
- return u.pathname.split('/').filter(Boolean).pop() || undefined;
1630
- }
1631
- catch {
1632
- return url.split('/').filter(Boolean).pop() || undefined;
1633
- }
1634
- }
1635
- try {
1636
- const artifacts = await client.listArtifacts(runId);
1637
- const record = Array.isArray(artifacts?.artifacts)
1638
- ? artifacts.artifacts.find((a) => String(a?.id ?? '') === artifactId)
1639
- : null;
1640
- const storageKey = typeof record?.storageKey === 'string' ? record.storageKey.trim() : '';
1641
- return storageKey ? storageKey.split('/').filter(Boolean).pop() || undefined : undefined;
1642
- }
1643
- catch {
1644
- return undefined;
1645
- }
1646
- })();
1647
- const downloaded = await (async () => {
1648
- try {
1649
- return await client.downloadArtifactToCache(runId, artifactId, { fileNameHint: hint });
1650
- }
1651
- catch (error) {
1652
- const url = typeof found?.url === 'string' ? found.url.trim() : '';
1653
- if (url) {
1654
- return await client.downloadUrlToCache(runId, artifactId, url, { fileNameHint: hint });
1655
- }
1656
- throw error;
1657
- }
1658
- })();
1659
- console.log(pretty(wrap('design.artifacts.download', downloaded, { runId, artifactId })));
1660
- process.exit(0);
1661
- }
1662
- if (verb === 'artifacts') {
1663
- throw new Error('Usage: artifacts list <runId> | artifacts download <runId>/<artifactId>\n' +
1664
- 'Tip: you can still use --json or pipe a JSON object via stdin.');
1665
- }
1666
1121
  // Should be unreachable due to the verb guard.
1667
1122
  throw new Error(`Unknown command: ${verb}`);
1668
1123
  }
@@ -1755,6 +1210,11 @@ async function main() {
1755
1210
  console.log(JSON.stringify(safe, null, 2));
1756
1211
  process.exit(0);
1757
1212
  }
1213
+ if (verb) {
1214
+ console.error(`Unknown command: ${verb}`);
1215
+ printHelp();
1216
+ process.exit(1);
1217
+ }
1758
1218
  // Default: run MCP server.
1759
1219
  const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
1760
1220
  const sessionToken = process.env.DESIGN_MCP_SESSION_TOKEN?.trim();