@hustle-together/api-dev-tools 3.12.16 → 4.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
package/bin/cli.js CHANGED
@@ -464,6 +464,162 @@ function parseFont(input, defaultFont) {
464
464
  return defaultFont;
465
465
  }
466
466
 
467
+ // ═══════════════════════════════════════════════════════════════════════════
468
+ // Brandfetch API Integration
469
+ // ═══════════════════════════════════════════════════════════════════════════
470
+
471
+ /**
472
+ * Fetch brand data from Brandfetch API
473
+ * @param {string} domain - Company domain (e.g., "stripe.com")
474
+ * @param {string} apiKey - Brandfetch API key
475
+ * @returns {Promise<object|null>} Brand data or null on error
476
+ */
477
+ async function fetchBrandData(domain, apiKey) {
478
+ const https = require("https");
479
+
480
+ return new Promise((resolve) => {
481
+ const options = {
482
+ hostname: "api.brandfetch.io",
483
+ path: `/v2/brands/${encodeURIComponent(domain)}`,
484
+ method: "GET",
485
+ headers: {
486
+ "Authorization": `Bearer ${apiKey}`,
487
+ "Content-Type": "application/json",
488
+ },
489
+ };
490
+
491
+ const req = https.request(options, (res) => {
492
+ let data = "";
493
+
494
+ res.on("data", (chunk) => {
495
+ data += chunk;
496
+ });
497
+
498
+ res.on("end", () => {
499
+ if (res.statusCode === 200) {
500
+ try {
501
+ const brand = JSON.parse(data);
502
+ resolve(brand);
503
+ } catch (e) {
504
+ resolve(null);
505
+ }
506
+ } else {
507
+ resolve(null);
508
+ }
509
+ });
510
+ });
511
+
512
+ req.on("error", () => {
513
+ resolve(null);
514
+ });
515
+
516
+ req.setTimeout(10000, () => {
517
+ req.destroy();
518
+ resolve(null);
519
+ });
520
+
521
+ req.end();
522
+ });
523
+ }
524
+
525
+ /**
526
+ * Parse Brandfetch API response into config-compatible format
527
+ * @param {object} brandData - Raw Brandfetch API response
528
+ * @returns {object} Parsed brand config
529
+ */
530
+ function parseBrandData(brandData) {
531
+ const result = {
532
+ brandName: brandData.name || brandData.domain || "Brand",
533
+ description: brandData.description || "",
534
+ primaryColor: "#E11D48",
535
+ secondaryColor: "#1E40AF",
536
+ accentColor: "#8B5CF6",
537
+ fontFamily: "Inter",
538
+ headingFont: "Inter",
539
+ logoUrl: null,
540
+ logoSvg: null,
541
+ iconUrl: null,
542
+ };
543
+
544
+ // Extract colors by type
545
+ if (brandData.colors && brandData.colors.length > 0) {
546
+ // Sort by type priority: brand > accent > dark > light
547
+ const brandColor = brandData.colors.find(c => c.type === "brand");
548
+ const accentColor = brandData.colors.find(c => c.type === "accent");
549
+ const darkColor = brandData.colors.find(c => c.type === "dark");
550
+ const lightColor = brandData.colors.find(c => c.type === "light");
551
+
552
+ // Primary = brand color or first color
553
+ if (brandColor) {
554
+ result.primaryColor = brandColor.hex.toUpperCase();
555
+ } else if (brandData.colors[0]) {
556
+ result.primaryColor = brandData.colors[0].hex.toUpperCase();
557
+ }
558
+
559
+ // Secondary = dark color or second color
560
+ if (darkColor) {
561
+ result.secondaryColor = darkColor.hex.toUpperCase();
562
+ } else if (brandData.colors[1]) {
563
+ result.secondaryColor = brandData.colors[1].hex.toUpperCase();
564
+ }
565
+
566
+ // Accent = accent color or third color
567
+ if (accentColor) {
568
+ result.accentColor = accentColor.hex.toUpperCase();
569
+ } else if (lightColor) {
570
+ result.accentColor = lightColor.hex.toUpperCase();
571
+ } else if (brandData.colors[2]) {
572
+ result.accentColor = brandData.colors[2].hex.toUpperCase();
573
+ }
574
+ }
575
+
576
+ // Extract fonts
577
+ if (brandData.fonts && brandData.fonts.length > 0) {
578
+ const titleFont = brandData.fonts.find(f => f.type === "title");
579
+ const bodyFont = brandData.fonts.find(f => f.type === "body");
580
+
581
+ if (bodyFont && bodyFont.name) {
582
+ result.fontFamily = bodyFont.name;
583
+ } else if (brandData.fonts[0] && brandData.fonts[0].name) {
584
+ result.fontFamily = brandData.fonts[0].name;
585
+ }
586
+
587
+ if (titleFont && titleFont.name) {
588
+ result.headingFont = titleFont.name;
589
+ } else {
590
+ result.headingFont = result.fontFamily;
591
+ }
592
+ }
593
+
594
+ // Extract logos - prefer SVG, then PNG
595
+ if (brandData.logos && brandData.logos.length > 0) {
596
+ // Find primary logo (type: "logo")
597
+ const primaryLogo = brandData.logos.find(l => l.type === "logo") || brandData.logos[0];
598
+ const icon = brandData.logos.find(l => l.type === "icon" || l.type === "symbol");
599
+
600
+ if (primaryLogo && primaryLogo.formats) {
601
+ // Prefer SVG
602
+ const svg = primaryLogo.formats.find(f => f.format === "svg");
603
+ const png = primaryLogo.formats.find(f => f.format === "png");
604
+
605
+ if (svg) {
606
+ result.logoSvg = svg.src;
607
+ result.logoUrl = svg.src;
608
+ } else if (png) {
609
+ result.logoUrl = png.src;
610
+ }
611
+ }
612
+
613
+ if (icon && icon.formats) {
614
+ const iconSvg = icon.formats.find(f => f.format === "svg");
615
+ const iconPng = icon.formats.find(f => f.format === "png");
616
+ result.iconUrl = iconSvg?.src || iconPng?.src || null;
617
+ }
618
+ }
619
+
620
+ return result;
621
+ }
622
+
467
623
  // ═══════════════════════════════════════════════════════════════════════════
