@fragments-sdk/cli 0.11.1 → 0.13.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.
Files changed (89) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +419 -410
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
  5. package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
  6. package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
  7. package/dist/chunk-DXX6HADE.js +443 -0
  8. package/dist/chunk-DXX6HADE.js.map +1 -0
  9. package/dist/chunk-EYXVAMEX.js +626 -0
  10. package/dist/chunk-EYXVAMEX.js.map +1 -0
  11. package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
  12. package/dist/chunk-FO6EBJWP.js.map +1 -0
  13. package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
  14. package/dist/chunk-QM7SVOGF.js.map +1 -0
  15. package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
  16. package/dist/chunk-RF3C6LGA.js.map +1 -0
  17. package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
  18. package/dist/chunk-SM674YAS.js.map +1 -0
  19. package/dist/chunk-SXTKFDCR.js +104 -0
  20. package/dist/chunk-SXTKFDCR.js.map +1 -0
  21. package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
  22. package/dist/core/index.js +13 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-XK6PRUE5.js +636 -0
  28. package/dist/init-XK6PRUE5.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
  31. package/dist/scan-generate-U3RFVDTX.js +1115 -0
  32. package/dist/scan-generate-U3RFVDTX.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
  34. package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
  35. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
  36. package/dist/static-viewer-KKCR4KXR.js.map +1 -0
  37. package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
  38. package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
  39. package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
  40. package/dist/viewer-M2EQQSGE.js.map +1 -0
  41. package/package.json +11 -9
  42. package/src/ai-client.ts +156 -0
  43. package/src/bin.ts +99 -2
  44. package/src/build.ts +95 -33
  45. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  46. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  47. package/src/commands/enhance.ts +11 -35
  48. package/src/commands/govern.ts +122 -0
  49. package/src/commands/init.ts +288 -260
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/sync.ts +357 -0
  54. package/src/commands/validate.ts +43 -1
  55. package/src/core/component-extractor.test.ts +282 -0
  56. package/src/core/component-extractor.ts +1030 -0
  57. package/src/core/discovery.ts +93 -7
  58. package/src/service/enhance/props-extractor.ts +235 -13
  59. package/src/validators.ts +236 -0
  60. package/src/viewer/vite-plugin.ts +1 -1
  61. package/dist/chunk-5G3VZH43.js.map +0 -1
  62. package/dist/chunk-OQO55NKV.js.map +0 -1
  63. package/dist/chunk-WXSR2II7.js.map +0 -1
  64. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  65. package/dist/init-UFGK5TCN.js +0 -867
  66. package/dist/init-UFGK5TCN.js.map +0 -1
  67. package/dist/scan-generate-SJAN5MVI.js +0 -691
  68. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  69. package/dist/viewer-DLLJIMCK.js.map +0 -1
  70. package/src/ai.ts +0 -266
  71. package/src/commands/init-framework.ts +0 -414
  72. package/src/mcp/bin.ts +0 -36
  73. package/src/migrate/bin.ts +0 -114
  74. package/src/theme/index.ts +0 -77
  75. package/src/viewer/bin.ts +0 -86
  76. package/src/viewer/cli/health.ts +0 -256
  77. package/src/viewer/cli/index.ts +0 -33
  78. package/src/viewer/cli/scan.ts +0 -124
  79. package/src/viewer/cli/utils.ts +0 -174
  80. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  81. /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
  82. /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
  83. /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
  84. /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
  85. /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
  86. /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
  87. /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
  88. /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
  89. /package/dist/{tokens-CE46OTMD.js.map → tokens-L46MK5AW.js.map} +0 -0
@@ -1,37 +1,48 @@
1
1
  /**
2
- * fragments init - Smart interactive initialization
2
+ * fragments init - Zero-config project initialization
3
3
  *
4
- * Handles four scenarios:
5
- * 1. --scan <path> Scan external component library, generate fragment files
6
- * 2. Stories found Configure and load existing stories
7
- * 3. Components found (no stories) → Auto-generate documentation
8
- * 4. Fresh project → Guided setup with example component
4
+ * Default: auto-detect everything, zero prompts, instant setup.
5
+ * --configure: interactive mode for theme seeds, snapshots, etc.
6
+ * --scan <path>: scan external component library, generate fragment files.
9
7
  */
