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