468
624
  // File Copy Utilities
469
625
  // ═══════════════════════════════════════════════════════════════════════════
@@ -732,9 +888,21 @@ async function main() {
732
888
  { label: "Brandfetch - Auto-fetch from company domain (requires API key)", value: "brandfetch" },
733
889
  ]);
734
890
 
891
+ // Default values (may be overridden by Brandfetch)
892
+ let brandDefaults = {
893
+ brandName: path.basename(targetDir),
894
+ primaryColor: "#E11D48",
895
+ secondaryColor: "#1E40AF",
896
+ accentColor: "#8B5CF6",
897
+ fontFamily: "Inter",
898
+ headingFont: "Inter",
899
+ logoUrl: null,
900
+ iconUrl: null,
901
+ };
902
+
735
903
  if (config.brandSource === "brandfetch") {
736
904
  log(`\n${c.bold}Brandfetch Integration${c.reset}`);
737
- log(`${c.dim}Automatically pulls logos, colors, and fonts from any company domain${c.reset}\n`);
905
+ log(`${c.dim}Fetch brand assets to pre-populate the interview${c.reset}\n`);
738
906
 
739
907
  if (!config.brandfetchApiKey) {
740
908
  log(`${c.white}Get your free API key:${c.reset} https://brandfetch.com/developers`);
@@ -749,142 +917,189 @@ async function main() {
749
917
  default: "",
750
918
  });
751
919
 
752
- if (!config.brandDomain) {
753
- log(`\n${c.dim}No domain provided - falling back to manual interview${c.reset}`);
754
- config.brandSource = "manual";
920
+ if (config.brandDomain && config.brandfetchApiKey) {
921
+ // Actually fetch brand data!
922
+ startSpinner(`Fetching brand data from ${config.brandDomain}...`);
923
+ const brandData = await fetchBrandData(config.brandDomain, config.brandfetchApiKey);
924
+
925
+ if (brandData) {
926
+ const parsed = parseBrandData(brandData);
927
+ stopSpinner(true, `Fetched brand data from ${config.brandDomain}`);
928
+
929
+ // Update defaults with fetched data
930
+ brandDefaults = { ...brandDefaults, ...parsed };
931
+
932
+ log(`\n${c.bold}Brand Data Retrieved:${c.reset}`);
933
+ log(` ${c.white}Name:${c.reset} ${parsed.brandName}`);
934
+ log(` ${c.white}Primary:${c.reset} ${parsed.primaryColor}`);
935
+ log(` ${c.white}Secondary:${c.reset} ${parsed.secondaryColor}`);
936
+ log(` ${c.white}Accent:${c.reset} ${parsed.accentColor}`);
937
+ log(` ${c.white}Body Font:${c.reset} ${parsed.fontFamily}`);
938
+ log(` ${c.white}Heading Font:${c.reset} ${parsed.headingFont}`);
939
+ if (parsed.logoUrl) log(` ${c.white}Logo:${c.reset} ${c.dim}${parsed.logoUrl.substring(0, 50)}...${c.reset}`);
940
+
941
+ // Store logo URLs for brand guide
942
+ config.logoUrl = parsed.logoUrl;
943
+ config.iconUrl = parsed.iconUrl;
944
+
945
+ log(`\n${c.dim}These values will be used as defaults in the interview below.${c.reset}`);
946
+ log(`${c.dim}Press Enter to accept or type a different value.${c.reset}\n`);
947
+ } else {
948
+ stopSpinner(false, `Could not fetch brand data from ${config.brandDomain}`);
949
+ log(`${c.dim}Continuing with manual defaults...${c.reset}\n`);
950
+ }
951
+ } else if (!config.brandDomain) {
952
+ log(`\n${c.dim}No domain provided - using manual defaults${c.reset}`);
755
953
  }
756
954
  }
757
955
 
758
- if (config.brandSource === "manual") {
759
- log(`\n${c.bold}━━━ Brand Interview ━━━${c.reset}`);
956
+ // Full Brand Interview (with Brandfetch data as defaults if available)
957
+ log(`\n${c.bold}━━━ Brand Interview ━━━${c.reset}`);
958
+ if (config.brandSource === "brandfetch" && config.brandDomain) {
959
+ log(`${c.dim}Confirm or customize the fetched brand values${c.reset}\n`);
960
+ } else {
760
961
  log(`${c.dim}Let's define your brand's visual identity${c.reset}\n`);
962
+ }
761
963
 
762
- // Basic identity
763
- config.brandName = await textInput("Brand/Project name", {
764
- default: path.basename(targetDir),
765
- });
964
+ // Basic identity
965
+ config.brandName = await textInput("Brand/Project name", {
966
+ default: brandDefaults.brandName,
967
+ });
766
968
 
767
- // Color palette
768
- log(`\n${c.bold}Color Palette${c.reset}`);
769
- log(`${c.dim}Define colors that represent your brand${c.reset}`);
770
- log(`${c.dim}Enter hex (#E11D48) or color name (red, blue, coral, navy, etc.)${c.reset}\n`);
969
+ // Color palette
970
+ log(`\n${c.bold}Color Palette${c.reset}`);
971
+ log(`${c.dim}Define colors that represent your brand${c.reset}`);
972
+ log(`${c.dim}Enter hex (#E11D48) or color name (red, blue, coral, navy, etc.)${c.reset}\n`);
771
973
 
772
- let primaryInput = await textInput("Primary color (main CTAs, links)", {
773
- default: "#E11D48",
774
- hint: "hex or name",
775
- });
776
- config.primaryColor = parseColor(primaryInput, "#E11D48");
777
- if (primaryInput && primaryInput !== config.primaryColor) {
778
- log(` ${c.dim}→ Resolved to ${config.primaryColor}${c.reset}`);
779
- }
974
+ let primaryInput = await textInput("Primary color (main CTAs, links)", {
975
+ default: brandDefaults.primaryColor,
976
+ hint: "hex or name",
977
+ });
978
+ config.primaryColor = parseColor(primaryInput, brandDefaults.primaryColor);
979
+ if (primaryInput && primaryInput !== config.primaryColor) {
980
+ log(` ${c.dim}→ Resolved to ${config.primaryColor}${c.reset}`);
981
+ }
780
982
 
781
- let secondaryInput = await textInput("Secondary color (accents)", {
782
- default: "#1E40AF",
783
- hint: "hex or name",
784
- });
785
- config.secondaryColor = parseColor(secondaryInput, "#1E40AF");
786
- if (secondaryInput && secondaryInput !== config.secondaryColor) {
787
- log(` ${c.dim}→ Resolved to ${config.secondaryColor}${c.reset}`);
788
- }
983
+ let secondaryInput = await textInput("Secondary color (accents)", {
984
+ default: brandDefaults.secondaryColor,
985
+ hint: "hex or name",
986
+ });
987
+ config.secondaryColor = parseColor(secondaryInput, brandDefaults.secondaryColor);
988
+ if (secondaryInput && secondaryInput !== config.secondaryColor) {
989
+ log(` ${c.dim}→ Resolved to ${config.secondaryColor}${c.reset}`);
990
+ }
991
+
992
+ let accentInput = await textInput("Accent color (highlights, badges)", {
993
+ default: brandDefaults.accentColor,
994
+ hint: "hex or name",
995
+ });
996
+ config.accentColor = parseColor(accentInput, brandDefaults.accentColor);
997
+ if (accentInput && accentInput !== config.accentColor) {
998
+ log(` ${c.dim}→ Resolved to ${config.accentColor}${c.reset}`);
999
+ }
1000
+
1001
+ // Typography
1002
+ log(`\n${c.bold}Typography${c.reset}`);
1003
+ log(`${c.dim}Fonts define your brand's personality${c.reset}`);
1004
+ log(`${c.dim}Select from presets or describe what you want (e.g., "elegant serif", "modern sans")${c.reset}\n`);
1005
+
1006
+ // Build font options with fetched font as first option if available
1007
+ const fontOptions = [];
1008
+ if (brandDefaults.fontFamily && brandDefaults.fontFamily !== "Inter") {
1009
+ fontOptions.push({ label: `${brandDefaults.fontFamily} - From Brandfetch (Recommended)`, value: brandDefaults.fontFamily });
1010
+ }
1011
+ fontOptions.push(
1012
+ { label: "Inter - Clean, modern, highly readable", value: "Inter" },
1013
+ { label: "Geist - GitHub/Vercel aesthetic", value: "Geist" },
1014
+ { label: "Plus Jakarta Sans - Friendly, approachable", value: "Plus Jakarta Sans" },
1015
+ { label: "DM Sans - Geometric, professional", value: "DM Sans" },
1016
+ { label: "IBM Plex Sans - Technical, serious", value: "IBM Plex Sans" },
1017
+ { label: "Other - Describe or enter font name", value: "custom" },
1018
+ );
789
1019
 
790
- let accentInput = await textInput("Accent color (highlights, badges)", {
791
- default: "#8B5CF6",
792
- hint: "hex or name",
1020
+ config.fontFamily = await selectOne("Primary body font", fontOptions);
1021
+
1022
+ if (config.fontFamily === "custom") {
1023
+ let fontInput = await textInput("Font name or description", {
1024
+ default: brandDefaults.fontFamily,
1025
+ hint: 'e.g., "Playfair" or "elegant serif"',
793
1026
  });
794
- config.accentColor = parseColor(accentInput, "#8B5CF6");
795
- if (accentInput && accentInput !== config.accentColor) {
796
- log(` ${c.dim}→ Resolved to ${config.accentColor}${c.reset}`);
797
- }
1027
+ config.fontFamily = parseFont(fontInput, brandDefaults.fontFamily);
1028
+ log(` ${c.dim}→ Using: ${config.fontFamily}${c.reset}`);
1029
+ }
798
1030
 
799
- // Typography
800
- log(`\n${c.bold}Typography${c.reset}`);
801
- log(`${c.dim}Fonts define your brand's personality${c.reset}`);
802
- log(`${c.dim}Select from presets or describe what you want (e.g., "elegant serif", "modern sans")${c.reset}\n`);
803
-
804
- config.fontFamily = await selectOne("Primary body font", [
805
- { label: "Inter - Clean, modern, highly readable", value: "Inter" },
806
- { label: "Geist - GitHub/Vercel aesthetic", value: "Geist" },
807
- { label: "Plus Jakarta Sans - Friendly, approachable", value: "Plus Jakarta Sans" },
808
- { label: "DM Sans - Geometric, professional", value: "DM Sans" },
809
- { label: "IBM Plex Sans - Technical, serious", value: "IBM Plex Sans" },
810
- { label: "Other - Describe or enter font name", value: "custom" },
811
- ]);
812
-
813
- if (config.fontFamily === "custom") {
814
- let fontInput = await textInput("Font name or description", {
815
- default: "Inter",
816
- hint: 'e.g., "Playfair" or "elegant serif"',
817
- });
818
- config.fontFamily = parseFont(fontInput, "Inter");
819
- log(` ${c.dim}→ Using: ${config.fontFamily}${c.reset}`);
820
- }
1031
+ // Build heading font options with fetched font as first option if available
1032
+ const headingOptions = [];
1033
+ if (brandDefaults.headingFont && brandDefaults.headingFont !== config.fontFamily) {
1034
+ headingOptions.push({ label: `${brandDefaults.headingFont} - From Brandfetch (Recommended)`, value: brandDefaults.headingFont });
1035
+ }
1036
+ headingOptions.push(
1037
+ { label: "Same as body font", value: config.fontFamily },
1038
+ { label: "Playfair Display - Elegant, sophisticated", value: "Playfair Display" },
1039
+ { label: "Cal Sans - Bold, impactful", value: "Cal Sans" },
1040
+ { label: "Clash Display - Modern, striking", value: "Clash Display" },
1041
+ { label: "Other - Describe or enter font name", value: "custom" },
1042
+ );
821
1043
 
822
- config.headingFont = await selectOne("Heading font", [
823
- { label: "Same as body font", value: config.fontFamily },
824
- { label: "Playfair Display - Elegant, sophisticated", value: "Playfair Display" },
825
- { label: "Cal Sans - Bold, impactful", value: "Cal Sans" },
826
- { label: "Clash Display - Modern, striking", value: "Clash Display" },
827
- { label: "Other - Describe or enter font name", value: "custom" },
828
- ]);
829
-
830
- if (config.headingFont === "custom") {
831
- let headingInput = await textInput("Heading font name or description", {
832
- default: config.fontFamily,
833
- hint: 'e.g., "sans-serif that pairs nicely"',
834
- });
835
- config.headingFont = parseFont(headingInput, config.fontFamily);
836
- log(` ${c.dim}→ Using: ${config.headingFont}${c.reset}`);
837
- }
1044
+ config.headingFont = await selectOne("Heading font", headingOptions);
838
1045
 
839
- // UI Style preferences
840
- log(`\n${c.bold}UI Style Preferences${c.reset}`);
841
- log(`${c.dim}Define the overall look and feel${c.reset}\n`);
842
-
843
- config.buttonStyle = await selectOne("Button style", [
844
- { label: "Sharp (0px) - Modern, minimal tech aesthetic", value: "sharp" },
845
- { label: "Subtle (4px) - Professional, slightly softened", value: "subtle" },
846
- { label: "Rounded (8px) - Friendly, approachable", value: "rounded" },
847
- { label: "Pill (9999px) - Playful, fully rounded", value: "pill" },
848
- ]);
849
-
850
- // Map button style to border radius
851
- const radiusMap = { sharp: "0", subtle: "4px", rounded: "8px", pill: "9999px" };
852
- config.borderRadius = radiusMap[config.buttonStyle];
853
-
854
- config.cardStyle = await selectOne("Card style", [
855
- { label: "Flat - Minimal, no depth", value: "flat" },
856
- { label: "Bordered - Subtle outline separation", value: "bordered" },
857
- { label: "Elevated - Shadow for depth", value: "elevated" },
858
- ]);
859
-
860
- // Visual content style
861
- log(`\n${c.bold}Visual Content${c.reset}`);
862
- log(`${c.dim}Preferences for images and icons${c.reset}\n`);
863
-
864
- config.imageStyle = await selectOne("Preferred image style", [
865
- { label: "Photography - Real photos, authentic feel", value: "photography" },
866
- { label: "Illustrations - Custom drawn, unique personality", value: "illustration" },
867
- { label: "Abstract - Shapes, gradients, patterns", value: "abstract" },
868
- { label: "Minimal - Clean, simple graphics", value: "minimal" },
869
- ]);
870
-
871
- config.iconStyle = await selectOne("Icon style", [
872
- { label: "Outline - Light, modern (Lucide, Heroicons)", value: "outline" },
873
- { label: "Solid - Bold, impactful (Phosphor filled)", value: "solid" },
874
- { label: "Duotone - Two-tone, distinctive", value: "duotone" },
875
- ]);
876
-
877
- // Animation preferences
878
- config.animationLevel = await selectOne("Animation level", [
879
- { label: "None - Static UI, pure function", value: "none" },
880
- { label: "Subtle - Micro-interactions, fade-ins", value: "subtle" },
881
- { label: "Moderate - Page transitions, hovers", value: "moderate" },
882
- { label: "Expressive - Bold animations, personality", value: "expressive" },
883
- ]);
884
-
885
- // Dark mode
886
- config.darkMode = await confirm("Include dark mode support?", true);
1046
+ if (config.headingFont === "custom") {
1047
+ let headingInput = await textInput("Heading font name or description", {
1048
+ default: brandDefaults.headingFont,
1049
+ hint: 'e.g., "sans-serif that pairs nicely"',
1050
+ });
1051
+ config.headingFont = parseFont(headingInput, brandDefaults.headingFont);
1052
+ log(` ${c.dim}→ Using: ${config.headingFont}${c.reset}`);
887
1053
  }
1054
+
1055
+ // UI Style preferences
1056
+ log(`\n${c.bold}UI Style Preferences${c.reset}`);
1057
+ log(`${c.dim}Define the overall look and feel${c.reset}\n`);
1058
+
1059
+ config.buttonStyle = await selectOne("Button style", [
1060
+ { label: "Sharp (0px) - Modern, minimal tech aesthetic", value: "sharp" },
1061
+ { label: "Subtle (4px) - Professional, slightly softened", value: "subtle" },
1062
+ { label: "Rounded (8px) - Friendly, approachable", value: "rounded" },
1063
+ { label: "Pill (9999px) - Playful, fully rounded", value: "pill" },
1064
+ ]);
1065
+
1066
+ // Map button style to border radius
1067
+ const radiusMap = { sharp: "0", subtle: "4px", rounded: "8px", pill: "9999px" };
1068
+ config.borderRadius = radiusMap[config.buttonStyle];
1069
+
1070
+ config.cardStyle = await selectOne("Card style", [
1071
+ { label: "Flat - Minimal, no depth", value: "flat" },
1072
+ { label: "Bordered - Subtle outline separation", value: "bordered" },
1073
+ { label: "Elevated - Shadow for depth", value: "elevated" },
1074
+ ]);
1075
+
1076
+ // Visual content style
1077
+ log(`\n${c.bold}Visual Content${c.reset}`);
1078
+ log(`${c.dim}Preferences for images and icons${c.reset}\n`);
1079
+
1080
+ config.imageStyle = await selectOne("Preferred image style", [
1081
+ { label: "Photography - Real photos, authentic feel", value: "photography" },
1082
+ { label: "Illustrations - Custom drawn, unique personality", value: "illustration" },
1083
+ { label: "Abstract - Shapes, gradients, patterns", value: "abstract" },
1084
+ { label: "Minimal - Clean, simple graphics", value: "minimal" },
1085
+ ]);
1086
+
1087
+ config.iconStyle = await selectOne("Icon style", [
1088
+ { label: "Outline - Light, modern (Lucide, Heroicons)", value: "outline" },
1089
+ { label: "Solid - Bold, impactful (Phosphor filled)", value: "solid" },
1090
+ { label: "Duotone - Two-tone, distinctive", value: "duotone" },
1091
+ ]);
1092
+
1093
+ // Animation preferences
1094
+ config.animationLevel = await selectOne("Animation level", [
1095
+ { label: "None - Static UI, pure function", value: "none" },
1096
+ { label: "Subtle - Micro-interactions, fade-ins", value: "subtle" },
1097
+ { label: "Moderate - Page transitions, hovers", value: "moderate" },
1098
+ { label: "Expressive - Bold animations, personality", value: "expressive" },
1099
+ ]);
1100
+
1101
+ // Dark mode
1102
+ config.darkMode = await confirm("Include dark mode support?", true);
888
1103
  }
889
1104
  }
890
1105
 
@@ -1082,6 +1297,15 @@ async function main() {
1082
1297
  logSuccess("Created registry.json");
1083
1298
  }
1084
1299
 
1300
+ // Hustle-build defaults (for --auto mode)
1301
+ const defaultsSource = path.join(sourceTemplatesDir, "hustle-build-defaults.json");
1302
+ const defaultsDest = path.join(claudeDir, "hustle-build-defaults.json");
1303
+
1304
+ if (fs.existsSync(defaultsSource) && !fs.existsSync(defaultsDest)) {
1305
+ copyFile(defaultsSource, defaultsDest);
1306
+ logSuccess("Created hustle-build-defaults.json");
1307
+ }
1308
+
1085
1309
  // Research cache
1086
1310
  const researchDir = path.join(claudeDir, "research");
1087
1311
  if (!fs.existsSync(researchDir)) {
@@ -1289,6 +1513,17 @@ async function main() {
1289
1513
  ? `\n> **Source**: Auto-fetched from ${config.brandDomain} via Brandfetch API\n> Run \`/hustle-brand-refresh\` to update from latest brand assets`
1290
1514
  : "";
1291
1515
 
1516
+ // Build logo section if logos were fetched
1517
+ const logoSection = config.logoUrl
1518
+ ? `
1519
+ ### Logo Assets
1520
+ ${config.logoUrl ? `- **Primary Logo**: ${config.logoUrl}` : ""}
1521
+ ${config.iconUrl ? `- **Icon/Symbol**: ${config.iconUrl}` : ""}
1522
+
1523
+ > Download and place logos in \`/public/logo.svg\` and \`/public/icon.svg\`
1524
+ `
1525
+ : "";
1526
+
1292
1527
  const brandGuideContent = `# ${config.brandName} Brand Guide
1293
1528
 
1294
1529
  > Auto-generated by HUSTLE API Dev Tools v3.12.7
@@ -1300,6 +1535,7 @@ async function main() {
1300
1535
 
1301
1536
  ### Brand Name
1302
1537
  **${config.brandName}**
1538
+ ${logoSection}
1303
1539
 
1304
1540
  ### Brand Colors
1305
1541
  | Role | Color | Hex | Usage |