10
8
 
11
9
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
12
- import { resolve, join, relative, dirname, basename } from "node:path";
10
+ import { resolve, join, relative } from "node:path";
13
11
  import { spawn } from "node:child_process";
14
12
  import pc from "picocolors";
15
13
  import { BRAND } from "../core/index.js";
16
14
  import fg from "fast-glob";
17
- import { input, confirm, select } from "@inquirer/prompts";
18
15
  import {
19
- setupFramework,
20
- detectFramework,
21
- type Framework,
22
- } from "./init-framework.js";
16
+ detectSetupFramework,
17
+ findEntryFile,
18
+ addStylesImport,
19
+ addThemeProvider,
20
+ addTranspilePackages,
21
+ } from "./setup.js";
23
22
 
24
23
  export interface InitOptions {
25
24
  /** Project root directory */
26
25
  projectRoot?: string;
27
26
  /** Force overwrite existing config */
28
27
  force?: boolean;
29
- /** Non-interactive mode - auto-detect and use defaults */
28
+ /** Non-interactive mode - auto-detect and use defaults (legacy, now the default behavior) */
30
29
  yes?: boolean;
31
30
  /** Explicit framework override */
32
31
  framework?: string;
33
32
  /** Path to scan for components (enables scan mode) */
34
33
  scan?: string;
34
+ /** Enable interactive configuration (theme seeds, snapshots, etc.) */
35
+ configure?: boolean;
36
+ /** Use AI to fill knowledge fields during --scan */
37
+ enrich?: boolean;
38
+ /** Show what --enrich would generate without calling API */
39
+ dryRun?: boolean;
40
+ /** AI provider for enrichment: anthropic or openai */
41
+ provider?: 'anthropic' | 'openai';
42
+ /** API key for AI enrichment */
43
+ apiKey?: string;
44
+ /** Override AI model for enrichment */
45
+ model?: string;
35
46
  }
36
47
 
37
48
  export interface InitResult {
@@ -49,14 +60,13 @@ interface DetectionResult {
49
60
  hasConfig: boolean;
50
61
  configPath: string | null;
51
62
  suggestedComponentPath: string;
63
+ scenario: "stories" | "components" | "fresh";
52
64
  }
53
65
 
54
66
  /**
55
67
  * Detect what exists in the project
56
68
  */
57
69
  async function detectProject(projectRoot: string): Promise<DetectionResult> {
58
- console.log(pc.dim("\nScanning project...\n"));
59
-
60
70
  // Check for existing config
61
71
  const configPath = join(projectRoot, BRAND.configFile);
62
72
  const legacyConfigPath = join(projectRoot, BRAND.legacyConfigFile);
@@ -127,12 +137,16 @@ async function detectProject(projectRoot: string): Promise<DetectionResult> {
127
137
  }
128
138
  }
129
139
 
140
+ const scenario: DetectionResult["scenario"] =
141
+ storyFiles.length > 0 ? "stories" : likelyComponents.length > 0 ? "components" : "fresh";
142
+
130
143
  return {
131
144
  storyFiles,
132
145
  componentFiles: likelyComponents,
133
146
  hasConfig,
134
147
  configPath: foundConfigPath,
135
148
  suggestedComponentPath,
149
+ scenario,
136
150
  };
137
151
  }
138
152
 
