@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.
- package/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +10 -0
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/settings.local.json +1 -7
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +34 -20
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +365 -38
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +488 -0
- package/README.md +346 -53
- package/bin/cli.js +359 -123
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +97 -20
- package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
- package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
- package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
- package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
- package/{.claude/hooks → hooks}/hook_utils.py +0 -0
- package/hooks/ntfy-on-question.py +15 -2
- package/hooks/orchestrator-handoff.py +81 -3
- package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
- package/hooks/periodic-reground.py +40 -0
- package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
- package/hooks/run-code-review.py +176 -29
- package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
- package/package.json +1 -1
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +53 -1
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +74 -7
- package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/.claude/commands/hustle-combine.md +0 -1089
- package/.claude/commands/hustle-ui-create-page.md +0 -1078
- package/.claude/commands/hustle-ui-create.md +0 -1058
- package/.claude/hooks/auto-answer.py +0 -305
- package/.claude/hooks/cache-research.py +0 -337
- package/.claude/hooks/check-api-routes.py +0 -168
- package/.claude/hooks/check-playwright-setup.py +0 -103
- package/.claude/hooks/check-storybook-setup.py +0 -81
- package/.claude/hooks/check-update.py +0 -132
- package/.claude/hooks/detect-interruption.py +0 -165
- package/.claude/hooks/enforce-a11y-audit.py +0 -202
- package/.claude/hooks/enforce-brand-guide.py +0 -241
- package/.claude/hooks/enforce-component-type-confirm.py +0 -97
- package/.claude/hooks/enforce-freshness.py +0 -184
- package/.claude/hooks/enforce-page-components.py +0 -186
- package/.claude/hooks/enforce-page-data-schema.py +0 -155
- package/.claude/hooks/enforce-questions-sourced.py +0 -146
- package/.claude/hooks/enforce-schema-from-interview.py +0 -248
- package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
- package/.claude/hooks/enforce-ui-interview.py +0 -130
- package/.claude/hooks/generate-manifest-entry.py +0 -1161
- package/.claude/hooks/lib/__init__.py +0 -1
- package/.claude/hooks/lib/greptile.py +0 -355
- package/.claude/hooks/lib/ntfy.py +0 -209
- package/.claude/hooks/notify-input-needed.py +0 -73
- package/.claude/hooks/notify-phase-complete.py +0 -90
- package/.claude/hooks/ntfy-on-question.py +0 -240
- package/.claude/hooks/orchestrator-completion.py +0 -313
- package/.claude/hooks/orchestrator-handoff.py +0 -267
- package/.claude/hooks/orchestrator-session-startup.py +0 -146
- package/.claude/hooks/run-code-review.py +0 -393
- package/.claude/hooks/session-logger.py +0 -323
- package/.claude/hooks/test-orchestrator-reground.py +0 -248
- package/.claude/hooks/track-scope-coverage.py +0 -220
- package/.claude/hooks/track-token-usage.py +0 -121
- package/.claude/hooks/update-api-showcase.py +0 -161
- package/.claude/hooks/update-registry.py +0 -352
- package/.claude/hooks/update-ui-showcase.py +0 -224
- package/.claude/test-auto-answer-bot.py +0 -183
- package/.claude/test-completion-detector.py +0 -263
- package/.claude/test-orchestrator-state.json +0 -20
- package/.claude/test-orchestrator.sh +0 -271
- /package/{.claude/commands → commands}/hustle-build.md +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
- /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
- /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}
|
|
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 (
|
|
753
|
-
|
|
754
|
-
config.
|
|
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
|
-
|
|
759
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
964
|
+
// Basic identity
|
|
965
|
+
config.brandName = await textInput("Brand/Project name", {
|
|
966
|
+
default: brandDefaults.brandName,
|
|
967
|
+
});
|
|
766
968
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
1027
|
+
config.fontFamily = parseFont(fontInput, brandDefaults.fontFamily);
|
|
1028
|
+
log(` ${c.dim}→ Using: ${config.fontFamily}${c.reset}`);
|
|
1029
|
+
}
|
|
798
1030
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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 |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|