@superdesign/cli 0.1.0

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/index.js ADDED
@@ -0,0 +1,1107 @@
1
+ import { config as external_dotenv_config } from "dotenv";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join, resolve as external_path_resolve } from "path";
4
+ import { Command } from "commander";
5
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
6
+ import { homedir, hostname, platform, release } from "os";
7
+ import axios from "axios";
8
+ import ora from "ora";
9
+ const CONFIG_DIR_NAME = '.superdesign';
10
+ const CONFIG_FILE_NAME = 'config.json';
11
+ const EXIT_CODES = {
12
+ SUCCESS: 0,
13
+ GENERAL_ERROR: 1,
14
+ AUTH_REQUIRED: 2,
15
+ AUTH_FAILED: 3,
16
+ API_ERROR: 4,
17
+ VALIDATION_ERROR: 5,
18
+ TIMEOUT: 6
19
+ };
20
+ const SKILLS_DIR = '.claude/skills/superdesign';
21
+ const SKILL_FILE_NAME = 'SKILL.md';
22
+ function getConfigDir() {
23
+ return join(homedir(), CONFIG_DIR_NAME);
24
+ }
25
+ function getConfigPath() {
26
+ return join(getConfigDir(), CONFIG_FILE_NAME);
27
+ }
28
+ function ensureConfigDir() {
29
+ const configDir = getConfigDir();
30
+ if (!existsSync(configDir)) mkdirSync(configDir, {
31
+ recursive: true,
32
+ mode: 448
33
+ });
34
+ }
35
+ function loadConfig() {
36
+ const configPath = getConfigPath();
37
+ if (!existsSync(configPath)) return {};
38
+ try {
39
+ const content = readFileSync(configPath, 'utf-8');
40
+ return JSON.parse(content);
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+ function saveConfig(config) {
46
+ ensureConfigDir();
47
+ const configPath = getConfigPath();
48
+ writeFileSync(configPath, JSON.stringify(config, null, 2), {
49
+ encoding: 'utf-8',
50
+ mode: 384
51
+ });
52
+ }
53
+ function updateConfig(updates) {
54
+ const current = loadConfig();
55
+ const updated = {
56
+ ...current,
57
+ ...updates
58
+ };
59
+ saveConfig(updated);
60
+ return updated;
61
+ }
62
+ function clearConfig() {
63
+ const configPath = getConfigPath();
64
+ if (existsSync(configPath)) unlinkSync(configPath);
65
+ }
66
+ function manager_isAuthenticated() {
67
+ const config = loadConfig();
68
+ return !!config.apiKey;
69
+ }
70
+ function getApiKey() {
71
+ const config = loadConfig();
72
+ if (!config.apiKey) throw new Error('Not authenticated. Run `superdesign login` first.');
73
+ return config.apiKey;
74
+ }
75
+ function getApiUrl() {
76
+ const config = loadConfig();
77
+ return config.apiUrl || process.env.SUPERDESIGN_API_URL || 'https://api.superdesign.dev/v1';
78
+ }
79
+ class ApiClientError extends Error {
80
+ constructor(message, code, status, details){
81
+ super(message), this.code = code, this.status = status, this.details = details;
82
+ this.name = 'ApiClientError';
83
+ }
84
+ }
85
+ function createApiClient(requireAuth = true) {
86
+ const baseURL = getApiUrl();
87
+ const client = axios.create({
88
+ baseURL,
89
+ timeout: 30000,
90
+ headers: {
91
+ 'Content-Type': 'application/json'
92
+ }
93
+ });
94
+ if (requireAuth) client.interceptors.request.use((config)=>{
95
+ if (!manager_isAuthenticated()) throw new ApiClientError('Not authenticated. Run `superdesign login` first.', 'auth_required', 401);
96
+ const apiKey = getApiKey();
97
+ config.headers.Authorization = `Bearer ${apiKey}`;
98
+ return config;
99
+ });
100
+ client.interceptors.response.use((response)=>response, (error)=>{
101
+ if (error.response) {
102
+ const { status, data } = error.response;
103
+ const apiError = data?.error;
104
+ throw new ApiClientError(apiError?.message || error.message, apiError?.code || 'api_error', status, apiError?.details);
105
+ }
106
+ if ('ECONNREFUSED' === error.code) throw new ApiClientError('Could not connect to SuperDesign API', 'connection_error');
107
+ if ('ETIMEDOUT' === error.code) throw new ApiClientError('Request timed out', 'timeout');
108
+ throw new ApiClientError(error.message || 'Unknown error', 'unknown_error');
109
+ });
110
+ return client;
111
+ }
112
+ function createPublicApiClient() {
113
+ return createApiClient(false);
114
+ }
115
+ function getApiClient() {
116
+ return createApiClient(true);
117
+ }
118
+ async function createSession(data) {
119
+ const client = createPublicApiClient();
120
+ const response = await client.post('/cli-auth/sessions', data);
121
+ return response.data;
122
+ }
123
+ async function pollSession(pollToken) {
124
+ const client = createPublicApiClient();
125
+ const response = await client.get('/cli-auth/sessions/poll', {
126
+ params: {
127
+ token: pollToken
128
+ }
129
+ });
130
+ return response.data;
131
+ }
132
+ async function claimSession(pollToken) {
133
+ const client = createPublicApiClient();
134
+ const response = await client.post('/cli-auth/sessions/claim', {
135
+ pollToken
136
+ });
137
+ return response.data;
138
+ }
139
+ async function openBrowser(url) {
140
+ const open = await import("open");
141
+ await open.default(url);
142
+ }
143
+ async function poll(fn, isDone, options = {}) {
144
+ const intervalMs = options.intervalMs ?? 2000;
145
+ const timeoutMs = options.timeoutMs ?? 300000;
146
+ const startTime = Date.now();
147
+ let attempt = 0;
148
+ while(Date.now() - startTime < timeoutMs){
149
+ attempt++;
150
+ if (options.onPoll) options.onPoll(attempt);
151
+ try {
152
+ const result = await fn();
153
+ if (isDone(result)) return {
154
+ success: true,
155
+ data: result
156
+ };
157
+ } catch (err) {
158
+ const message = err instanceof Error ? err.message : 'Unknown error';
159
+ return {
160
+ success: false,
161
+ error: message
162
+ };
163
+ }
164
+ await sleep(intervalMs);
165
+ }
166
+ return {
167
+ success: false,
168
+ timedOut: true,
169
+ error: 'Polling timed out'
170
+ };
171
+ }
172
+ function sleep(ms) {
173
+ return new Promise((resolve)=>setTimeout(resolve, ms));
174
+ }
175
+ let jsonMode = false;
176
+ function setJsonMode(enabled) {
177
+ jsonMode = enabled;
178
+ }
179
+ function isJsonMode() {
180
+ return jsonMode;
181
+ }
182
+ function output(data) {
183
+ if (jsonMode) console.log(JSON.stringify(data, null, 2));
184
+ else if ('string' == typeof data) console.log(data);
185
+ else console.log(JSON.stringify(data, null, 2));
186
+ }
187
+ function success(message) {
188
+ if (!jsonMode) console.log(`✓ ${message}`);
189
+ }
190
+ function output_error(message) {
191
+ if (jsonMode) console.error(JSON.stringify({
192
+ error: message
193
+ }));
194
+ else console.error(`✗ ${message}`);
195
+ }
196
+ function info(message) {
197
+ if (!jsonMode) console.log(message);
198
+ }
199
+ let currentSpinner = null;
200
+ function startSpinner(text) {
201
+ if (isJsonMode()) return null;
202
+ currentSpinner = ora(text).start();
203
+ return currentSpinner;
204
+ }
205
+ function updateSpinner(text) {
206
+ if (currentSpinner) currentSpinner.text = text;
207
+ }
208
+ function succeedSpinner(text) {
209
+ if (currentSpinner) {
210
+ currentSpinner.succeed(text);
211
+ currentSpinner = null;
212
+ }
213
+ }
214
+ function failSpinner(text) {
215
+ if (currentSpinner) {
216
+ currentSpinner.fail(text);
217
+ currentSpinner = null;
218
+ }
219
+ }
220
+ async function runAuthFlow(options = {}) {
221
+ const { openBrowser: shouldOpenBrowser = true, showSuccessMessage = true } = options;
222
+ try {
223
+ startSpinner('Creating auth session...');
224
+ const session = await createSession({
225
+ cliVersion: "0.1.0",
226
+ os: `${platform()} ${release()}`,
227
+ hostname: hostname()
228
+ });
229
+ updateSpinner('Waiting for browser authorization...');
230
+ if (shouldOpenBrowser) try {
231
+ await openBrowser(session.authUrl);
232
+ info(`\nOpened browser to: ${session.authUrl}`);
233
+ } catch {
234
+ info(`\nPlease open this URL in your browser:\n${session.authUrl}`);
235
+ }
236
+ else info(`\nPlease open this URL in your browser:\n${session.authUrl}`);
237
+ info(`\nSession code: ${session.sessionCode}`);
238
+ info('Waiting for authorization...\n');
239
+ const pollResult = await poll(()=>pollSession(session.pollToken), (response)=>'pending' !== response.status, {
240
+ intervalMs: 2000,
241
+ timeoutMs: 600000,
242
+ onPoll: (attempt)=>{
243
+ if (attempt % 10 === 0) updateSpinner(`Waiting for authorization... (${Math.floor(2000 * attempt / 1000)}s)`);
244
+ }
245
+ });
246
+ if (pollResult.timedOut) {
247
+ failSpinner('Authorization timed out');
248
+ return {
249
+ success: false,
250
+ error: 'Session expired. Please try again.'
251
+ };
252
+ }
253
+ if (!pollResult.success || !pollResult.data) {
254
+ failSpinner('Authorization failed');
255
+ return {
256
+ success: false,
257
+ error: pollResult.error || 'Failed to get authorization status'
258
+ };
259
+ }
260
+ const authResponse = pollResult.data;
261
+ if ('expired' === authResponse.status) {
262
+ failSpinner('Session expired');
263
+ return {
264
+ success: false,
265
+ error: 'Session expired. Please try again.'
266
+ };
267
+ }
268
+ if ('approved' !== authResponse.status || !authResponse.apiKey) {
269
+ failSpinner('Authorization denied');
270
+ return {
271
+ success: false,
272
+ error: 'Authorization was denied or failed.'
273
+ };
274
+ }
275
+ await claimSession(session.pollToken);
276
+ updateConfig({
277
+ apiKey: authResponse.apiKey,
278
+ teamId: authResponse.teamId,
279
+ teamName: authResponse.teamName
280
+ });
281
+ succeedSpinner('Authenticated successfully!');
282
+ if (showSuccessMessage) success(`Logged in to team: ${authResponse.teamName}`);
283
+ return {
284
+ success: true,
285
+ teamId: authResponse.teamId,
286
+ teamName: authResponse.teamName,
287
+ apiKey: authResponse.apiKey
288
+ };
289
+ } catch (err) {
290
+ failSpinner('Login failed');
291
+ const message = err instanceof Error ? err.message : 'Unknown error';
292
+ output_error(message);
293
+ return {
294
+ success: false,
295
+ error: message
296
+ };
297
+ }
298
+ }
299
+ const LOGO = `
300
+ ███████╗██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ███████╗███████╗██╗ ██████╗ ███╗ ██╗
301
+ ██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝██╔════╝██║██╔════╝ ████╗ ██║
302
+ ███████╗██║ ██║██████╔╝█████╗ ██████╔╝██║ ██║█████╗ ███████╗██║██║ ███╗██╔██╗ ██║
303
+ ╚════██║██║ ██║██╔═══╝ ██╔══╝ ██╔══██╗██║ ██║██╔══╝ ╚════██║██║██║ ██║██║╚██╗██║
304
+ ███████║╚██████╔╝██║ ███████╗██║ ██║██████╔╝███████╗███████║██║╚██████╔╝██║ ╚████║
305
+ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝
306
+ `.trim();
307
+ const TAGLINE = 'AI designer for coding agents';
308
+ const SCRAMBLE_CHARS = '░▒▓█▄▀│┤┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌';
309
+ function ascii_animation_sleep(ms) {
310
+ return new Promise((resolve)=>setTimeout(resolve, ms));
311
+ }
312
+ function getRandomChar() {
313
+ return SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
314
+ }
315
+ function shouldSkipAnimation() {
316
+ if (process.env.CI) return true;
317
+ if (!process.stdout.isTTY) return true;
318
+ if (process.env.NO_COLOR) return true;
319
+ return false;
320
+ }
321
+ function clearFrame(lineCount) {
322
+ for(let i = 0; i < lineCount; i++){
323
+ process.stdout.write('\x1B[1A');
324
+ process.stdout.write('\x1B[2K');
325
+ }
326
+ }
327
+ async function animateLogo() {
328
+ if (shouldSkipAnimation()) {
329
+ console.log(LOGO);
330
+ console.log(TAGLINE);
331
+ console.log();
332
+ return;
333
+ }
334
+ const lines = LOGO.split('\n');
335
+ const frames = 20;
336
+ const frameDelay = 60;
337
+ const scrambledLines = lines.map((line)=>line.split('').map((char)=>' ' === char ? ' ' : getRandomChar()).join(''));
338
+ console.log(scrambledLines.join('\n'));
339
+ for(let frame = 1; frame <= frames; frame++){
340
+ await ascii_animation_sleep(frameDelay);
341
+ clearFrame(lines.length);
342
+ const revealRatio = frame / frames;
343
+ const easedRatio = 1 - Math.pow(1 - revealRatio, 3);
344
+ const renderedLines = lines.map((line)=>line.split('').map((char, i)=>{
345
+ if (' ' === char) return ' ';
346
+ const positionFactor = i / line.length;
347
+ const threshold = 1.2 * easedRatio - 0.3 * positionFactor;
348
+ if (Math.random() < threshold) return char;
349
+ return getRandomChar();
350
+ }).join(''));
351
+ console.log(renderedLines.join('\n'));
352
+ }
353
+ clearFrame(lines.length);
354
+ console.log(LOGO);
355
+ await ascii_animation_sleep(100);
356
+ console.log(TAGLINE);
357
+ console.log();
358
+ }
359
+ function createLoginCommand() {
360
+ const command = new Command('login').description('Authenticate with SuperDesign platform').option('--json', 'Output in JSON format').option('--no-browser', 'Do not open browser automatically').action(async (options)=>{
361
+ if (options.json) setJsonMode(true);
362
+ if (!isJsonMode()) await animateLogo();
363
+ const currentConfig = loadConfig();
364
+ if (currentConfig.apiKey) {
365
+ info(`Already authenticated as team: ${currentConfig.teamName || 'Unknown'}`);
366
+ info('Run `superdesign login` again to re-authenticate.');
367
+ }
368
+ const result = await runAuthFlow({
369
+ openBrowser: false !== options.noBrowser,
370
+ showSuccessMessage: !isJsonMode()
371
+ });
372
+ if (!result.success) {
373
+ output_error(result.error || 'Authentication failed');
374
+ process.exit(EXIT_CODES.AUTH_FAILED);
375
+ }
376
+ if (isJsonMode()) output({
377
+ success: true,
378
+ teamId: result.teamId,
379
+ teamName: result.teamName
380
+ });
381
+ else {
382
+ info('\nYou can now use SuperDesign CLI commands.');
383
+ info('Run `superdesign init` to install Claude Code skills.');
384
+ }
385
+ });
386
+ return command;
387
+ }
388
+ function createLogoutCommand() {
389
+ const command = new Command('logout').description('Log out and clear stored credentials').option('--json', 'Output in JSON format').action(async (options)=>{
390
+ if (options.json) setJsonMode(true);
391
+ try {
392
+ if (!manager_isAuthenticated()) {
393
+ if (isJsonMode()) output({
394
+ success: true,
395
+ message: 'Not logged in'
396
+ });
397
+ else info('Not logged in.');
398
+ return;
399
+ }
400
+ const config = loadConfig();
401
+ const teamName = config.teamName;
402
+ clearConfig();
403
+ if (isJsonMode()) output({
404
+ success: true,
405
+ message: 'Logged out successfully',
406
+ teamName
407
+ });
408
+ else {
409
+ success('Logged out successfully!');
410
+ if (teamName) info(`Disconnected from team: ${teamName}`);
411
+ info('\nRun `superdesign login` or `superdesign init` to authenticate again.');
412
+ }
413
+ } catch (err) {
414
+ const message = err instanceof Error ? err.message : 'Unknown error';
415
+ output_error(`Failed to logout: ${message}`);
416
+ }
417
+ });
418
+ return command;
419
+ }
420
+ function getSkillTemplate() {
421
+ return `---
422
+ name: superdesign
423
+ description: Superdesign is a design agent, where it specialised in frontend UI/UX design; Use this skill before you implement any UI that require some design thinking
424
+ ---
425
+
426
+ # SuperDesign Integration
427
+ This skill provides integration with the SuperDesign platform for AI-powered design generation.
428
+
429
+ ## Available Commands
430
+
431
+ ### Create Project
432
+
433
+ Create a new new project, can pass optional HTML as initial starting point for design iteration:
434
+
435
+ \`\`\`bash
436
+ # Basic project
437
+ superdesign create-project --title "My Project" --json
438
+
439
+ # Project with initial HTML
440
+ superdesign create-project --title "My Project" --html "<html>...</html>" --json
441
+
442
+ # Project with HTML from file
443
+ superdesign create-project --title "My Project" --html-file ./index.html --json
444
+ \`\`\`
445
+
446
+ **Output (JSON):**
447
+ \`\`\`json
448
+ {
449
+ "projectId": "uuid",
450
+ "title": "My Project",
451
+ "projectUrl": "https://app.superdesign.ai/...",
452
+ "shareToken": "token"
453
+ }
454
+ \`\`\`
455
+
456
+ ### Create Design Draft
457
+
458
+ Generate a design draft using AI designer:
459
+
460
+ \`\`\`bash
461
+ superdesign create-design-draft \\
462
+ --project-id <project-id> \\
463
+ --title "Hero Section" \\
464
+ --prompt "Create a modern hero section with gradient background" \\
465
+ --device desktop \\
466
+ --json
467
+ \`\`\`
468
+
469
+ **Options:**
470
+ - \`--project-id\` (required): Project to add draft to
471
+ - \`--title\` (required): Draft title
472
+ - \`--prompt\` (required): AI generation prompt
473
+ - \`--device\`: Device mode (mobile, tablet, desktop)
474
+
475
+ **Output (JSON):**
476
+ \`\`\`json
477
+ {
478
+ "draftId": "uuid",
479
+ "nodeId": "uuid",
480
+ "title": "Hero Section",
481
+ "projectUrl": "https://app.superdesign.ai/...",
482
+ "nodeUrl": "https://app.superdesign.ai/...?node=...",
483
+ "previewUrl": "https://preview.superdesign.ai/...",
484
+ "creditsConsumed": 1
485
+ }
486
+ \`\`\`
487
+
488
+ ### Iterate Design Draft
489
+
490
+ Create variations or improvements of an existing draft:
491
+
492
+ \`\`\`bash
493
+ # Replace mode - updates the existing draft
494
+ superdesign iterate-design-draft \\
495
+ --draft-id <draft-id> \\
496
+ --prompt "Make the colors more vibrant" \\
497
+ --mode replace \\
498
+ --json
499
+
500
+ # Branch mode - creates new variations
501
+ superdesign iterate-design-draft \\
502
+ --draft-id <draft-id> \\
503
+ --prompt "Try different color schemes" \\
504
+ --mode branch \\
505
+ --count 3 \\
506
+ --json
507
+ \`\`\`
508
+
509
+ **Options:**
510
+ - \`--draft-id\` (required): Draft to iterate on
511
+ - \`--prompt\` (required): Iteration instructions
512
+ - \`--mode\` (required): "replace" or "branch"
513
+ - \`--count\`: Number of variations (1-4, branch mode only)
514
+
515
+ **Output (JSON):**
516
+ \`\`\`json
517
+ {
518
+ "drafts": [
519
+ {
520
+ "draftId": "uuid",
521
+ "nodeId": "uuid",
522
+ "title": "Variation 1",
523
+ "nodeUrl": "https://...",
524
+ "previewUrl": "https://..."
525
+ }
526
+ ],
527
+ "projectUrl": "https://...",
528
+ "creditsConsumed": 3
529
+ }
530
+ \`\`\`
531
+
532
+ ### Plan Flow Pages
533
+
534
+ Get AI suggestions for related pages in a user flow:
535
+
536
+ \`\`\`bash
537
+ superdesign plan-flow-pages \\
538
+ --draft-id <draft-id> \\
539
+ --source-node-id <node-id> \\
540
+ --context "E-commerce checkout flow" \\
541
+ --json
542
+ \`\`\`
543
+
544
+ **Options:**
545
+ - \`--draft-id\` (required): Source draft
546
+ - \`--source-node-id\` (required): Starting node in flow
547
+ - \`--context\`: Additional context for planning
548
+
549
+ **Output (JSON):**
550
+ \`\`\`json
551
+ {
552
+ "pages": [
553
+ {
554
+ "title": "Cart Review",
555
+ "prompt": "Create a cart review page showing..."
556
+ },
557
+ {
558
+ "title": "Payment",
559
+ "prompt": "Create a payment form with..."
560
+ }
561
+ ],
562
+ "creditsConsumed": 1
563
+ }
564
+ \`\`\`
565
+
566
+ ### Execute Flow Pages
567
+
568
+ Generate the planned flow pages:
569
+
570
+ \`\`\`bash
571
+ superdesign execute-flow-pages \\
572
+ --draft-id <draft-id> \\
573
+ --source-node-id <node-id> \\
574
+ --pages '[{"title":"Cart","prompt":"Create cart page"},{"title":"Payment","prompt":"Create payment form"}]' \\
575
+ --json
576
+ \`\`\`
577
+
578
+ **Options:**
579
+ - \`--draft-id\` (required): Source draft
580
+ - \`--source-node-id\` (required): Starting node
581
+ - \`--pages\` (required): JSON array of pages to generate
582
+ - \`--context\`: Additional context
583
+
584
+ **Output (JSON):**
585
+ \`\`\`json
586
+ {
587
+ "drafts": [
588
+ {
589
+ "index": 0,
590
+ "draftId": "uuid",
591
+ "nodeId": "uuid",
592
+ "title": "Cart",
593
+ "nodeUrl": "https://...",
594
+ "previewUrl": "https://..."
595
+ }
596
+ ],
597
+ "projectUrl": "https://...",
598
+ "creditsConsumed": 2
599
+ }
600
+ \`\`\`
601
+
602
+ ## Workflow Examples
603
+
604
+ ### Create a new design from scratch
605
+
606
+ \`\`\`bash
607
+ # 1. Create project
608
+ PROJECT=$(superdesign create-project --title "Landing Page" --json)
609
+ PROJECT_ID=$(echo $PROJECT | jq -r '.projectId')
610
+
611
+ # 2. Generate initial design
612
+ DRAFT=$(superdesign create-design-draft \\
613
+ --project-id $PROJECT_ID \\
614
+ --title "Hero" \\
615
+ --prompt "Modern SaaS landing page hero with gradient, CTA button" \\
616
+ --json)
617
+
618
+ echo "Preview: $(echo $DRAFT | jq -r '.previewUrl')"
619
+ \`\`\`
620
+
621
+ ### Iterate on existing design
622
+
623
+ \`\`\`bash
624
+ # Create variations
625
+ superdesign iterate-design-draft \\
626
+ --draft-id <draft-id> \\
627
+ --prompt "Try a dark theme version" \\
628
+ --mode branch \\
629
+ --count 2 \\
630
+ --json
631
+ \`\`\`
632
+
633
+ ### Build a complete user flow
634
+
635
+ \`\`\`bash
636
+ # 1. Plan the flow
637
+ PLAN=$(superdesign plan-flow-pages \\
638
+ --draft-id <draft-id> \\
639
+ --source-node-id <node-id> \\
640
+ --context "User onboarding flow" \\
641
+ --json)
642
+
643
+ # 2. Execute the plan
644
+ PAGES=$(echo $PLAN | jq -c '.pages')
645
+ superdesign execute-flow-pages \\
646
+ --draft-id <draft-id> \\
647
+ --source-node-id <node-id> \\
648
+ --pages "$PAGES" \\
649
+ --json
650
+ \`\`\`
651
+
652
+
653
+ ## Tips
654
+
655
+ - Always use \`--json\` flag when parsing output programmatically
656
+ - Store project and draft IDs for subsequent operations
657
+ - Use \`plan-flow-pages\` before \`execute-flow-pages\` for best results
658
+ - The \`previewUrl\` can be used to view designs without authentication
659
+ `;
660
+ }
661
+ function createInitCommand() {
662
+ const command = new Command('init').description('Install SuperDesign skill files for Claude Code (auto-login if needed)').option('--json', 'Output in JSON format').option('--force', 'Overwrite existing skill files').action(async (options)=>{
663
+ if (options.json) setJsonMode(true);
664
+ if (!isJsonMode()) await animateLogo();
665
+ try {
666
+ if (!manager_isAuthenticated()) {
667
+ info('Not authenticated. Starting login flow...\n');
668
+ const authResult = await runAuthFlow({
669
+ showSuccessMessage: true
670
+ });
671
+ if (!authResult.success) {
672
+ output_error(authResult.error || 'Authentication failed');
673
+ process.exit(EXIT_CODES.AUTH_FAILED);
674
+ }
675
+ info('');
676
+ }
677
+ const cwd = process.cwd();
678
+ const skillsPath = join(cwd, SKILLS_DIR);
679
+ const skillFilePath = join(skillsPath, SKILL_FILE_NAME);
680
+ if (existsSync(skillFilePath) && !options.force) {
681
+ if (isJsonMode()) output({
682
+ success: false,
683
+ error: 'Skill file already exists. Use --force to overwrite.',
684
+ path: skillFilePath
685
+ });
686
+ else {
687
+ info(`Skill file already exists at: ${skillFilePath}`);
688
+ info('Use --force to overwrite.');
689
+ }
690
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
691
+ }
692
+ mkdirSync(skillsPath, {
693
+ recursive: true
694
+ });
695
+ const skillContent = getSkillTemplate();
696
+ writeFileSync(skillFilePath, skillContent, 'utf-8');
697
+ if (isJsonMode()) output({
698
+ success: true,
699
+ path: skillFilePath,
700
+ message: 'Skill files installed successfully'
701
+ });
702
+ else {
703
+ success('Skill files installed successfully!');
704
+ info(`\nSkill file created at: ${skillFilePath}`);
705
+ info('\nClaude Code will now have access to SuperDesign commands.');
706
+ info('Available commands:');
707
+ info(' - superdesign create-project');
708
+ info(' - superdesign create-design-draft');
709
+ info(' - superdesign iterate-design-draft');
710
+ info(' - superdesign plan-flow-pages');
711
+ info(' - superdesign execute-flow-pages');
712
+ }
713
+ } catch (err) {
714
+ const message = err instanceof Error ? err.message : 'Unknown error';
715
+ output_error(`Failed to initialize: ${message}`);
716
+ process.exit(EXIT_CODES.GENERAL_ERROR);
717
+ }
718
+ });
719
+ return command;
720
+ }
721
+ async function createProject(data) {
722
+ const client = getApiClient();
723
+ const response = await client.post('/external/projects', data);
724
+ return response.data;
725
+ }
726
+ async function addDraft(projectId, data) {
727
+ const client = getApiClient();
728
+ const response = await client.post(`/external/projects/${projectId}/drafts/add`, data);
729
+ return response.data;
730
+ }
731
+ function createCreateProjectCommand() {
732
+ const command = new Command('create-project').description('Create a new SuperDesign project').requiredOption('--title <title>', 'Project title').option('--html <html>', 'Initial HTML content for first draft').option('--html-file <path>', 'Path to HTML file for first draft').option('--device <mode>', 'Device mode for draft (mobile, tablet, desktop)', 'desktop').option('--json', 'Output in JSON format').action(async (options)=>{
733
+ if (options.json) setJsonMode(true);
734
+ try {
735
+ if (!manager_isAuthenticated()) {
736
+ output_error('Not authenticated. Run `superdesign login` first.');
737
+ process.exit(EXIT_CODES.AUTH_REQUIRED);
738
+ }
739
+ if (options.device && ![
740
+ 'mobile',
741
+ 'tablet',
742
+ 'desktop'
743
+ ].includes(options.device)) {
744
+ output_error('Invalid device mode. Must be: mobile, tablet, or desktop');
745
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
746
+ }
747
+ let htmlContent;
748
+ if (options.htmlFile) {
749
+ if (!existsSync(options.htmlFile)) {
750
+ output_error(`HTML file not found: ${options.htmlFile}`);
751
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
752
+ }
753
+ htmlContent = readFileSync(options.htmlFile, 'utf-8');
754
+ } else if (options.html) htmlContent = options.html;
755
+ startSpinner('Creating project...');
756
+ const project = await createProject({
757
+ title: options.title
758
+ });
759
+ let draftResult = null;
760
+ if (htmlContent) draftResult = await addDraft(project.projectId, {
761
+ title: 'Initial Draft',
762
+ html: htmlContent,
763
+ deviceMode: options.device
764
+ });
765
+ succeedSpinner('Project created successfully!');
766
+ const result = {
767
+ projectId: project.projectId,
768
+ title: project.title,
769
+ projectUrl: project.projectUrl,
770
+ shareToken: project.shareToken,
771
+ ...draftResult && {
772
+ draft: {
773
+ draftId: draftResult.draftId,
774
+ nodeId: draftResult.nodeId,
775
+ nodeUrl: draftResult.nodeUrl,
776
+ previewUrl: draftResult.previewUrl
777
+ }
778
+ }
779
+ };
780
+ if (isJsonMode()) output(result);
781
+ else {
782
+ success(`Project "${project.title}" created!`);
783
+ info(`\nProject ID: ${project.projectId}`);
784
+ info(`Project URL: ${project.projectUrl}`);
785
+ if (draftResult) {
786
+ info(`\nDraft ID: ${draftResult.draftId}`);
787
+ info(`Draft URL: ${draftResult.nodeUrl}`);
788
+ info(`Preview URL: ${draftResult.previewUrl}`);
789
+ }
790
+ }
791
+ } catch (err) {
792
+ failSpinner('Failed to create project');
793
+ if (err instanceof ApiClientError) {
794
+ output_error(`API Error: ${err.message}`);
795
+ process.exit(EXIT_CODES.API_ERROR);
796
+ }
797
+ const message = err instanceof Error ? err.message : 'Unknown error';
798
+ output_error(message);
799
+ process.exit(EXIT_CODES.GENERAL_ERROR);
800
+ }
801
+ });
802
+ return command;
803
+ }
804
+ async function createDraft(projectId, data) {
805
+ const client = getApiClient();
806
+ const response = await client.post(`/external/projects/${projectId}/drafts/create`, data);
807
+ return response.data;
808
+ }
809
+ async function iterateDraft(draftId, data) {
810
+ const client = getApiClient();
811
+ const response = await client.post(`/external/drafts/${draftId}/iterate`, data);
812
+ return response.data;
813
+ }
814
+ async function planFlowPages(draftId, data) {
815
+ const client = getApiClient();
816
+ const response = await client.post(`/external/drafts/${draftId}/flow/plan`, data);
817
+ return response.data;
818
+ }
819
+ async function executeFlowPages(draftId, data) {
820
+ const client = getApiClient();
821
+ const response = await client.post(`/external/drafts/${draftId}/flow/execute`, data);
822
+ return response.data;
823
+ }
824
+ async function getJobStatus(jobId) {
825
+ const client = getApiClient();
826
+ const response = await client.get(`/external/jobs/${jobId}`);
827
+ return response.data;
828
+ }
829
+ function isJobDone(response) {
830
+ return 'completed' === response.status || 'failed' === response.status;
831
+ }
832
+ function isJobCompleted(response) {
833
+ return 'completed' === response.status;
834
+ }
835
+ function isJobFailed(response) {
836
+ return 'failed' === response.status;
837
+ }
838
+ async function runJob(config) {
839
+ const { startLabel, pollingLabel, successLabel, timeoutLabel, failureLabel, timeoutMs = 300000, startJob, transformResult, displayResult } = config;
840
+ try {
841
+ startSpinner(startLabel);
842
+ const job = await startJob();
843
+ updateSpinner(pollingLabel);
844
+ const pollResult = await poll(()=>getJobStatus(job.jobId), isJobDone, {
845
+ intervalMs: 2000,
846
+ timeoutMs,
847
+ onPoll: (attempt)=>{
848
+ if (attempt % 5 === 0) updateSpinner(`${pollingLabel} (${Math.floor(2000 * attempt / 1000)}s)`);
849
+ }
850
+ });
851
+ if (pollResult.timedOut) {
852
+ failSpinner(timeoutLabel);
853
+ output_error('Job timed out. The operation may still be processing in the background.');
854
+ if (isJsonMode()) output({
855
+ error: 'timeout',
856
+ jobId: job.jobId
857
+ });
858
+ process.exit(EXIT_CODES.TIMEOUT);
859
+ }
860
+ if (!pollResult.success || !pollResult.data) {
861
+ failSpinner(failureLabel);
862
+ output_error(pollResult.error || 'Failed to get job status');
863
+ process.exit(EXIT_CODES.API_ERROR);
864
+ }
865
+ const jobResult = pollResult.data;
866
+ if (isJobFailed(jobResult)) {
867
+ failSpinner(failureLabel);
868
+ output_error(`${jobResult.error.code}: ${jobResult.error.message}`);
869
+ process.exit(EXIT_CODES.API_ERROR);
870
+ }
871
+ if (!isJobCompleted(jobResult)) {
872
+ failSpinner('Unexpected job status');
873
+ output_error('Job ended in unexpected state');
874
+ process.exit(EXIT_CODES.API_ERROR);
875
+ }
876
+ succeedSpinner(successLabel);
877
+ if (isJsonMode()) output(transformResult(jobResult));
878
+ else displayResult(jobResult);
879
+ process.exit(EXIT_CODES.SUCCESS);
880
+ } catch (err) {
881
+ failSpinner(failureLabel);
882
+ if (err instanceof ApiClientError) {
883
+ output_error(`API Error: ${err.message}`);
884
+ process.exit(EXIT_CODES.API_ERROR);
885
+ }
886
+ const message = err instanceof Error ? err.message : 'Unknown error';
887
+ output_error(message);
888
+ process.exit(EXIT_CODES.GENERAL_ERROR);
889
+ }
890
+ }
891
+ function job_runner_requireAuth(isAuthenticated) {
892
+ if (!isAuthenticated()) {
893
+ output_error('Not authenticated. Run `superdesign login` first.');
894
+ process.exit(EXIT_CODES.AUTH_REQUIRED);
895
+ }
896
+ }
897
+ function createCreateDesignDraftCommand() {
898
+ const command = new Command('create-design-draft').description('Create a design draft using AI generation').requiredOption('--project-id <id>', 'Project ID').requiredOption('--title <title>', 'Draft title').requiredOption('--prompt <prompt>', 'Design prompt for AI generation').option('--device <mode>', 'Device mode (mobile, tablet, desktop)', 'desktop').option('--json', 'Output in JSON format').action(async (options)=>{
899
+ if (options.json) setJsonMode(true);
900
+ job_runner_requireAuth(manager_isAuthenticated);
901
+ if (options.device && ![
902
+ 'mobile',
903
+ 'tablet',
904
+ 'desktop'
905
+ ].includes(options.device)) {
906
+ output_error('Invalid device mode. Must be: mobile, tablet, or desktop');
907
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
908
+ }
909
+ await runJob({
910
+ startLabel: 'Creating design draft...',
911
+ pollingLabel: 'Generating design with AI...',
912
+ successLabel: 'Design draft created!',
913
+ timeoutLabel: 'Generation timed out',
914
+ failureLabel: 'Failed to create draft',
915
+ startJob: ()=>createDraft(options.projectId, {
916
+ title: options.title,
917
+ prompt: options.prompt,
918
+ deviceMode: options.device
919
+ }),
920
+ transformResult: (job)=>({
921
+ draftId: job.result.draftId,
922
+ nodeId: job.result.nodeId,
923
+ title: job.result.title,
924
+ projectUrl: job.result.projectUrl,
925
+ nodeUrl: job.result.nodeUrl,
926
+ previewUrl: job.result.previewUrl,
927
+ creditsConsumed: job.creditsConsumed
928
+ }),
929
+ displayResult: (job)=>{
930
+ success(`Draft "${job.result.title}" created!`);
931
+ info(`\nDraft ID: ${job.result.draftId}`);
932
+ info(`Node URL: ${job.result.nodeUrl}`);
933
+ info(`Preview URL: ${job.result.previewUrl}`);
934
+ info(`Credits consumed: ${job.creditsConsumed}`);
935
+ }
936
+ });
937
+ });
938
+ return command;
939
+ }
940
+ function createIterateDesignDraftCommand() {
941
+ const command = new Command('iterate-design-draft').description('Iterate on an existing draft using AI').requiredOption('--draft-id <id>', 'Draft ID to iterate on').requiredOption('--prompt <prompt>', 'Iteration prompt').requiredOption('--mode <mode>', 'Iteration mode (replace or branch)').option('--count <count>', 'Number of variations to generate (1-4)', '1').option('--json', 'Output in JSON format').action(async (options)=>{
942
+ if (options.json) setJsonMode(true);
943
+ job_runner_requireAuth(manager_isAuthenticated);
944
+ if (![
945
+ 'replace',
946
+ 'branch'
947
+ ].includes(options.mode)) {
948
+ output_error('Invalid mode. Must be: replace or branch');
949
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
950
+ }
951
+ const count = parseInt(options.count || '1', 10);
952
+ if (isNaN(count) || count < 1 || count > 4) {
953
+ output_error('Invalid count. Must be between 1 and 4');
954
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
955
+ }
956
+ await runJob({
957
+ startLabel: 'Starting iteration...',
958
+ pollingLabel: 'Iterating design with AI...',
959
+ successLabel: 'Iteration complete!',
960
+ timeoutLabel: 'Iteration timed out',
961
+ failureLabel: 'Failed to iterate draft',
962
+ startJob: ()=>iterateDraft(options.draftId, {
963
+ prompt: options.prompt,
964
+ mode: options.mode,
965
+ count: count
966
+ }),
967
+ transformResult: (job)=>({
968
+ drafts: job.result.drafts,
969
+ projectUrl: job.result.projectUrl,
970
+ creditsConsumed: job.creditsConsumed
971
+ }),
972
+ displayResult: (job)=>{
973
+ success(`Created ${job.result.drafts.length} draft variation(s)!`);
974
+ info(`\nProject URL: ${job.result.projectUrl}`);
975
+ info(`Credits consumed: ${job.creditsConsumed}`);
976
+ info('\nDrafts:');
977
+ job.result.drafts.forEach((draft, i)=>{
978
+ info(` ${i + 1}. ${draft.title}`);
979
+ info(` Draft ID: ${draft.draftId}`);
980
+ info(` Preview: ${draft.previewUrl}`);
981
+ });
982
+ }
983
+ });
984
+ });
985
+ return command;
986
+ }
987
+ function createPlanFlowPagesCommand() {
988
+ const command = new Command('plan-flow-pages').description('Plan flow pages using AI').requiredOption('--draft-id <id>', 'Source draft ID').requiredOption('--source-node-id <id>', 'Source node ID in the flow').option('--context <context>', 'Additional context for flow planning').option('--json', 'Output in JSON format').action(async (options)=>{
989
+ if (options.json) setJsonMode(true);
990
+ job_runner_requireAuth(manager_isAuthenticated);
991
+ await runJob({
992
+ startLabel: 'Planning flow pages...',
993
+ pollingLabel: 'AI is analyzing and planning pages...',
994
+ successLabel: 'Flow pages planned!',
995
+ timeoutLabel: 'Planning timed out',
996
+ failureLabel: 'Failed to plan flow pages',
997
+ startJob: ()=>planFlowPages(options.draftId, {
998
+ sourceNodeId: options.sourceNodeId,
999
+ flowContext: options.context
1000
+ }),
1001
+ transformResult: (job)=>({
1002
+ pages: job.result.pages,
1003
+ creditsConsumed: job.creditsConsumed
1004
+ }),
1005
+ displayResult: (job)=>{
1006
+ success(`Planned ${job.result.pages.length} flow page(s)!`);
1007
+ info(`Credits consumed: ${job.creditsConsumed}`);
1008
+ info('\nSuggested pages:');
1009
+ job.result.pages.forEach((page, i)=>{
1010
+ info(`\n ${i + 1}. ${page.title}`);
1011
+ const promptPreview = page.prompt.length > 100 ? `${page.prompt.substring(0, 100)}...` : page.prompt;
1012
+ info(` Prompt: ${promptPreview}`);
1013
+ });
1014
+ info('\nUse `superdesign execute-flow-pages` to generate these pages.');
1015
+ }
1016
+ });
1017
+ });
1018
+ return command;
1019
+ }
1020
+ function parsePages(pagesJson) {
1021
+ const parsed = JSON.parse(pagesJson);
1022
+ if (!Array.isArray(parsed)) throw new Error('Pages must be an array');
1023
+ for (const page of parsed){
1024
+ if (!page.title || 'string' != typeof page.title) throw new Error('Each page must have a title string');
1025
+ if (!page.prompt || 'string' != typeof page.prompt) throw new Error('Each page must have a prompt string');
1026
+ }
1027
+ return parsed;
1028
+ }
1029
+ function createExecuteFlowPagesCommand() {
1030
+ const command = new Command('execute-flow-pages').description('Generate flow pages using AI').requiredOption('--draft-id <id>', 'Source draft ID').requiredOption('--source-node-id <id>', 'Source node ID in the flow').requiredOption('--pages <json>', 'JSON array of pages to generate [{title, prompt}]').option('--context <context>', 'Additional context for flow generation').option('--json', 'Output in JSON format').action(async (options)=>{
1031
+ if (options.json) setJsonMode(true);
1032
+ job_runner_requireAuth(manager_isAuthenticated);
1033
+ let pages;
1034
+ try {
1035
+ pages = parsePages(options.pages);
1036
+ } catch (parseErr) {
1037
+ const msg = parseErr instanceof Error ? parseErr.message : 'Invalid JSON';
1038
+ output_error(`Invalid pages JSON: ${msg}`);
1039
+ output_error('Expected format: [{"title": "Page 1", "prompt": "Create a..."}]');
1040
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
1041
+ }
1042
+ if (0 === pages.length) {
1043
+ output_error('At least one page is required');
1044
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
1045
+ }
1046
+ if (pages.length > 10) {
1047
+ output_error('Maximum 10 pages allowed');
1048
+ process.exit(EXIT_CODES.VALIDATION_ERROR);
1049
+ }
1050
+ const timeoutMs = Math.max(300000, 2 * pages.length * 60000);
1051
+ await runJob({
1052
+ startLabel: `Generating ${pages.length} flow page(s)...`,
1053
+ pollingLabel: `AI is generating ${pages.length} page(s)...`,
1054
+ successLabel: 'Flow pages generated!',
1055
+ timeoutLabel: 'Generation timed out',
1056
+ failureLabel: 'Failed to execute flow pages',
1057
+ timeoutMs,
1058
+ startJob: ()=>executeFlowPages(options.draftId, {
1059
+ sourceNodeId: options.sourceNodeId,
1060
+ flowContext: options.context,
1061
+ pages
1062
+ }),
1063
+ transformResult: (job)=>({
1064
+ drafts: job.result.drafts,
1065
+ projectUrl: job.result.projectUrl,
1066
+ creditsConsumed: job.creditsConsumed
1067
+ }),
1068
+ displayResult: (job)=>{
1069
+ success(`Generated ${job.result.drafts.length} flow page(s)!`);
1070
+ info(`\nProject URL: ${job.result.projectUrl}`);
1071
+ info(`Credits consumed: ${job.creditsConsumed}`);
1072
+ info('\nGenerated pages:');
1073
+ job.result.drafts.forEach((draft)=>{
1074
+ info(`\n ${draft.index + 1}. ${draft.title}`);
1075
+ info(` Draft ID: ${draft.draftId}`);
1076
+ info(` Node URL: ${draft.nodeUrl}`);
1077
+ info(` Preview: ${draft.previewUrl}`);
1078
+ });
1079
+ }
1080
+ });
1081
+ });
1082
+ return command;
1083
+ }
1084
+ const src_filename = fileURLToPath(import.meta.url);
1085
+ const src_dirname = dirname(src_filename);
1086
+ external_dotenv_config({
1087
+ path: external_path_resolve(src_dirname, '../.env')
1088
+ });
1089
+ external_dotenv_config();
1090
+ function createProgram() {
1091
+ const program = new Command();
1092
+ program.name('superdesign').description('SuperDesign CLI - AI product designer for coding agents').version("0.1.0");
1093
+ program.addCommand(createLoginCommand());
1094
+ program.addCommand(createLogoutCommand());
1095
+ program.addCommand(createInitCommand());
1096
+ program.addCommand(createCreateProjectCommand());
1097
+ program.addCommand(createCreateDesignDraftCommand());
1098
+ program.addCommand(createIterateDesignDraftCommand());
1099
+ program.addCommand(createPlanFlowPagesCommand());
1100
+ program.addCommand(createExecuteFlowPagesCommand());
1101
+ return program;
1102
+ }
1103
+ async function run() {
1104
+ const program = createProgram();
1105
+ await program.parseAsync(process.argv);
1106
+ }
1107
+ export { ApiClientError, addDraft, claimSession, clearConfig, createApiClient, createDraft, createProgram, createProject, createPublicApiClient, createSession, executeFlowPages, getApiClient, getApiKey, getApiUrl, getConfigDir, getConfigPath, getJobStatus, manager_isAuthenticated as isAuthenticated, isJobCompleted, isJobDone, isJobFailed, iterateDraft, loadConfig, planFlowPages, pollSession, run, saveConfig, updateConfig };