@just-every/design 0.1.1 → 0.1.3

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
@@ -3,12 +3,19 @@
3
3
  * CLI entrypoint for @just-every/design
4
4
  */
5
5
  import { createInterface } from 'node:readline/promises';
6
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
7
+ import { existsSync } from 'node:fs';
8
+ import { spawnSync } from 'node:child_process';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
6
11
  import { runApprovalLinkLoginFlow } from './auth.js';
12
+ import { refreshLoginSession } from './auth.js';
7
13
  import { pickAccountSlug } from './account-picker.js';
8
14
  import { clearConfig, readConfig, resolveConfigPath, writeConfig } from './config.js';
9
15
  import { DesignAppClient } from './design-client.js';
10
- import { detectDefaultClients, runInstall } from './install.js';
16
+ import { detectClients, detectDefaultClients, runInstall, runRemove, } from './install.js';
11
17
  import { startMcpServer } from './server.js';
18
+ import { buildCreateRunRequest, NOT_AUTHENTICATED_HELP, waitForRun } from './tool-logic.js';
12
19
  function parseArgs(argv) {
13
20
  const flags = {};
14
21
  const command = [];
@@ -42,12 +49,22 @@ function parseArgs(argv) {
42
49
  function printHelp() {
43
50
  const configPath = resolveConfigPath();
44
51
  console.error('Usage:');
45
- console.error(' npx @just-every/design [command] [options]');
52
+ console.error(' npx -y @just-every/design@latest [command] [options]');
46
53
  console.error(' design [command] [options]');
47
54
  console.error('');
48
55
  console.error('Commands:');
49
56
  console.error(' (default) Start the MCP server (stdio)');
50
- console.error(' install Install MCP config entries for common clients');
57
+ console.error(' install Interactive setup (auth + client config)');
58
+ console.error(' remove Remove Every Design from detected clients');
59
+ console.error(' create Create a design run (CLI helper)');
60
+ console.error(' screenshot <url> Screenshot a URL (CLI helper)');
61
+ console.error(' critique Critique a target vs current screenshot (CLI helper)');
62
+ console.error(' wait Wait for a design run (CLI helper)');
63
+ console.error(' get Fetch a run by id (CLI helper)');
64
+ console.error(' list List recent runs (CLI helper)');
65
+ console.error(' events Fetch run events (CLI helper)');
66
+ console.error(' artifacts list List run artifacts (CLI helper)');
67
+ console.error(' artifacts download Download an artifact (CLI helper)');
51
68
  console.error(' auth login Login via approval-link flow and save config');
52
69
  console.error(' auth status Check current auth against /api/me');
53
70
  console.error(' auth logout Delete the saved config file');
@@ -58,14 +75,448 @@ function printHelp() {
58
75
  console.error(' --account <slug> Preselect company/account slug (skips interactive prompt)');
59
76
  console.error(` --config <path> Config path (default: ${configPath})`);
60
77
  console.error(' --no-open Do not open the approval URL in a browser (auth login)');
78
+ console.error(' --json <string> JSON args payload for CLI helpers (or pipe JSON via stdin)');
79
+ console.error(' --out <path> Output path (screenshot)');
80
+ console.error(' --width <px> Viewport width (screenshot, default 1696)');
81
+ console.error(' --height <px> Viewport height (screenshot, default 2528)');
82
+ console.error(' --wait-ms <ms> Extra wait budget before screenshot (screenshot, default 4000)');
61
83
  console.error(' --client <name[,name...]> Install target(s): code,codex,claude-desktop,claude-code,cursor,gemini,qwen,all,auto');
62
84
  console.error(' --name <serverName> MCP server name key (default: every-design)');
85
+ console.error(' --launcher <npx|local> How clients launch the MCP server (default for install: local)');
86
+ console.error(' --no-path Do not modify shell config to add ~/.local/bin to PATH');
63
87
  console.error(' --yes Non-interactive install (no prompts)');
64
88
  console.error(' --dry-run Print what would change, but do not write');
65
89
  console.error(' --force Overwrite existing entries');
66
- console.error(' --no-skills Do not install Codex/Code skill playbook');
90
+ console.error(' --no-skills Do not install local Skills/playbooks');
67
91
  console.error(' --help, -h Show help');
68
92
  }
93
+ function which(bin) {
94
+ try {
95
+ const tool = process.platform === 'win32' ? 'where' : 'which';
96
+ const res = spawnSync(tool, [bin], { encoding: 'utf8' });
97
+ if (res.status !== 0)
98
+ return null;
99
+ const first = String(res.stdout || '').trim().split(/\r?\n/)[0];
100
+ return first ? first.trim() : null;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function resolveHeadlessBrowserBinary() {
107
+ const explicit = process.env.CHROME_PATH?.trim();
108
+ if (explicit && existsSync(explicit))
109
+ return explicit;
110
+ const candidates = [
111
+ 'google-chrome',
112
+ 'google-chrome-stable',
113
+ 'chromium',
114
+ 'chromium-browser',
115
+ 'chrome',
116
+ 'msedge',
117
+ 'microsoft-edge',
118
+ ];
119
+ for (const c of candidates) {
120
+ const found = which(c);
121
+ if (found)
122
+ return found;
123
+ }
124
+ if (process.platform === 'darwin') {
125
+ const macCandidates = [
126
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
127
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
128
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
129
+ ];
130
+ for (const c of macCandidates) {
131
+ if (existsSync(c))
132
+ return c;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ async function runScreenshotCommand(args) {
138
+ const url = args.url.trim();
139
+ if (!url)
140
+ throw new Error('Missing URL. Usage: every-design screenshot http://127.0.0.1:3000/path');
141
+ const outPath = path.resolve(args.outPath);
142
+ await mkdir(path.dirname(outPath), { recursive: true });
143
+ const browser = resolveHeadlessBrowserBinary();
144
+ if (!browser) {
145
+ throw new Error('No headless Chrome/Chromium/Edge binary found. Install a Chromium-based browser or set CHROME_PATH to an executable path.');
146
+ }
147
+ const tmpProfile = await (async () => {
148
+ const base = path.join(os.tmpdir(), 'every-design-screenshot-');
149
+ const { mkdtemp } = await import('node:fs/promises');
150
+ return mkdtemp(base);
151
+ })();
152
+ try {
153
+ const waitMs = Number.isFinite(args.waitMs) ? Math.max(0, Math.min(120_000, Math.round(args.waitMs))) : 4000;
154
+ const res = spawnSync(browser, [
155
+ '--headless=new',
156
+ '--disable-gpu',
157
+ '--hide-scrollbars',
158
+ '--mute-audio',
159
+ '--no-first-run',
160
+ '--no-default-browser-check',
161
+ '--disable-background-networking',
162
+ '--disable-sync',
163
+ '--disable-extensions',
164
+ '--metrics-recording-only',
165
+ '--force-device-scale-factor=1',
166
+ `--window-size=${args.width},${args.height}`,
167
+ `--user-data-dir=${tmpProfile}`,
168
+ `--virtual-time-budget=${waitMs || 0}`,
169
+ `--screenshot=${outPath}`,
170
+ url,
171
+ ], { encoding: 'utf8' });
172
+ if (res.status !== 0 || !existsSync(outPath)) {
173
+ const detail = (res.stderr || res.stdout || '').trim();
174
+ throw new Error(`Screenshot failed (exit ${res.status ?? 'unknown'}): ${detail || 'no output'}`);
175
+ }
176
+ return {
177
+ screenshotPath: outPath,
178
+ width: args.width,
179
+ height: args.height,
180
+ engine: 'chrome-headless',
181
+ binary: browser,
182
+ };
183
+ }
184
+ finally {
185
+ const { rm } = await import('node:fs/promises');
186
+ await rm(tmpProfile, { recursive: true, force: true }).catch(() => undefined);
187
+ }
188
+ }
189
+ function isTtyInteractive() {
190
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
191
+ }
192
+ function looksExpired(iso) {
193
+ if (!iso)
194
+ return false;
195
+ const ms = Date.parse(iso);
196
+ if (!Number.isFinite(ms))
197
+ return false;
198
+ // Consider it expired if it will expire in the next minute.
199
+ return ms <= Date.now() + 60_000;
200
+ }
201
+ function hasAuth(cfg) {
202
+ const envToken = process.env.DESIGN_MCP_SESSION_TOKEN?.trim();
203
+ if (envToken)
204
+ return true;
205
+ if (!cfg?.sessionToken?.trim())
206
+ return false;
207
+ if (looksExpired(cfg.sessionExpiresAt))
208
+ return false;
209
+ return true;
210
+ }
211
+ async function readStdinText() {
212
+ if (process.stdin.isTTY)
213
+ return '';
214
+ const chunks = [];
215
+ for await (const chunk of process.stdin) {
216
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
217
+ }
218
+ return Buffer.concat(chunks).toString('utf8');
219
+ }
220
+ function parseJsonOrThrow(raw) {
221
+ const trimmed = raw.trim();
222
+ if (!trimmed)
223
+ return null;
224
+ try {
225
+ return JSON.parse(trimmed);
226
+ }
227
+ catch (error) {
228
+ const message = error instanceof Error ? error.message : String(error);
229
+ throw new Error(`Invalid JSON input: ${message}`);
230
+ }
231
+ }
232
+ async function readJsonArgs(remainingArgs, flags) {
233
+ const jsonFlag = resolveFlagString(flags, 'json');
234
+ const raw = (jsonFlag ?? remainingArgs.join(' ') ?? '').trim();
235
+ const fromArgs = parseJsonOrThrow(raw);
236
+ if (fromArgs && typeof fromArgs === 'object' && !Array.isArray(fromArgs)) {
237
+ return fromArgs;
238
+ }
239
+ if (raw) {
240
+ throw new Error('JSON input must be an object.');
241
+ }
242
+ const stdin = await readStdinText();
243
+ if (!stdin.trim()) {
244
+ throw new Error('Missing JSON input. Pass --json or pipe a JSON object via stdin.');
245
+ }
246
+ const parsed = parseJsonOrThrow(stdin);
247
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
248
+ throw new Error('JSON input must be an object.');
249
+ }
250
+ return parsed;
251
+ }
252
+ async function readJsonArgsOptional(remainingArgs, flags) {
253
+ const jsonFlag = resolveFlagString(flags, 'json');
254
+ const hasInline = remainingArgs.some((t) => t.trim().length > 0);
255
+ const hasStdin = !process.stdin.isTTY;
256
+ if (!jsonFlag && !hasInline && !hasStdin)
257
+ return {};
258
+ return readJsonArgs(remainingArgs, flags);
259
+ }
260
+ function pretty(value) {
261
+ return JSON.stringify(value, null, 2);
262
+ }
263
+ async function promptYesNo(question, defaultYes = false) {
264
+ if (!isTtyInteractive())
265
+ return defaultYes;
266
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
267
+ try {
268
+ const suffix = defaultYes ? ' (Y/n) ' : ' (y/N) ';
269
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
270
+ if (!answer)
271
+ return defaultYes;
272
+ return answer === 'y' || answer === 'yes';
273
+ }
274
+ finally {
275
+ rl.close();
276
+ }
277
+ }
278
+ function pathHasEntry(entry) {
279
+ const list = (process.env.PATH ?? '')
280
+ .split(path.delimiter)
281
+ .map((p) => p.trim())
282
+ .filter(Boolean);
283
+ return list.includes(entry);
284
+ }
285
+ async function backupFileIfExists(filePath) {
286
+ try {
287
+ await readFile(filePath);
288
+ }
289
+ catch {
290
+ return;
291
+ }
292
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
293
+ await copyFile(filePath, `${filePath}.bak-${ts}`);
294
+ }
295
+ function renderPathExportSnippet(shellKind, entry) {
296
+ // Prefer $HOME to keep the file portable.
297
+ const home = os.homedir();
298
+ const value = entry.startsWith(home + path.sep)
299
+ ? `$HOME${entry.slice(home.length)}`
300
+ : entry;
301
+ return `\n# Added by Every Design\nexport PATH="${value}:$PATH"\n`;
302
+ }
303
+ function renderFishPathSnippet(entry) {
304
+ const home = os.homedir();
305
+ const value = entry.startsWith(home + path.sep)
306
+ ? `$HOME${entry.slice(home.length)}`
307
+ : entry;
308
+ return `\n# Added by Every Design\nset -gx PATH ${value} $PATH\n`;
309
+ }
310
+ async function ensurePathEntryOnSystem(args) {
311
+ if (process.platform === 'win32') {
312
+ return { changed: false, note: 'Skipped PATH update on Windows (add the launcher directory to PATH manually).' };
313
+ }
314
+ if (pathHasEntry(args.entry)) {
315
+ return { changed: false, note: `PATH already includes ${args.entry}` };
316
+ }
317
+ const shell = (process.env.SHELL ?? '').toLowerCase();
318
+ const home = os.homedir();
319
+ // Choose one or more files to edit (best-effort).
320
+ // We update both profile + rc files for bash/zsh so login and non-login shells
321
+ // both pick up the change.
322
+ const targets = [];
323
+ if (shell.includes('fish')) {
324
+ const xdgConfig = process.env.XDG_CONFIG_HOME?.trim() || path.join(home, '.config');
325
+ targets.push({ filePath: path.join(xdgConfig, 'fish', 'config.fish'), kind: 'fish' });
326
+ }
327
+ else if (shell.includes('zsh')) {
328
+ targets.push({ filePath: path.join(home, '.zshrc'), kind: 'zsh' });
329
+ targets.push({ filePath: path.join(home, '.zprofile'), kind: 'zsh' });
330
+ }
331
+ else {
332
+ // Default to bash/sh style.
333
+ targets.push({ filePath: path.join(home, '.bashrc'), kind: 'bash' });
334
+ targets.push({ filePath: path.join(home, '.bash_profile'), kind: 'bash' });
335
+ targets.push({ filePath: path.join(home, '.profile'), kind: 'bash' });
336
+ }
337
+ const labelTargets = targets.map((t) => t.filePath).join(', ');
338
+ if (!args.yes && isTtyInteractive()) {
339
+ const ok = await promptYesNo(`Your PATH does not include ${args.entry}. Add it via: ${labelTargets}?`, true);
340
+ if (!ok) {
341
+ return { changed: false, note: `Skipped PATH update (you can add ${args.entry} manually).` };
342
+ }
343
+ }
344
+ if (args.dryRun) {
345
+ return { changed: false, note: `[dry-run] Would add ${args.entry} to PATH via ${labelTargets}` };
346
+ }
347
+ const updated = [];
348
+ const skipped = [];
349
+ for (const target of targets) {
350
+ await mkdir(path.dirname(target.filePath), { recursive: true }).catch(() => undefined);
351
+ const before = await readFile(target.filePath, 'utf8').catch(() => '');
352
+ if (before.includes(args.entry) || before.includes('Added by Every Design')) {
353
+ skipped.push(target.filePath);
354
+ continue;
355
+ }
356
+ await backupFileIfExists(target.filePath);
357
+ const snippet = target.kind === 'fish' ? renderFishPathSnippet(args.entry) : renderPathExportSnippet(target.kind, args.entry);
358
+ await writeFile(target.filePath, `${before.replace(/\s*$/, '')}${snippet}`, 'utf8');
359
+ updated.push(target.filePath);
360
+ }
361
+ if (updated.length === 0) {
362
+ return {
363
+ changed: false,
364
+ note: skipped.length
365
+ ? `PATH entry already present in: ${skipped.join(', ')}`
366
+ : `Skipped PATH update (no writable shell config files found).`,
367
+ };
368
+ }
369
+ return {
370
+ changed: true,
371
+ note: `Added ${args.entry} to PATH in: ${updated.join(', ')} (restart your terminal).`,
372
+ };
373
+ }
374
+ async function runAuthLoginFlow(params) {
375
+ const { configPath, designOrigin, loginOrigin, flags } = params;
376
+ const openBrowser = !flags['no-open'];
377
+ const result = await runApprovalLinkLoginFlow({
378
+ loginOrigin,
379
+ openBrowser,
380
+ });
381
+ const client = new DesignAppClient({
382
+ designOrigin,
383
+ sessionToken: result.sessionToken,
384
+ });
385
+ try {
386
+ console.error('Verifying session…');
387
+ await client.getMe();
388
+ }
389
+ catch (error) {
390
+ const message = error instanceof Error ? error.message : String(error);
391
+ throw new Error(`Login completed but the Design API rejected the session. ` +
392
+ `This usually means the login service did not issue a valid Better Auth session cookie. ` +
393
+ `Error: ${message}`);
394
+ }
395
+ let activeAccountSlug;
396
+ const preferredFromCli = resolveFlagString(flags, 'account')?.trim();
397
+ const preferredFromEnv = process.env.DESIGN_MCP_ACCOUNT_SLUG?.trim();
398
+ const preferred = preferredFromCli || preferredFromEnv || undefined;
399
+ if (!preferred && result.organizationSlug) {
400
+ // New path: company selection happens on the approval page.
401
+ activeAccountSlug = result.organizationSlug;
402
+ console.error(`Selected company: ${activeAccountSlug}`);
403
+ }
404
+ else {
405
+ // Legacy fallback: resolve + optionally prompt locally.
406
+ try {
407
+ console.error('Fetching your companies…');
408
+ const accounts = await client.listAccounts();
409
+ const isInteractive = isTtyInteractive();
410
+ const rl = isInteractive ? createInterface({ input: process.stdin, output: process.stderr }) : null;
411
+ const selected = await pickAccountSlug(accounts, {
412
+ preferredSlug: preferred,
413
+ isInteractive,
414
+ prompt: async (message) => {
415
+ if (!rl)
416
+ return '';
417
+ return rl.question(message);
418
+ },
419
+ log: (message) => console.error(message),
420
+ });
421
+ if (rl)
422
+ rl.close();
423
+ if (selected) {
424
+ // If /api/accounts already set a default active cookie, switch only when needed.
425
+ if (client.activeAccountSlug && client.activeAccountSlug !== selected) {
426
+ await client.switchAccount(selected);
427
+ }
428
+ activeAccountSlug = selected;
429
+ }
430
+ else {
431
+ activeAccountSlug = client.activeAccountSlug;
432
+ }
433
+ }
434
+ catch (error) {
435
+ const message = error instanceof Error ? error.message : String(error);
436
+ console.error(`[auth login] Warning: failed to resolve active account: ${message}`);
437
+ }
438
+ }
439
+ const nextConfig = {
440
+ version: 1,
441
+ loginOrigin,
442
+ designOrigin,
443
+ sessionToken: client.sessionToken || result.sessionToken,
444
+ sessionExpiresAt: result.sessionExpiresAt,
445
+ userId: result.userId,
446
+ activeAccountSlug,
447
+ };
448
+ await writeConfig(nextConfig, configPath);
449
+ console.error(`Saved auth to ${configPath}`);
450
+ if (activeAccountSlug)
451
+ console.error(`Active account: ${activeAccountSlug}`);
452
+ return nextConfig;
453
+ }
454
+ function recommendedMode(client, skillsEnabled) {
455
+ if (!client.installed)
456
+ return 'disabled';
457
+ if (client.supportsSkill && skillsEnabled)
458
+ return 'skill';
459
+ return 'mcp';
460
+ }
461
+ async function promptInstallPlan(clients, skillsEnabled) {
462
+ const installed = clients.filter((c) => c.installed);
463
+ const missing = clients.filter((c) => !c.installed);
464
+ console.error('Detected clients:');
465
+ installed.forEach((c, idx) => {
466
+ const mode = recommendedMode(c, skillsEnabled);
467
+ const rec = mode === 'skill' ? 'Skill (recommended)' : mode === 'mcp' ? 'MCP (recommended)' : 'Disabled';
468
+ const extra = c.details.length ? ` — ${c.details.join('; ')}` : '';
469
+ console.error(` ${idx + 1}) ${c.label}: ${rec}${extra}`);
470
+ });
471
+ if (missing.length) {
472
+ console.error('Not detected:');
473
+ missing.forEach((c) => console.error(` - ${c.label}`));
474
+ }
475
+ const useRecommended = await promptYesNo('Use recommended defaults for detected clients?', true);
476
+ const plan = Object.create(null);
477
+ for (const c of clients) {
478
+ plan[c.client] = useRecommended ? recommendedMode(c, skillsEnabled) : 'disabled';
479
+ }
480
+ if (useRecommended)
481
+ return plan;
482
+ if (!isTtyInteractive())
483
+ return plan;
484
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
485
+ try {
486
+ for (const c of installed) {
487
+ const def = recommendedMode(c, skillsEnabled);
488
+ const prompt = (() => {
489
+ if (c.supportsSkill && skillsEnabled) {
490
+ return `${c.label} [s=skill (recommended) | m=mcp | d=disabled] (default ${def}): `;
491
+ }
492
+ return `${c.label} [m=mcp (recommended) | d=disabled] (default ${def}): `;
493
+ })();
494
+ const answer = (await rl.question(prompt)).trim().toLowerCase();
495
+ if (!answer) {
496
+ plan[c.client] = def;
497
+ continue;
498
+ }
499
+ if (answer === 'd' || answer === 'off' || answer === 'disabled') {
500
+ plan[c.client] = 'disabled';
501
+ continue;
502
+ }
503
+ if (answer === 'm' || answer === 'mcp') {
504
+ plan[c.client] = 'mcp';
505
+ continue;
506
+ }
507
+ if (answer === 's' || answer === 'skill') {
508
+ plan[c.client] = c.supportsSkill && skillsEnabled ? 'skill' : 'mcp';
509
+ continue;
510
+ }
511
+ console.error(`Unrecognized choice '${answer}', using default (${def}).`);
512
+ plan[c.client] = def;
513
+ }
514
+ }
515
+ finally {
516
+ rl.close();
517
+ }
518
+ return plan;
519
+ }
69
520
  function resolveFlagString(flags, name) {
70
521
  const value = flags[name];
71
522
  if (typeof value === 'string')
@@ -94,14 +545,91 @@ async function main() {
94
545
  const loginOrigin = resolveLoginOrigin(flags);
95
546
  const verb = command[0] ?? '';
96
547
  const sub = command[1] ?? '';
548
+ if (verb === 'screenshot') {
549
+ const url = String(command[1] ?? '').trim();
550
+ const width = Number.parseInt(String(resolveFlagString(flags, 'width') ?? '1696'), 10);
551
+ const height = Number.parseInt(String(resolveFlagString(flags, 'height') ?? '2528'), 10);
552
+ const waitMs = Number.parseInt(String(resolveFlagString(flags, 'wait-ms') ?? '4000'), 10);
553
+ const out = resolveFlagString(flags, 'out')?.trim();
554
+ const outPath = out
555
+ ? out
556
+ : path.join(process.cwd(), `every-design-screenshot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`);
557
+ try {
558
+ const result = await runScreenshotCommand({
559
+ url,
560
+ outPath,
561
+ width: Number.isFinite(width) && width > 0 ? width : 1696,
562
+ height: Number.isFinite(height) && height > 0 ? height : 2528,
563
+ waitMs: Number.isFinite(waitMs) ? waitMs : 4000,
564
+ });
565
+ console.log(pretty(result));
566
+ process.exit(0);
567
+ }
568
+ catch (error) {
569
+ const message = error instanceof Error ? error.message : String(error);
570
+ console.error(message);
571
+ process.exit(1);
572
+ }
573
+ }
97
574
  if (verb === 'install') {
98
- const clientList = resolveFlagString(flags, 'client')?.trim() ||
99
- resolveFlagString(flags, 'clients')?.trim() ||
100
- 'auto';
101
- const requested = clientList
102
- .split(',')
103
- .map((s) => s.trim())
104
- .filter(Boolean);
575
+ const yes = Boolean(flags.yes);
576
+ const dryRun = Boolean(flags['dry-run']);
577
+ const force = Boolean(flags.force);
578
+ const skillsEnabled = !flags['no-skills'];
579
+ const updatePath = !flags['no-path'];
580
+ const serverName = (resolveFlagString(flags, 'name') ?? 'every-design').trim();
581
+ const launcherFlag = resolveFlagString(flags, 'launcher')?.trim().toLowerCase();
582
+ const launcherFromFlag = launcherFlag === 'npx' ? 'npx' : launcherFlag === 'local' ? 'local' : null;
583
+ if (launcherFlag && !launcherFromFlag) {
584
+ console.error(`Invalid --launcher value: ${launcherFlag}. Expected npx|local.`);
585
+ process.exit(1);
586
+ }
587
+ console.error('Every Design setup');
588
+ console.error('');
589
+ console.error('Step 1/3: Authenticate');
590
+ const existingAuth = await readConfig(configPath);
591
+ let authedCfg = existingAuth;
592
+ if (!hasAuth(existingAuth)) {
593
+ if (dryRun) {
594
+ console.error(`Dry run: no auth found at ${configPath}. Install would run auth login first.`);
595
+ }
596
+ else if (!isTtyInteractive()) {
597
+ console.error(`No auth found at ${configPath}.`);
598
+ console.error('Run `every-design auth login` (or `npx -y @just-every/design@latest auth login`) first, then re-run install.');
599
+ process.exit(1);
600
+ }
601
+ if (!dryRun) {
602
+ const proceed = yes ? true : await promptYesNo('No auth found. Login now?', true);
603
+ if (!proceed) {
604
+ console.error('Aborted.');
605
+ process.exit(1);
606
+ }
607
+ authedCfg = await runAuthLoginFlow({ configPath, designOrigin, loginOrigin, flags });
608
+ }
609
+ }
610
+ else {
611
+ console.error('Auth already configured.');
612
+ if (looksExpired(existingAuth?.sessionExpiresAt)) {
613
+ console.error('Warning: saved session looks expired; you may need to re-run auth login.');
614
+ }
615
+ }
616
+ console.error('');
617
+ console.error('Step 2/3: Choose launcher');
618
+ const launcher = launcherFromFlag ?? 'local';
619
+ if (!launcherFromFlag) {
620
+ console.error('Using launcher: local (default).');
621
+ console.error('Tip: pass --launcher npx to skip local install and always run via npx.');
622
+ }
623
+ if (launcher === 'local' && updatePath) {
624
+ const localBin = path.join(os.homedir(), '.local', 'bin');
625
+ const ensured = await ensurePathEntryOnSystem({ entry: localBin, dryRun, yes });
626
+ if (ensured.note)
627
+ console.error(ensured.note);
628
+ }
629
+ console.error('');
630
+ console.error('Step 3/3: Configure clients');
631
+ const clientList = resolveFlagString(flags, 'client')?.trim() || resolveFlagString(flags, 'clients')?.trim();
632
+ const detections = detectClients();
105
633
  const allClients = [
106
634
  'code',
107
635
  'codex',
@@ -111,132 +639,331 @@ async function main() {
111
639
  'gemini',
112
640
  'qwen',
113
641
  ];
114
- const selected = (() => {
115
- if (requested.includes('all'))
116
- return allClients;
117
- if (requested.includes('auto'))
118
- return detectDefaultClients();
119
- return requested.filter((c) => allClients.includes(c));
120
- })();
121
- const serverName = (resolveFlagString(flags, 'name') ?? 'every-design').trim();
122
- const installSkills = !flags['no-skills'];
123
- const dryRun = Boolean(flags['dry-run']);
124
- const force = Boolean(flags.force);
125
- const yes = Boolean(flags.yes);
126
- const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
127
- const shouldProceed = yes
128
- ? true
129
- : await (async () => {
130
- if (!isInteractive)
131
- return true;
132
- const rl = createInterface({ input: process.stdin, output: process.stderr });
133
- try {
134
- console.error('This will update MCP client config files in your home directory.');
135
- console.error(`Clients: ${selected.length ? selected.join(', ') : '(none detected)'}`);
136
- console.error(`Server name: ${serverName}`);
137
- const answer = (await rl.question('Proceed? (y/N) ')).trim().toLowerCase();
138
- return answer === 'y' || answer === 'yes';
139
- }
140
- finally {
141
- rl.close();
142
- }
642
+ let plan;
643
+ if (clientList) {
644
+ const requested = clientList
645
+ .split(',')
646
+ .map((s) => s.trim())
647
+ .filter(Boolean);
648
+ const selected = (() => {
649
+ if (requested.includes('all'))
650
+ return allClients;
651
+ if (requested.includes('auto'))
652
+ return detectDefaultClients();
653
+ return requested.filter((c) => allClients.includes(c));
143
654
  })();
144
- if (!shouldProceed) {
145
- console.error('Aborted.');
655
+ plan = Object.create(null);
656
+ for (const c of allClients) {
657
+ plan[c] = selected.includes(c)
658
+ ? (c === 'code' || c === 'codex') && skillsEnabled
659
+ ? 'skill'
660
+ : 'mcp'
661
+ : 'disabled';
662
+ }
663
+ }
664
+ else if (yes) {
665
+ plan = Object.create(null);
666
+ for (const c of detections) {
667
+ plan[c.client] = recommendedMode(c, skillsEnabled);
668
+ }
669
+ }
670
+ else {
671
+ plan = await promptInstallPlan(detections, skillsEnabled);
672
+ }
673
+ const mcpClients = [];
674
+ const skillClients = [];
675
+ for (const c of allClients) {
676
+ const mode = plan[c] ?? 'disabled';
677
+ if (mode === 'disabled')
678
+ continue;
679
+ if (mode === 'skill') {
680
+ if (c === 'code' || c === 'codex' || c === 'claude-code') {
681
+ skillClients.push(c);
682
+ continue;
683
+ }
684
+ mcpClients.push(c);
685
+ continue;
686
+ }
687
+ mcpClients.push(c);
688
+ }
689
+ if (mcpClients.length === 0 && skillClients.length === 0) {
690
+ console.error('No clients selected. Nothing to do.');
146
691
  process.exit(1);
147
692
  }
148
- if (selected.length === 0) {
149
- console.error('No clients detected. Re-run with `--client all` to force installation.');
693
+ const confirm = yes ? true : await promptYesNo('Apply these changes?', true);
694
+ if (!confirm) {
695
+ console.error('Aborted.');
150
696
  process.exit(1);
151
697
  }
152
- const result = await runInstall({
153
- clients: selected,
154
- serverName,
155
- packageName: '@just-every/design',
156
- installSkills,
157
- dryRun,
158
- force,
159
- });
160
- if (result.changed.length) {
698
+ const results = [];
699
+ if (skillClients.length) {
700
+ results.push({
701
+ title: 'Skill + MCP (recommended)',
702
+ result: await runInstall({
703
+ clients: skillClients,
704
+ serverName,
705
+ packageName: '@just-every/design',
706
+ installSkills: true,
707
+ dryRun,
708
+ force,
709
+ launcher,
710
+ }),
711
+ });
712
+ }
713
+ if (mcpClients.length) {
714
+ results.push({
715
+ title: 'MCP only',
716
+ result: await runInstall({
717
+ clients: mcpClients,
718
+ serverName,
719
+ packageName: '@just-every/design',
720
+ installSkills: false,
721
+ dryRun,
722
+ force,
723
+ launcher,
724
+ }),
725
+ });
726
+ }
727
+ const changed = results.flatMap((r) => r.result.changed);
728
+ const skipped = results.flatMap((r) => r.result.skipped);
729
+ const notes = results.flatMap((r) => r.result.notes);
730
+ console.error('');
731
+ console.error('Summary');
732
+ console.error(`- Auth: ${hasAuth(authedCfg) ? 'configured' : 'missing'}`);
733
+ console.error(`- Server name: ${serverName}`);
734
+ console.error(`- Launcher: ${launcher}`);
735
+ console.error(`- Clients configured: ${[...new Set([...skillClients, ...mcpClients])].join(', ')}`);
736
+ if (dryRun) {
737
+ console.error('- Dry run: no files were modified');
738
+ }
739
+ if (changed.length) {
161
740
  console.error('Updated:');
162
- for (const p of result.changed)
741
+ for (const p of changed)
163
742
  console.error(`- ${p}`);
164
743
  }
165
- if (result.skipped.length) {
744
+ if (skipped.length) {
166
745
  console.error('Skipped:');
167
- for (const p of result.skipped)
746
+ for (const p of skipped)
168
747
  console.error(`- ${p}`);
169
748
  }
170
- if (result.notes.length) {
749
+ if (notes.length) {
171
750
  console.error('Notes:');
172
- for (const n of result.notes)
751
+ for (const n of notes)
173
752
  console.error(`- ${n}`);
174
753
  }
175
- console.error('Next: run `npx @just-every/design auth login` once to authenticate.');
754
+ console.error('');
755
+ console.error('Next steps: restart your client(s) so they pick up the new MCP config.');
176
756
  process.exit(0);
177
757
  }
178
- if (verb === 'auth' && sub === 'login') {
179
- const openBrowser = !flags['no-open'];
180
- const result = await runApprovalLinkLoginFlow({
181
- loginOrigin,
182
- openBrowser,
183
- });
758
+ // CLI helpers (non-MCP). These are mostly useful for Skills/playbooks and scripting.
759
+ if (verb === 'create' || verb === 'critique' || verb === 'wait' || verb === 'get' || verb === 'list' || verb === 'events' || verb === 'artifacts') {
760
+ const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
761
+ const sessionToken = (process.env.DESIGN_MCP_SESSION_TOKEN ?? cfg.sessionToken ?? '').trim();
762
+ if (!sessionToken) {
763
+ console.error(NOT_AUTHENTICATED_HELP);
764
+ process.exit(1);
765
+ }
184
766
  const client = new DesignAppClient({
185
- designOrigin,
186
- sessionToken: result.sessionToken,
767
+ designOrigin: cfg.designOrigin,
768
+ sessionToken,
769
+ activeAccountSlug: cfg.activeAccountSlug,
187
770
  });
188
- let activeAccountSlug;
189
771
  try {
190
- const accounts = await client.listAccounts();
191
- const preferred = resolveFlagString(flags, 'account')?.trim() ||
192
- process.env.DESIGN_MCP_ACCOUNT_SLUG?.trim() ||
193
- undefined;
194
- const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
195
- const rl = isInteractive
196
- ? createInterface({ input: process.stdin, output: process.stderr })
197
- : null;
198
- const selected = await pickAccountSlug(accounts, {
199
- preferredSlug: preferred,
200
- isInteractive,
201
- prompt: async (message) => {
202
- if (!rl)
203
- return '';
204
- return rl.question(message);
205
- },
206
- log: (message) => console.error(message),
207
- });
208
- if (rl) {
209
- rl.close();
772
+ if (verb === 'create') {
773
+ const input = await readJsonArgs(command.slice(1), flags);
774
+ const { prompt, config } = await buildCreateRunRequest(client, input);
775
+ const created = await client.createRun({ prompt, config });
776
+ console.log(pretty(created));
777
+ process.exit(0);
210
778
  }
211
- if (selected) {
212
- // If /api/accounts already set a default active cookie, switch only when needed.
213
- if (client.activeAccountSlug && client.activeAccountSlug !== selected) {
214
- await client.switchAccount(selected);
779
+ if (verb === 'critique') {
780
+ const input = await readJsonArgs(command.slice(1), flags);
781
+ const originalPrompt = String(input.originalPrompt ?? '').trim();
782
+ if (!originalPrompt)
783
+ throw new Error('Missing required argument: originalPrompt');
784
+ const concerns = typeof input.concerns === 'string' ? input.concerns.trim() : '';
785
+ const target = Array.isArray(input.target) ? input.target : [];
786
+ const render = Array.isArray(input.render) ? input.render : [];
787
+ if (target.length === 0)
788
+ throw new Error('Missing required argument: target (array of images)');
789
+ if (render.length === 0)
790
+ throw new Error('Missing required argument: render (array of images)');
791
+ const viewportInput = input.viewport && typeof input.viewport === 'object' ? input.viewport : null;
792
+ const viewport = {
793
+ width: Number(viewportInput?.width) || 1696,
794
+ height: Number(viewportInput?.height) || 2528,
795
+ };
796
+ const targetRefs = await client.uploadSourceImages(target);
797
+ const renderRefs = await client.uploadSourceImages(render);
798
+ const created = await client.createRun({
799
+ prompt: originalPrompt,
800
+ config: {
801
+ output: { designKind: 'interface', render: { viewport } },
802
+ critique: {
803
+ originalPrompt,
804
+ concerns,
805
+ target: targetRefs,
806
+ render: renderRefs,
807
+ },
808
+ },
809
+ });
810
+ const runId = String(created?.run?.id ?? '').trim();
811
+ if (!runId)
812
+ throw new Error('Critique run created but missing run id');
813
+ const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
814
+ const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
815
+ await waitForRun(client, runId, {
816
+ timeoutSeconds,
817
+ intervalSeconds,
818
+ log: (line) => console.error(line),
819
+ });
820
+ const artifacts = await client.listArtifacts(runId);
821
+ const list = Array.isArray(artifacts.artifacts) ? artifacts.artifacts : [];
822
+ const critiqueArtifact = list.find((a) => a.artifactType === 'critique') ?? null;
823
+ const generatedAssets = list.filter((a) => a.artifactType === 'critique-asset');
824
+ if (!critiqueArtifact) {
825
+ console.log(pretty({
826
+ runId,
827
+ status: 'completed',
828
+ critique: null,
829
+ generatedAssets,
830
+ error: 'Missing critique artifact',
831
+ }));
832
+ process.exit(0);
215
833
  }
216
- activeAccountSlug = selected;
834
+ const downloaded = await client.downloadArtifactToCache(runId, critiqueArtifact.id, { fileNameHint: 'critique.json' });
835
+ const raw = await readFile(downloaded.filePath, 'utf8');
836
+ const parsed = parseJsonOrThrow(raw);
837
+ console.log(pretty({
838
+ runId,
839
+ critique: parsed,
840
+ artifact: { id: critiqueArtifact.id, filePath: downloaded.filePath },
841
+ generatedAssets,
842
+ }));
843
+ process.exit(0);
217
844
  }
218
- else {
219
- activeAccountSlug = client.activeAccountSlug;
845
+ if (verb === 'wait') {
846
+ const input = await readJsonArgs(command.slice(1), flags);
847
+ const runId = String(input.runId ?? '').trim();
848
+ if (!runId)
849
+ throw new Error('Missing required argument: runId');
850
+ const timeoutSeconds = typeof input.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
851
+ const intervalSeconds = typeof input.intervalSeconds === 'number' ? input.intervalSeconds : 5;
852
+ const run = await waitForRun(client, runId, {
853
+ timeoutSeconds,
854
+ intervalSeconds,
855
+ log: (line) => console.error(line),
856
+ });
857
+ console.log(pretty(run));
858
+ process.exit(0);
859
+ }
860
+ if (verb === 'get') {
861
+ const input = await readJsonArgs(command.slice(1), flags);
862
+ const runId = String(input.runId ?? '').trim();
863
+ if (!runId)
864
+ throw new Error('Missing required argument: runId');
865
+ const run = await client.getRun(runId);
866
+ console.log(pretty(run));
867
+ process.exit(0);
220
868
  }
869
+ if (verb === 'events') {
870
+ const input = await readJsonArgs(command.slice(1), flags);
871
+ const runId = String(input.runId ?? '').trim();
872
+ if (!runId)
873
+ throw new Error('Missing required argument: runId');
874
+ const events = await client.getRunEvents(runId);
875
+ console.log(pretty(events));
876
+ process.exit(0);
877
+ }
878
+ if (verb === 'list') {
879
+ const input = await readJsonArgsOptional(command.slice(1), flags);
880
+ const page = typeof input.page === 'number' ? input.page : undefined;
881
+ const limit = typeof input.limit === 'number' ? input.limit : undefined;
882
+ const runs = await client.listRuns({ page, limit });
883
+ console.log(pretty(runs));
884
+ process.exit(0);
885
+ }
886
+ if (verb === 'artifacts' && sub === 'list') {
887
+ const input = await readJsonArgs(command.slice(2), flags);
888
+ const runId = String(input.runId ?? '').trim();
889
+ if (!runId)
890
+ throw new Error('Missing required argument: runId');
891
+ const artifacts = await client.listArtifacts(runId);
892
+ console.log(pretty(artifacts));
893
+ process.exit(0);
894
+ }
895
+ if (verb === 'artifacts' && sub === 'download') {
896
+ const input = await readJsonArgs(command.slice(2), flags);
897
+ const runId = String(input.runId ?? '').trim();
898
+ const artifactId = String(input.artifactId ?? '').trim();
899
+ if (!runId)
900
+ throw new Error('Missing required argument: runId');
901
+ if (!artifactId)
902
+ throw new Error('Missing required argument: artifactId');
903
+ const downloaded = await client.downloadArtifactToCache(runId, artifactId);
904
+ console.log(pretty(downloaded));
905
+ process.exit(0);
906
+ }
907
+ if (verb === 'artifacts') {
908
+ throw new Error('Usage: artifacts list|download');
909
+ }
910
+ // Should be unreachable due to the verb guard.
911
+ throw new Error(`Unknown command: ${verb}`);
221
912
  }
222
913
  catch (error) {
223
914
  const message = error instanceof Error ? error.message : String(error);
224
- console.error(`[auth login] Warning: failed to resolve active account: ${message}`);
915
+ console.error(message);
916
+ process.exit(1);
225
917
  }
226
- const nextConfig = {
227
- version: 1,
228
- loginOrigin,
229
- designOrigin,
230
- sessionToken: client.sessionToken || result.sessionToken,
231
- sessionExpiresAt: result.sessionExpiresAt,
232
- userId: result.userId,
233
- activeAccountSlug,
234
- };
235
- await writeConfig(nextConfig, configPath);
236
- console.error(`Saved config to ${configPath}`);
237
- if (activeAccountSlug) {
238
- console.error(`Active account: ${activeAccountSlug}`);
918
+ }
919
+ if (verb === 'remove') {
920
+ const yes = Boolean(flags.yes);
921
+ const dryRun = Boolean(flags['dry-run']);
922
+ const force = Boolean(flags.force);
923
+ const detected = detectClients();
924
+ const installed = detected.filter((c) => c.installed);
925
+ console.error('This will remove Every Design (@just-every/design) from detected clients.');
926
+ if (installed.length) {
927
+ console.error('Detected:');
928
+ for (const c of installed) {
929
+ console.error(`- ${c.label}`);
930
+ }
931
+ }
932
+ else {
933
+ console.error('No clients detected.');
239
934
  }
935
+ const proceed = yes ? true : await promptYesNo('Continue?', false);
936
+ if (!proceed) {
937
+ console.error('Aborted.');
938
+ process.exit(1);
939
+ }
940
+ const result = await runRemove({
941
+ packageName: '@just-every/design',
942
+ dryRun,
943
+ force,
944
+ });
945
+ console.error('');
946
+ console.error('Summary');
947
+ if (result.changed.length) {
948
+ console.error('Updated:');
949
+ for (const p of result.changed)
950
+ console.error(`- ${p}`);
951
+ }
952
+ if (result.skipped.length) {
953
+ console.error('Skipped:');
954
+ for (const p of result.skipped)
955
+ console.error(`- ${p}`);
956
+ }
957
+ if (result.notes.length) {
958
+ console.error('Notes:');
959
+ for (const n of result.notes)
960
+ console.error(`- ${n}`);
961
+ }
962
+ console.error('Restart your client(s) to unload the MCP server config.');
963
+ process.exit(0);
964
+ }
965
+ if (verb === 'auth' && sub === 'login') {
966
+ await runAuthLoginFlow({ configPath, designOrigin, loginOrigin, flags });
240
967
  process.exit(0);
241
968
  }
242
969
  if (verb === 'auth' && sub === 'logout') {
@@ -248,7 +975,7 @@ async function main() {
248
975
  const cfg = await loadMergedConfig(configPath, { designOrigin, loginOrigin });
249
976
  const sessionToken = cfg.sessionToken ?? process.env.DESIGN_MCP_SESSION_TOKEN ?? '';
250
977
  if (!sessionToken) {
251
- console.error('Not authenticated. Run `npx @just-every/design auth login`.');
978
+ console.error(NOT_AUTHENTICATED_HELP);
252
979
  process.exit(1);
253
980
  }
254
981
  const client = new DesignAppClient({
@@ -281,7 +1008,7 @@ async function main() {
281
1008
  }
282
1009
  async function loadMergedConfig(configPath, defaults) {
283
1010
  const existing = await readConfig(configPath);
284
- return {
1011
+ const merged = {
285
1012
  version: 1,
286
1013
  loginOrigin: existing?.loginOrigin ?? defaults.loginOrigin,
287
1014
  designOrigin: existing?.designOrigin ?? defaults.designOrigin,
@@ -290,6 +1017,38 @@ async function loadMergedConfig(configPath, defaults) {
290
1017
  userId: existing?.userId,
291
1018
  activeAccountSlug: existing?.activeAccountSlug,
292
1019
  };
1020
+ // Keep sessions alive for CLI usage by hitting the Better Auth session endpoint.
1021
+ // This will refresh/extend the session and may rotate the cookie token.
1022
+ const envOverride = process.env.DESIGN_MCP_SESSION_TOKEN?.trim();
1023
+ if (!envOverride && merged.sessionToken) {
1024
+ const refreshWindowMs = 14 * 24 * 60 * 60 * 1000; // 14 days
1025
+ const expiresAtMs = merged.sessionExpiresAt ? Date.parse(merged.sessionExpiresAt) : NaN;
1026
+ const msRemaining = Number.isFinite(expiresAtMs) ? expiresAtMs - Date.now() : NaN;
1027
+ const shouldRefresh = !Number.isFinite(msRemaining) || msRemaining <= refreshWindowMs;
1028
+ if (shouldRefresh) {
1029
+ try {
1030
+ const refreshed = await refreshLoginSession({
1031
+ loginOrigin: merged.loginOrigin,
1032
+ sessionToken: merged.sessionToken,
1033
+ });
1034
+ if (refreshed) {
1035
+ const nextToken = refreshed.sessionToken;
1036
+ const nextExpiresAt = refreshed.sessionExpiresAt;
1037
+ const changed = refreshed.changed || (nextExpiresAt && nextExpiresAt !== merged.sessionExpiresAt);
1038
+ if (changed) {
1039
+ merged.sessionToken = nextToken;
1040
+ if (nextExpiresAt)
1041
+ merged.sessionExpiresAt = nextExpiresAt;
1042
+ await writeConfig(merged, configPath);
1043
+ }
1044
+ }
1045
+ }
1046
+ catch {
1047
+ // Best-effort only: do not block CLI usage if login is temporarily unavailable.
1048
+ }
1049
+ }
1050
+ }
1051
+ return merged;
293
1052
  }
294
1053
  main();
295
1054
  //# sourceMappingURL=cli.js.map