@@ -328,52 +342,6 @@ export default defineFragment({
328
342
  `;
329
343
  }
330
344
 
331
- /**
332
- * Convert a filename to PascalCase component name
333
- */
334
- function toPascalCase(str: string): string {
335
- return str
336
- .replace(/[-_.](\w)/g, (_, c) => c.toUpperCase())
337
- .replace(/^\w/, (c) => c.toUpperCase());
338
- }
339
-
340
- /**
341
- * Generate a minimal fragment stub for a discovered component
342
- */
343
- function generateFragmentStub(componentName: string, importPath: string): string {
344
- return `import React from 'react';
345
- import { defineFragment } from '@fragments-sdk/cli/core';
346
- import { ${componentName} } from '${importPath}';
347
-
348
- export default defineFragment({
349
- component: ${componentName},
350
-
351
- meta: {
352
- name: '${componentName}',
353
- description: '${componentName} component',
354
- category: 'general',
355
- status: 'beta',
356
- },
357
-
358
- usage: {
359
- when: ['TODO: describe when to use ${componentName}'],
360
- whenNot: ['TODO: describe when not to use ${componentName}'],
361
- },
362
-
363
- props: {},
364
-
365
- variants: [
366
- {
367
- name: 'Default',
368
- description: 'Default ${componentName}',
369
- code: \`<${componentName} />\`,
370
- render: () => <${componentName} />,
371
- },
372
- ],
373
- });
374
- `;
375
- }
376
-
377
345
  /**
378
346
  * Start the dev server
379
347
  */
@@ -395,7 +363,142 @@ function startDevServer(projectRoot: string): void {
395
363
  }
396
364
 
397
365
  /**
398
- * Main init function - smart and interactive by default
366
+ * Map init-framework's Framework type to setup's Framework type
367
+ */
368
+ function mapFrameworkLabel(framework: string): string {
369
+ const labels: Record<string, string> = {
370
+ "nextjs-app": "Next.js (App Router)",
371
+ "nextjs-pages": "Next.js (Pages Router)",
372
+ "vite": "Vite",
373
+ "remix": "Remix",
374
+ "astro": "Astro",
375
+ };
376
+ return labels[framework] || "Unknown";
377
+ }
378
+
379
+ // ============================================
380
+ // Interactive configuration (--configure)
381
+ // ============================================
382
+
383
+ interface ConfigureResult {
384
+ componentPath: string;
385
+ createExample: boolean;
386
+ startServer: boolean;
387
+ themeBlock: string;
388
+ snapshotsBlock: string;
389
+ }
390
+
391
+ async function runInteractiveConfigure(
392
+ detection: DetectionResult,
393
+ scenario: "stories" | "components" | "fresh"
394
+ ): Promise<ConfigureResult> {
395
+ const { input, confirm, select } = await import("@inquirer/prompts");
396
+
397
+ const componentPath = await input({
398
+ message: "Where are your components located?",
399
+ default: detection.suggestedComponentPath,
400
+ });
401
+
402
+ let createExample = scenario === "fresh";
403
+ if (scenario === "fresh") {
404
+ createExample = await confirm({
405
+ message: "Create an example Button component to get started?",
406
+ default: true,
407
+ });
408
+ }
409
+
410
+ // Theme seed configuration
411
+ let themeBlock = "";
412
+ const configureTheme = await confirm({
413
+ message: "Configure theme seeds? (brand color, density, radius)",
414
+ default: false,
415
+ });
416
+
417
+ if (configureTheme) {
418
+ const brand = await input({
419
+ message: "Brand color (hex)",
420
+ default: "#18181b",
421
+ validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) || "Enter a valid hex color (e.g., #6366f1)",
422
+ });
423
+
424
+ const neutral = await select({
425
+ message: "Neutral palette",
426
+ choices: [
427
+ { value: "stone", name: "Stone (warm gray)" },
428
+ { value: "ice", name: "Ice (cool blue-gray)" },
429
+ { value: "earth", name: "Earth (olive/khaki)" },
430
+ { value: "sand", name: "Sand (warm beige)" },
431
+ { value: "fire", name: "Fire (warm red-gray)" },
432
+ ],
433
+ default: "stone",
434
+ });
435
+
436
+ const density = await select({
437
+ message: "Spacing density",
438
+ choices: [
439
+ { value: "compact", name: "Compact (tighter spacing)" },
440
+ { value: "default", name: "Default" },
441
+ { value: "relaxed", name: "Relaxed (more breathing room)" },
442
+ ],
443
+ default: "default",
444
+ });
445
+
446
+ const radiusStyle = await select({
447
+ message: "Border radius style",
448
+ choices: [
449
+ { value: "sharp", name: "Sharp (0px)" },
450
+ { value: "subtle", name: "Subtle (2px)" },
451
+ { value: "default", name: "Default (6px)" },
452
+ { value: "rounded", name: "Rounded (10px)" },
453
+ { value: "pill", name: "Pill (999px)" },
454
+ ],
455
+ default: "default",
456
+ });
457
+
458
+ // Build theme config block — only include non-default values
459
+ const themeEntries: string[] = [];
460
+ if (brand !== "#18181b") themeEntries.push(` brand: '${brand}'`);
461
+ if (neutral !== "stone") themeEntries.push(` neutral: '${neutral}'`);
462
+ if (density !== "default") themeEntries.push(` density: '${density}'`);
463
+ if (radiusStyle !== "default") themeEntries.push(` radiusStyle: '${radiusStyle}'`);
464
+
465
+ if (themeEntries.length > 0) {
466
+ themeBlock = `\n // Theme seed values (derives 120+ CSS custom properties)\n theme: {\n${themeEntries.join(",\n")},\n },\n`;
467
+ }
468
+ }
469
+
470
+ // Snapshot toggle
471
+ let snapshotsBlock = "";
472
+ const enableSnapshots = await confirm({
473
+ message: "Enable visual snapshot tests per component variant?",
474
+ default: false,
475
+ });
476
+
477
+ if (enableSnapshots) {
478
+ snapshotsBlock = `\n // Visual snapshot testing\n snapshots: {\n enabled: true,\n },\n`;
479
+ }
480
+
481
+ // Ask about starting the server
482
+ const startServer = await confirm({
483
+ message: "Start the viewer now?",
484
+ default: true,
485
+ });
486
+
487
+ return {
488
+ componentPath,
489
+ createExample,
490
+ startServer,
491
+ themeBlock,
492
+ snapshotsBlock,
493
+ };
494
+ }
495
+
496
+ // ============================================
497
+ // Main init function
498
+ // ============================================
499
+
500
+ /**
501
+ * Main init function - zero-config by default, interactive with --configure
399
502
  */
400
503
  export async function init(options: InitOptions = {}): Promise<InitResult> {
401
504
  const projectRoot = resolve(options.projectRoot || process.cwd());
@@ -425,6 +528,11 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
425
528
  scanPath,
426
529
  force: options.force,
427
530
  verbose: true,
531
+ enrich: options.enrich,
532
+ dryRun: options.dryRun,
533
+ provider: options.provider,
534
+ apiKey: options.apiKey,
535
+ model: options.model,
428
536
  });
429
537
 
430
538
  // Create config pointing at the scanned path
@@ -446,9 +554,16 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
446
554
  // Next steps
447
555
  if (scanResult.success) {
448
556
  console.log(pc.cyan("Next steps:"));
449
- console.log(` 1. Search generated files for ${pc.bold("TODO:")} markers and fill in human knowledge`);
450
- console.log(` 2. Run ${pc.bold(`${BRAND.cliCommand} dev`)} to preview your components`);
451
- console.log(` 3. Run ${pc.bold(`${BRAND.cliCommand} build`)} to compile fragments.json`);
557
+ if (options.enrich && !options.dryRun) {
558
+ console.log(` 1. Review AI-enriched fields (usage.when, a11yRules, scenarioTags) in generated files`);
559
+ if (scanResult.generated.some(g => g.todoCount > 0)) {
560
+ console.log(` 2. Search remaining ${pc.bold("TODO:")} markers and fill in human knowledge`);
561
+ }
562
+ } else {
563
+ console.log(` 1. Search generated files for ${pc.bold("TODO:")} markers and fill in human knowledge`);
564
+ }
565
+ console.log(` ${options.enrich ? '3' : '2'}. Run ${pc.bold(`${BRAND.cliCommand} dev`)} to preview your components`);
566
+ console.log(` ${options.enrich ? '4' : '3'}. Run ${pc.bold(`${BRAND.cliCommand} build`)} to compile fragments.json`);
452
567
  console.log();
453
568
  }
454
569
 
@@ -465,184 +580,76 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
465
580
  };
466
581
  }
467
582
 
468
- console.log(pc.cyan(`\n✨ Welcome to ${BRAND.name}!\n`));
583
+ console.log(pc.cyan(`\n${BRAND.name} init\n`));
469
584
 
470
585
  // Step 1: Detect what exists
471
586
  const detection = await detectProject(projectRoot);
472
587
 
473
588
  // Check for existing config
474
589
  if (detection.hasConfig && !options.force) {
475
- console.log(pc.yellow(`⚠ Config already exists: ${BRAND.configFile}`));
476
-
477
- if (!options.yes) {
478
- const overwrite = await confirm({
479
- message: "Do you want to reinitialize? (This will overwrite your config)",
480
- default: false,
481
- });
482
-
483
- if (!overwrite) {
484
- console.log(pc.dim(`\nKeeping existing configuration. Run \`${BRAND.cliCommand} dev\` to start.\n`));
485
- return {
486
- success: true,
487
- scenario: "stories",
488
- storiesFound: detection.storyFiles.length,
489
- componentsFound: detection.componentFiles.length,
490
- errors: [],
491
- };
492
- }
590
+ if (options.configure) {
591
+ // In configure mode, warn but continue
592
+ console.log(pc.yellow(` ! Config exists: ${BRAND.configFile} (will overwrite)`));
593
+ } else {
594
+ console.log(pc.green(` ✓ Already initialized`) + pc.dim(` (${BRAND.configFile} exists)`));
595
+ console.log();
596
+ console.log(pc.dim(` Run ${pc.bold(`${BRAND.cliCommand} init --force`)} to reinitialize`));
597
+ console.log(pc.dim(` Run ${pc.bold(`${BRAND.cliCommand} init --configure`)} to customize theme, snapshots, etc.`));
598
+ console.log();
599
+ return {
600
+ success: true,
601
+ scenario: detection.scenario,
602
+ storiesFound: detection.storyFiles.length,
603
+ componentsFound: detection.componentFiles.length,
604
+ errors: [],
605
+ };
493
606
  }
494
607
  }
495
608
 
496
- // Step 2: Determine scenario and show what we found
609
+ // Step 2: Determine scenario
497
610
  let scenario: "stories" | "components" | "fresh";
498
611
 
499
612
  if (detection.storyFiles.length > 0) {
500
613
  scenario = "stories";
501
- console.log(pc.green(`✓ Found ${detection.storyFiles.length} Storybook story file(s)`));
502
- console.log(pc.dim(` ${detection.storyFiles.slice(0, 3).join("\n ")}`));
503
- if (detection.storyFiles.length > 3) {
504
- console.log(pc.dim(` ... and ${detection.storyFiles.length - 3} more`));
505
- }
506
- console.log();
507
- console.log(
508
- pc.cyan("Great news! ") +
509
- "Fragments can load your existing stories automatically."
510
- );
614
+ console.log(pc.green(` ✓ Found ${detection.storyFiles.length} Storybook stories`));
511
615
  } else if (detection.componentFiles.length > 0) {
512
616
  scenario = "components";
513
- console.log(pc.green(`✓ Found ${detection.componentFiles.length} component file(s)`));
514
- console.log(pc.dim(` ${detection.componentFiles.slice(0, 3).join("\n ")}`));
515
- if (detection.componentFiles.length > 3) {
516
- console.log(pc.dim(` ... and ${detection.componentFiles.length - 3} more`));
517
- }
518
- console.log();
519
- console.log(
520
- pc.cyan("No stories found, but that's fine! ") +
521
- "Fragments can auto-generate documentation from your TypeScript."
522
- );
617
+ console.log(pc.green(` ✓ Found ${detection.componentFiles.length} components`) + pc.dim(` in ${detection.suggestedComponentPath}`));
523
618
  } else {
524
619
  scenario = "fresh";
525
- console.log(pc.yellow("No components or stories found."));
526
- console.log();
527
- console.log(pc.cyan("Let's create your first fragment!"));
620
+ console.log(pc.dim(` · No existing components found`));
528
621
  }
529
622
 
530
- console.log();
623
+ // Step 3: Detect framework
624
+ const framework = await detectSetupFramework(projectRoot);
625
+ console.log(pc.green(` ✓ Detected ${mapFrameworkLabel(framework)}`));
531
626
 
532
- // Step 3: Gather configuration (interactive unless --yes)
627
+ // Step 4: Branch interactive configure or fast path
533
628
  let componentPath = detection.suggestedComponentPath;
534
- let runScan = scenario === "components" || scenario === "stories";
535
629
  let createExample = scenario === "fresh";
536
630
  let startServer = false;
537
631
  let themeBlock = "";
538
632
  let snapshotsBlock = "";
539
633
 
540
- if (!options.yes) {
541
- // Ask about component location
542
- componentPath = await input({
543
- message: "Where are your components located?",
544
- default: detection.suggestedComponentPath,
545
- });
546
-
547
- if (scenario === "fresh") {
548
- // Fresh project - ask about example
549
- createExample = await confirm({
550
- message: "Create an example Button component to get started?",
551
- default: true,
552
- });
553
- }
554
-
555
- // Theme seed configuration
556
- const configureTheme = await confirm({
557
- message: "Configure theme seeds? (brand color, density, radius)",
558
- default: false,
559
- });
560
-
561
- if (configureTheme) {
562
- const brand = await input({
563
- message: "Brand color (hex)",
564
- default: "#18181b",
565
- validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) || "Enter a valid hex color (e.g., #6366f1)",
566
- });
567
-
568
- const neutral = await select({
569
- message: "Neutral palette",
570
- choices: [
571
- { value: "stone", name: "Stone (warm gray)" },
572
- { value: "ice", name: "Ice (cool blue-gray)" },
573
- { value: "earth", name: "Earth (olive/khaki)" },
574
- { value: "sand", name: "Sand (warm beige)" },
575
- { value: "fire", name: "Fire (warm red-gray)" },
576
- ],
577
- default: "stone",
578
- });
579
-
580
- const density = await select({
581
- message: "Spacing density",
582
- choices: [
583
- { value: "compact", name: "Compact (tighter spacing)" },
584
- { value: "default", name: "Default" },
585
- { value: "relaxed", name: "Relaxed (more breathing room)" },
586
- ],
587
- default: "default",
588
- });
589
-
590
- const radiusStyle = await select({
591
- message: "Border radius style",
592
- choices: [
593
- { value: "sharp", name: "Sharp (0px)" },
594
- { value: "subtle", name: "Subtle (2px)" },
595
- { value: "default", name: "Default (6px)" },
596
- { value: "rounded", name: "Rounded (10px)" },
597
- { value: "pill", name: "Pill (999px)" },
598
- ],
599
- default: "default",
600
- });
601
-
602
- // Build theme config block — only include non-default values
603
- const themeEntries: string[] = [];
604
- if (brand !== "#18181b") themeEntries.push(` brand: '${brand}'`);
605
- if (neutral !== "stone") themeEntries.push(` neutral: '${neutral}'`);
606
- if (density !== "default") themeEntries.push(` density: '${density}'`);
607
- if (radiusStyle !== "default") themeEntries.push(` radiusStyle: '${radiusStyle}'`);
608
-
609
- if (themeEntries.length > 0) {
610
- themeBlock = `\n // Theme seed values (derives 120+ CSS custom properties)\n theme: {\n${themeEntries.join(",\n")},\n },\n`;
611
- }
612
- }
613
-
614
- // Snapshot toggle
615
- const enableSnapshots = await confirm({
616
- message: "Enable visual snapshot tests per component variant?",
617
- default: false,
618
- });
619
-
620
- if (enableSnapshots) {
621
- snapshotsBlock = `\n // Visual snapshot testing\n snapshots: {\n enabled: true,\n },\n`;
622
- }
623
-
624
- // Ask about starting the server
625
- startServer = await confirm({
626
- message: "Start the viewer now?",
627
- default: true,
628
- });
634
+ if (options.configure) {
635
+ const config = await runInteractiveConfigure(detection, scenario);
636
+ componentPath = config.componentPath;
637
+ createExample = config.createExample;
638
+ startServer = config.startServer;
639
+ themeBlock = config.themeBlock;
640
+ snapshotsBlock = config.snapshotsBlock;
629
641
  }
630
642
 
631
- // Step 4: Create configuration
632
- console.log(pc.dim("\nCreating configuration...\n"));
633
-
634
- // Build include patterns
643
+ // Step 5: Create configuration file
635
644
  const includePaths: string[] = [
636
645
  `${componentPath}/**/*.fragment.tsx`,
637
646
  ];
638
647
 
639
- // If Storybook stories detected, also include them for direct rendering
640
648
  if (scenario === 'stories') {
641
649
  includePaths.push(`${componentPath}/**/*.stories.tsx`);
642
650
  includePaths.push(`${componentPath}/**/*.stories.ts`);
643
651
  }
644
652
 
645
- // Create config file
646
653
  const configPath = join(projectRoot, BRAND.configFile);
647
654
  const configContent = generateConfig({
648
655
  includePaths,
@@ -654,30 +661,71 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
654
661
 
655
662
  try {
656
663
  await writeFile(configPath, configContent, "utf-8");
657
- console.log(pc.green(`✓ Created ${BRAND.configFile}`));
664
+ console.log(pc.green(` ✓ Created ${BRAND.configFile}`));
658
665
  } catch (e) {
659
666
  errors.push(`Failed to create config: ${e}`);
660
667
  }
661
668
 
662
- // Step 5: Handle scenario-specific setup
669
+ // Step 6: Auto-inject styles + framework config
670
+ const entryFile = await findEntryFile(projectRoot, framework);
671
+
672
+ if (entryFile) {
673
+ try {
674
+ const stylesResult = await addStylesImport(projectRoot, entryFile);
675
+ if (stylesResult.modified) {
676
+ console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
677
+ } else {
678
+ console.log(pc.dim(` · ${stylesResult.message}`));
679
+ }
680
+ } catch (e) {
681
+ errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
682
+ }
683
+
684
+ try {
685
+ const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
686
+ if (providerResult.modified) {
687
+ console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
688
+ } else {
689
+ console.log(pc.dim(` · ${providerResult.message}`));
690
+ }
691
+ } catch (e) {
692
+ errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
693
+ }
694
+ } else {
695
+ console.log(pc.yellow(` ! Could not detect entry file — add styles import manually`));
696
+ console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
697
+ }
698
+
699
+ // Next.js: add transpilePackages
700
+ if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
701
+ try {
702
+ const transpileResult = await addTranspilePackages(projectRoot);
703
+ if (transpileResult.modified) {
704
+ console.log(pc.green(` ✓ ${transpileResult.message}`));
705
+ } else {
706
+ console.log(pc.dim(` · ${transpileResult.message}`));
707
+ }
708
+ } catch (e) {
709
+ errors.push(`Failed to update next.config: ${e instanceof Error ? e.message : e}`);
710
+ }
711
+ }
712
+
713
+ // Step 7: Handle scenario-specific setup
663
714
  if (scenario === "fresh" && createExample) {
664
- // Create example component
665
715
  const exampleDir = join(projectRoot, componentPath, "Button");
666
716
 
667
717
  try {
668
718
  await mkdir(exampleDir, { recursive: true });
669
719
 
670
- // Write Button.tsx
671
720
  await writeFile(
672
721
  join(exampleDir, "Button.tsx"),
673
722
  generateExampleComponent(),
674
723
  "utf-8"
675
724
  );
676
725
  console.log(
677
- pc.green(`✓ Created ${relative(projectRoot, join(exampleDir, "Button.tsx"))}`)
726
+ pc.green(` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.tsx"))}`)
678
727
  );
679
728
 
680
- // Write Button.fragment.tsx
681
729
  await writeFile(
682
730
  join(exampleDir, "Button.fragment.tsx"),
683
731
  generateExampleFragment(),
@@ -685,7 +733,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
685
733
  );
686
734
  console.log(
687
735
  pc.green(
688
- `✓ Created ${relative(projectRoot, join(exampleDir, "Button.fragment.tsx"))}`
736
+ ` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.fragment.tsx"))}`
689
737
  )
690
738
  );
691
739
  } catch (e) {
@@ -693,70 +741,50 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
693
741
  }
694
742
  }
695
743
 
696
- if (runScan) {
697
- // Run scan to generate fragments.json from source code
698
- console.log(pc.dim("\nScanning source code for documentation...\n"));
744
+ if (scenario === "components" || scenario === "stories") {
699
745
  try {
700
746
  const { scan } = await import("./scan.js");
701
- await scan({
747
+ const scanResult = await scan({
702
748
  config: configPath,
703
749
  verbose: false,
750
+ quiet: true,
704
751
  });
705
- } catch (e) {
752
+ if (scanResult.success) {
753
+ console.log(pc.green(` ✓ Generated fragments.json`) + pc.dim(` (${scanResult.componentCount} components)`));
754
+ } else {
755
+ console.log(pc.dim(` · Auto-documentation will run when you start the dev server`));
756
+ }
757
+ } catch {
706
758
  console.log(
707
- pc.yellow(`Note: Auto-documentation will run when you start the dev server.`)
759
+ pc.dim(` · Auto-documentation will run when you start the dev server`)
708
760
  );
709
761
  }
710
762
  }
711
763
 
712
- // Step 6: Framework-specific configuration
713
- console.log(pc.dim("\nConfiguring framework integration...\n"));
714
-
715
- const frameworkOverride = options.framework as Framework | undefined;
716
- const frameworkResult = await setupFramework({
717
- projectRoot,
718
- framework: frameworkOverride,
719
- });
720
-
721
- if (frameworkResult.filesCreated.length > 0) {
722
- for (const file of frameworkResult.filesCreated) {
723
- console.log(pc.green(`✓ Created ${file}`));
724
- }
725
- }
764
+ // Step 8: Summary
765
+ console.log();
726
766
 
727
- if (frameworkResult.configModified.length > 0) {
728
- for (const file of frameworkResult.configModified) {
729
- console.log(pc.green(`✓ Updated ${file}`));
767
+ if (errors.length > 0) {
768
+ console.log(pc.red(` ${errors.length} error(s) occurred:`));
769
+ for (const error of errors) {
770
+ console.log(pc.red(` - ${error}`));
730
771
  }
731
- }
732
-
733
- if (frameworkResult.packagesToInstall.length > 0) {
734
- const pkgs = frameworkResult.packagesToInstall.join(" ");
735
- console.log(
736
- pc.yellow(`\n⚠ Install required dependencies: `) +
737
- pc.bold(`pnpm add -D ${pkgs}`)
738
- );
739
- }
740
-
741
- for (const warning of frameworkResult.warnings) {
742
- console.log(pc.yellow(` Note: ${warning}`));
743
- }
744
-
745
- // Step 7: Show next steps or start server
746
- if (errors.length === 0) {
747
- console.log(pc.green("\n✓ Setup complete!\n"));
772
+ console.log();
773
+ } else {
774
+ console.log(pc.green(` Done!`) + pc.dim(` Setup complete.\n`));
748
775
 
749
776
  if (startServer) {
750
- console.log(pc.cyan("Starting viewer...\n"));
751
777
  startDevServer(projectRoot);
752
778
  } else {
753
- console.log(pc.cyan("Next steps:"));
754
- console.log(` 1. Run ${pc.bold(`${BRAND.cliCommand} dev`)} to start the viewer`);
755
- if (scenario === "fresh") {
756
- console.log(` 2. Edit ${pc.bold(`${componentPath}/Button/Button.fragment.tsx`)}`);
757
- }
758
- console.log(` 3. Run ${pc.bold(`${BRAND.cliCommand} generate`)} to create fragment files for your components`);
779
+ console.log(` ${pc.bold("Get started:")}`);
780
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} dev`);
759
781
  console.log();
782
+
783
+ if (!options.configure) {
784
+ console.log(` ${pc.bold("Customize:")} theme seeds, snapshots, and more`);
785
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} init --configure`);
786
+ console.log();
787
+ }
760
788
  }
761
789
  }
762
790