@hustle-together/api-dev-tools 3.12.3 → 4.5.1
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 +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- 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 +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- 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 +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- 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 +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- 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 +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
package/bin/cli.js
CHANGED
|
@@ -51,9 +51,10 @@ ${c.red} ╔═════════════════════
|
|
|
51
51
|
║ ║
|
|
52
52
|
╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
53
53
|
|
|
54
|
-
${c.bold}
|
|
54
|
+
${c.red}${c.bold} HUSTLE${c.reset}
|
|
55
|
+
${c.bold} API Dev Tools${c.reset}
|
|
55
56
|
${c.dim} Interview-driven, research-first API development${c.reset}
|
|
56
|
-
${c.gray}
|
|
57
|
+
${c.gray}v3.12.7${c.reset}
|
|
57
58
|
`;
|
|
58
59
|
|
|
59
60
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -112,6 +113,513 @@ function logError(message) {
|
|
|
112
113
|
console.log(` ${c.red}✗${c.reset} ${message}`);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
117
|
+
// Interactive Prompt Utilities
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Single-select with arrow keys
|
|
122
|
+
* @param {string} question - The question to ask
|
|
123
|
+
* @param {Array<{label: string, value: any}>} options - Options to choose from
|
|
124
|
+
* @returns {Promise<any>} - The selected value
|
|
125
|
+
*/
|
|
126
|
+
async function selectOne(question, options) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let selected = 0;
|
|
129
|
+
|
|
130
|
+
const renderOptions = () => {
|
|
131
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
132
|
+
console.log(`\n${c.bold}${question}${c.reset}`);
|
|
133
|
+
options.forEach((opt, i) => {
|
|
134
|
+
const prefix = i === selected ? `${c.red}❯${c.reset}` : " ";
|
|
135
|
+
const label =
|
|
136
|
+
i === selected ? `${c.bold}${opt.label}${c.reset}` : `${c.dim}${opt.label}${c.reset}`;
|
|
137
|
+
console.log(` ${prefix} ${label}`);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const clearOptions = () => {
|
|
142
|
+
process.stdout.write(`\x1B[${options.length + 2}A`); // Move up
|
|
143
|
+
for (let i = 0; i < options.length + 2; i++) {
|
|
144
|
+
process.stdout.write("\x1B[2K\n"); // Clear line
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(`\x1B[${options.length + 2}A`); // Move back up
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
renderOptions();
|
|
150
|
+
|
|
151
|
+
process.stdin.setRawMode(true);
|
|
152
|
+
process.stdin.resume();
|
|
153
|
+
process.stdin.setEncoding("utf8");
|
|
154
|
+
|
|
155
|
+
const onKeypress = (key) => {
|
|
156
|
+
if (key === "\u001B[A") {
|
|
157
|
+
// Up arrow
|
|
158
|
+
selected = selected > 0 ? selected - 1 : options.length - 1;
|
|
159
|
+
clearOptions();
|
|
160
|
+
renderOptions();
|
|
161
|
+
} else if (key === "\u001B[B") {
|
|
162
|
+
// Down arrow
|
|
163
|
+
selected = selected < options.length - 1 ? selected + 1 : 0;
|
|
164
|
+
clearOptions();
|
|
165
|
+
renderOptions();
|
|
166
|
+
} else if (key === "\r" || key === "\n") {
|
|
167
|
+
// Enter
|
|
168
|
+
process.stdin.setRawMode(false);
|
|
169
|
+
process.stdin.pause();
|
|
170
|
+
process.stdin.removeListener("data", onKeypress);
|
|
171
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
172
|
+
clearOptions();
|
|
173
|
+
console.log(
|
|
174
|
+
`\n${c.bold}${question}${c.reset} ${c.white}${options[selected].label}${c.reset}`,
|
|
175
|
+
);
|
|
176
|
+
resolve(options[selected].value);
|
|
177
|
+
} else if (key === "\u0003") {
|
|
178
|
+
// Ctrl+C
|
|
179
|
+
process.stdout.write("\x1B[?25h");
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
process.stdin.on("data", onKeypress);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Multi-select with checkboxes
|
|
190
|
+
* @param {string} question - The question to ask
|
|
191
|
+
* @param {Array<{label: string, value: any, checked?: boolean}>} options - Options to choose from
|
|
192
|
+
* @returns {Promise<Array<any>>} - Array of selected values
|
|
193
|
+
*/
|
|
194
|
+
async function selectMany(question, options) {
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
let cursor = 0;
|
|
197
|
+
const checked = options.map((opt) => opt.checked || false);
|
|
198
|
+
|
|
199
|
+
const renderOptions = () => {
|
|
200
|
+
process.stdout.write("\x1B[?25l");
|
|
201
|
+
console.log(`\n${c.bold}${question}${c.reset} ${c.dim}(space to toggle, enter to confirm)${c.reset}`);
|
|
202
|
+
options.forEach((opt, i) => {
|
|
203
|
+
const pointer = i === cursor ? `${c.red}❯${c.reset}` : " ";
|
|
204
|
+
const checkbox = checked[i] ? `${c.white}◉${c.reset}` : `${c.dim}○${c.reset}`;
|
|
205
|
+
const label = i === cursor ? `${c.bold}${opt.label}${c.reset}` : opt.label;
|
|
206
|
+
console.log(` ${pointer} ${checkbox} ${label}`);
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const clearOptions = () => {
|
|
211
|
+
process.stdout.write(`\x1B[${options.length + 2}A`);
|
|
212
|
+
for (let i = 0; i < options.length + 2; i++) {
|
|
213
|
+
process.stdout.write("\x1B[2K\n");
|
|
214
|
+
}
|
|
215
|
+
process.stdout.write(`\x1B[${options.length + 2}A`);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
renderOptions();
|
|
219
|
+
|
|
220
|
+
process.stdin.setRawMode(true);
|
|
221
|
+
process.stdin.resume();
|
|
222
|
+
process.stdin.setEncoding("utf8");
|
|
223
|
+
|
|
224
|
+
const onKeypress = (key) => {
|
|
225
|
+
if (key === "\u001B[A") {
|
|
226
|
+
cursor = cursor > 0 ? cursor - 1 : options.length - 1;
|
|
227
|
+
clearOptions();
|
|
228
|
+
renderOptions();
|
|
229
|
+
} else if (key === "\u001B[B") {
|
|
230
|
+
cursor = cursor < options.length - 1 ? cursor + 1 : 0;
|
|
231
|
+
clearOptions();
|
|
232
|
+
renderOptions();
|
|
233
|
+
} else if (key === " ") {
|
|
234
|
+
checked[cursor] = !checked[cursor];
|
|
235
|
+
clearOptions();
|
|
236
|
+
renderOptions();
|
|
237
|
+
} else if (key === "\r" || key === "\n") {
|
|
238
|
+
process.stdin.setRawMode(false);
|
|
239
|
+
process.stdin.pause();
|
|
240
|
+
process.stdin.removeListener("data", onKeypress);
|
|
241
|
+
process.stdout.write("\x1B[?25h");
|
|
242
|
+
clearOptions();
|
|
243
|
+
const selectedLabels = options
|
|
244
|
+
.filter((_, i) => checked[i])
|
|
245
|
+
.map((o) => o.label)
|
|
246
|
+
.join(", ");
|
|
247
|
+
console.log(`\n${c.bold}${question}${c.reset} ${c.white}${selectedLabels || "None"}${c.reset}`);
|
|
248
|
+
resolve(options.filter((_, i) => checked[i]).map((o) => o.value));
|
|
249
|
+
} else if (key === "\u0003") {
|
|
250
|
+
process.stdout.write("\x1B[?25h");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
process.stdin.on("data", onKeypress);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Text input prompt
|
|
261
|
+
* @param {string} question - The question to ask
|
|
262
|
+
* @param {Object} options - Options
|
|
263
|
+
* @param {string} options.default - Default value
|
|
264
|
+
* @param {boolean} options.mask - Mask the echoed value (for API keys)
|
|
265
|
+
* @param {string} options.hint - Additional hint text
|
|
266
|
+
* @returns {Promise<string>} - The entered text
|
|
267
|
+
*/
|
|
268
|
+
async function textInput(question, options = {}) {
|
|
269
|
+
const rl = readline.createInterface({
|
|
270
|
+
input: process.stdin,
|
|
271
|
+
output: process.stdout,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
const defaultHint = options.default
|
|
276
|
+
? ` ${c.dim}(${options.mask ? maskSecret(options.default) : options.default})${c.reset}`
|
|
277
|
+
: "";
|
|
278
|
+
const extraHint = options.hint ? ` ${c.dim}${options.hint}${c.reset}` : "";
|
|
279
|
+
rl.question(`${c.bold}${question}${c.reset}${defaultHint}${extraHint}: `, (answer) => {
|
|
280
|
+
rl.close();
|
|
281
|
+
const result = answer.trim() || options.default || "";
|
|
282
|
+
|
|
283
|
+
// If masked, show confirmation with masked value
|
|
284
|
+
if (options.mask && result) {
|
|
285
|
+
// Move cursor up and rewrite line with masked value
|
|
286
|
+
process.stdout.write(`\x1B[1A\x1B[2K`);
|
|
287
|
+
console.log(`${c.bold}${question}${c.reset}: ${c.dim}${maskSecret(result)}${c.reset}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
resolve(result);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Yes/No confirmation
|
|
297
|
+
* @param {string} question - The question to ask
|
|
298
|
+
* @param {boolean} defaultYes - Default to yes
|
|
299
|
+
* @returns {Promise<boolean>}
|
|
300
|
+
*/
|
|
301
|
+
async function confirm(question, defaultYes = true) {
|
|
302
|
+
const hint = defaultYes ? `${c.dim}(Y/n)${c.reset}` : `${c.dim}(y/N)${c.reset}`;
|
|
303
|
+
const answer = await textInput(`${question} ${hint}`, {});
|
|
304
|
+
if (!answer) return defaultYes;
|
|
305
|
+
return answer.toLowerCase().startsWith("y");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Progress bar
|
|
310
|
+
* @param {number} current - Current value
|
|
311
|
+
* @param {number} total - Total value
|
|
312
|
+
* @param {string} label - Label to show
|
|
313
|
+
*/
|
|
314
|
+
function progressBar(current, total, label = "") {
|
|
315
|
+
const width = 40;
|
|
316
|
+
const percent = Math.round((current / total) * 100);
|
|
317
|
+
const filled = Math.round((current / total) * width);
|
|
318
|
+
const empty = width - filled;
|
|
319
|
+
const bar = `${c.red}${"█".repeat(filled)}${c.gray}${"░".repeat(empty)}${c.reset}`;
|
|
320
|
+
process.stdout.write(`\r ${bar} ${percent}% ${label}`);
|
|
321
|
+
if (current === total) console.log();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
325
|
+
// Validation Utilities
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Mask sensitive values (show only last 4 chars)
|
|
330
|
+
*/
|
|
331
|
+
function maskSecret(value) {
|
|
332
|
+
if (!value || value.length < 8) return "****";
|
|
333
|
+
return "****" + value.slice(-4);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validate hex color format
|
|
338
|
+
*/
|
|
339
|
+
function isValidHex(color) {
|
|
340
|
+
return /^#[0-9A-Fa-f]{3,8}$/.test(color);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Color presets for quick selection
|
|
345
|
+
*/
|
|
346
|
+
const COLOR_PRESETS = {
|
|
347
|
+
// Reds
|
|
348
|
+
red: "#EF4444",
|
|
349
|
+
rose: "#F43F5E",
|
|
350
|
+
pink: "#EC4899",
|
|
351
|
+
// Blues
|
|
352
|
+
blue: "#3B82F6",
|
|
353
|
+
sky: "#0EA5E9",
|
|
354
|
+
cyan: "#06B6D4",
|
|
355
|
+
indigo: "#6366F1",
|
|
356
|
+
// Greens
|
|
357
|
+
green: "#22C55E",
|
|
358
|
+
emerald: "#10B981",
|
|
359
|
+
teal: "#14B8A6",
|
|
360
|
+
// Yellows/Oranges
|
|
361
|
+
yellow: "#EAB308",
|
|
362
|
+
amber: "#F59E0B",
|
|
363
|
+
orange: "#F97316",
|
|
364
|
+
// Purples
|
|
365
|
+
purple: "#A855F7",
|
|
366
|
+
violet: "#8B5CF6",
|
|
367
|
+
fuchsia: "#D946EF",
|
|
368
|
+
// Neutrals
|
|
369
|
+
slate: "#64748B",
|
|
370
|
+
gray: "#6B7280",
|
|
371
|
+
zinc: "#71717A",
|
|
372
|
+
stone: "#78716C",
|
|
373
|
+
// Special
|
|
374
|
+
black: "#000000",
|
|
375
|
+
white: "#FFFFFF",
|
|
376
|
+
beige: "#F5F5DC",
|
|
377
|
+
cream: "#FFFDD0",
|
|
378
|
+
navy: "#1E3A5A",
|
|
379
|
+
maroon: "#800000",
|
|
380
|
+
coral: "#FF7F50",
|
|
381
|
+
gold: "#FFD700",
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Parse color input - accepts hex or color name
|
|
386
|
+
*/
|
|
387
|
+
function parseColor(input, defaultColor) {
|
|
388
|
+
if (!input) return defaultColor;
|
|
389
|
+
const trimmed = input.trim().toLowerCase();
|
|
390
|
+
|
|
391
|
+
// Check if it's a valid hex
|
|
392
|
+
if (isValidHex(input.trim())) {
|
|
393
|
+
return input.trim().toUpperCase();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check presets
|
|
397
|
+
if (COLOR_PRESETS[trimmed]) {
|
|
398
|
+
return COLOR_PRESETS[trimmed];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Return default if invalid
|
|
402
|
+
return defaultColor;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Font presets with descriptions
|
|
407
|
+
*/
|
|
408
|
+
const FONT_PRESETS = {
|
|
409
|
+
// Sans-serif
|
|
410
|
+
inter: { name: "Inter", type: "sans-serif", desc: "Clean, modern" },
|
|
411
|
+
geist: { name: "Geist", type: "sans-serif", desc: "GitHub/Vercel style" },
|
|
412
|
+
"plus jakarta sans": { name: "Plus Jakarta Sans", type: "sans-serif", desc: "Friendly" },
|
|
413
|
+
"dm sans": { name: "DM Sans", type: "sans-serif", desc: "Geometric" },
|
|
414
|
+
"ibm plex sans": { name: "IBM Plex Sans", type: "sans-serif", desc: "Technical" },
|
|
415
|
+
poppins: { name: "Poppins", type: "sans-serif", desc: "Geometric, modern" },
|
|
416
|
+
montserrat: { name: "Montserrat", type: "sans-serif", desc: "Clean, elegant" },
|
|
417
|
+
// Serif
|
|
418
|
+
playfair: { name: "Playfair Display", type: "serif", desc: "Elegant, sophisticated" },
|
|
419
|
+
lora: { name: "Lora", type: "serif", desc: "Well-balanced" },
|
|
420
|
+
merriweather: { name: "Merriweather", type: "serif", desc: "Readable, warm" },
|
|
421
|
+
georgia: { name: "Georgia", type: "serif", desc: "Classic web serif" },
|
|
422
|
+
"source serif": { name: "Source Serif Pro", type: "serif", desc: "Modern serif" },
|
|
423
|
+
crimson: { name: "Crimson Text", type: "serif", desc: "Book-style elegance" },
|
|
424
|
+
// Monospace
|
|
425
|
+
"jetbrains mono": { name: "JetBrains Mono", type: "mono", desc: "Developer favorite" },
|
|
426
|
+
"fira code": { name: "Fira Code", type: "mono", desc: "Ligatures, readable" },
|
|
427
|
+
"source code": { name: "Source Code Pro", type: "mono", desc: "Adobe, clean" },
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parse font input - accepts font name or description
|
|
432
|
+
*/
|
|
433
|
+
function parseFont(input, defaultFont) {
|
|
434
|
+
if (!input) return defaultFont;
|
|
435
|
+
const trimmed = input.trim().toLowerCase();
|
|
436
|
+
|
|
437
|
+
// Check exact match in presets
|
|
438
|
+
if (FONT_PRESETS[trimmed]) {
|
|
439
|
+
return FONT_PRESETS[trimmed].name;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Check if input contains keywords
|
|
443
|
+
if (trimmed.includes("serif") && !trimmed.includes("sans")) {
|
|
444
|
+
// User wants a serif font
|
|
445
|
+
if (trimmed.includes("sophist") || trimmed.includes("elegant")) {
|
|
446
|
+
return "Playfair Display";
|
|
447
|
+
}
|
|
448
|
+
return "Crimson Text";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (trimmed.includes("sans") || trimmed.includes("clean") || trimmed.includes("modern")) {
|
|
452
|
+
return "Inter";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (trimmed.includes("mono") || trimmed.includes("code")) {
|
|
456
|
+
return "JetBrains Mono";
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// If it looks like a font name (capitalized words), use it directly
|
|
460
|
+
if (/^[A-Z][a-z]+(\s[A-Z][a-z]+)*$/.test(input.trim())) {
|
|
461
|
+
return input.trim();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return defaultFont;
|
|
465
|
+
}
|
|
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
|
+
|
|
115
623
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
624
|
// File Copy Utilities
|
|
117
625
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -119,8 +627,10 @@ function logError(message) {
|
|
|
119
627
|
function copyDir(src, dest, options = {}) {
|
|
120
628
|
const { filter = () => true, overwrite = false } = options;
|
|
121
629
|
let copied = 0;
|
|
630
|
+
let skipped = 0;
|
|
631
|
+
let total = 0;
|
|
122
632
|
|
|
123
|
-
if (!fs.existsSync(src)) return copied;
|
|
633
|
+
if (!fs.existsSync(src)) return { copied: 0, skipped: 0, total: 0 };
|
|
124
634
|
|
|
125
635
|
if (!fs.existsSync(dest)) {
|
|
126
636
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -135,16 +645,22 @@ function copyDir(src, dest, options = {}) {
|
|
|
135
645
|
if (!filter(entry.name, srcPath)) continue;
|
|
136
646
|
|
|
137
647
|
if (entry.isDirectory()) {
|
|
138
|
-
|
|
648
|
+
const result = copyDir(srcPath, destPath, options);
|
|
649
|
+
copied += result.copied;
|
|
650
|
+
skipped += result.skipped;
|
|
651
|
+
total += result.total;
|
|
139
652
|
} else {
|
|
653
|
+
total++;
|
|
140
654
|
if (!fs.existsSync(destPath) || overwrite) {
|
|
141
655
|
fs.copyFileSync(srcPath, destPath);
|
|
142
656
|
copied++;
|
|
657
|
+
} else {
|
|
658
|
+
skipped++;
|
|
143
659
|
}
|
|
144
660
|
}
|
|
145
661
|
}
|
|
146
662
|
|
|
147
|
-
return copied;
|
|
663
|
+
return { copied, skipped, total };
|
|
148
664
|
}
|
|
149
665
|
|
|
150
666
|
function copyFile(src, dest, options = {}) {
|
|
@@ -205,10 +721,8 @@ function checkNode() {
|
|
|
205
721
|
async function main() {
|
|
206
722
|
// Parse arguments
|
|
207
723
|
const args = process.argv.slice(2);
|
|
208
|
-
const withStorybook = args.includes("--with-storybook");
|
|
209
|
-
const withPlaywright = args.includes("--with-playwright");
|
|
210
|
-
const withSandpack = args.includes("--with-sandpack");
|
|
211
724
|
const silent = args.includes("--silent") || args.includes("-s");
|
|
725
|
+
const quickMode = args.includes("--quick") || args.includes("-q");
|
|
212
726
|
|
|
213
727
|
// Show banner
|
|
214
728
|
if (!silent) {
|
|
@@ -222,7 +736,377 @@ async function main() {
|
|
|
222
736
|
const claudeDir = path.join(targetDir, ".claude");
|
|
223
737
|
const hooksDir = path.join(claudeDir, "hooks");
|
|
224
738
|
|
|
225
|
-
|
|
739
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
740
|
+
// WIZARD STEP 1: Choose Setup Mode
|
|
741
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
let config = {
|
|
744
|
+
withStorybook: true,
|
|
745
|
+
withPlaywright: true,
|
|
746
|
+
withSandpack: true,
|
|
747
|
+
githubToken: "",
|
|
748
|
+
greptileApiKey: "",
|
|
749
|
+
brandfetchApiKey: "",
|
|
750
|
+
ntfyEnabled: false,
|
|
751
|
+
ntfyTopic: "",
|
|
752
|
+
ntfyServer: "ntfy.sh",
|
|
753
|
+
createBrandGuide: true,
|
|
754
|
+
brandSource: "manual", // "brandfetch" or "manual"
|
|
755
|
+
brandDomain: "",
|
|
756
|
+
brandName: path.basename(targetDir),
|
|
757
|
+
primaryColor: "#E11D48",
|
|
758
|
+
secondaryColor: "#1E40AF",
|
|
759
|
+
accentColor: "#8B5CF6",
|
|
760
|
+
successColor: "#10B981",
|
|
761
|
+
warningColor: "#F59E0B",
|
|
762
|
+
errorColor: "#EF4444",
|
|
763
|
+
fontFamily: "Inter",
|
|
764
|
+
headingFont: "Inter",
|
|
765
|
+
monoFont: "JetBrains Mono",
|
|
766
|
+
borderRadius: "8px",
|
|
767
|
+
darkMode: true,
|
|
768
|
+
imageStyle: "photography", // photography, illustration, abstract, minimal
|
|
769
|
+
iconStyle: "outline", // outline, solid, duotone
|
|
770
|
+
buttonStyle: "rounded", // sharp, subtle, rounded, pill
|
|
771
|
+
cardStyle: "elevated", // flat, bordered, elevated
|
|
772
|
+
animationLevel: "subtle", // none, subtle, moderate, expressive
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
if (!silent && !quickMode) {
|
|
776
|
+
const setupMode = await selectOne("Choose installation mode", [
|
|
777
|
+
{ label: "Quick Setup (recommended defaults)", value: "quick" },
|
|
778
|
+
{ label: "Custom Setup (configure each option)", value: "custom" },
|
|
779
|
+
]);
|
|
780
|
+
|
|
781
|
+
if (setupMode === "custom") {
|
|
782
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
783
|
+
// WIZARD STEP 2: API Keys
|
|
784
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
785
|
+
|
|
786
|
+
log(`\n${c.red}━━━ API Keys ━━━${c.reset}`);
|
|
787
|
+
log(`${c.dim}These keys enable advanced features like code review and brand fetching${c.reset}\n`);
|
|
788
|
+
log(`${c.bold}Get your keys (all have free tiers):${c.reset}\n`);
|
|
789
|
+
log(` ${c.white}GITHUB_TOKEN${c.reset}`);
|
|
790
|
+
log(` ${c.dim}→${c.reset} https://github.com/settings/tokens`);
|
|
791
|
+
log(` ${c.dim}Purpose: Create issues, PRs, search code${c.reset}`);
|
|
792
|
+
log(` ${c.dim}Scope needed: 'repo' for private repos${c.reset}\n`);
|
|
793
|
+
log(` ${c.white}GREPTILE_API_KEY${c.reset}`);
|
|
794
|
+
log(` ${c.dim}→${c.reset} https://app.greptile.com/settings/api`);
|
|
795
|
+
log(` ${c.dim}Purpose: AI code review in Phase 11${c.reset}`);
|
|
796
|
+
log(` ${c.dim}Free tier: 100 reviews/month${c.reset}\n`);
|
|
797
|
+
log(` ${c.white}BRANDFETCH_API_KEY${c.reset}`);
|
|
798
|
+
log(` ${c.dim}→${c.reset} https://brandfetch.com/developers`);
|
|
799
|
+
log(` ${c.dim}Purpose: Auto-fetch logos, colors, fonts from domains${c.reset}`);
|
|
800
|
+
log(` ${c.dim}Free tier: 50 requests/month (basic assets)${c.reset}\n`);
|
|
801
|
+
|
|
802
|
+
const configureKeys = await selectOne("Configure API keys now?", [
|
|
803
|
+
{ label: "Yes, enter them now", value: true },
|
|
804
|
+
{ label: "No, I'll add to .env later", value: false },
|
|
805
|
+
]);
|
|
806
|
+
|
|
807
|
+
if (configureKeys) {
|
|
808
|
+
config.githubToken = await textInput("GITHUB_TOKEN", {
|
|
809
|
+
default: process.env.GITHUB_TOKEN || "",
|
|
810
|
+
mask: true,
|
|
811
|
+
});
|
|
812
|
+
config.greptileApiKey = await textInput("GREPTILE_API_KEY", {
|
|
813
|
+
default: process.env.GREPTILE_API_KEY || "",
|
|
814
|
+
mask: true,
|
|
815
|
+
});
|
|
816
|
+
config.brandfetchApiKey = await textInput("BRANDFETCH_API_KEY (optional)", {
|
|
817
|
+
default: process.env.BRANDFETCH_API_KEY || "",
|
|
818
|
+
mask: true,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
823
|
+
// WIZARD STEP 3: Testing Tools
|
|
824
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
log(`\n${c.red}━━━ Testing Tools ━━━${c.reset}`);
|
|
827
|
+
log(`${c.dim}Required for component development and E2E testing${c.reset}\n`);
|
|
828
|
+
log(`${c.bold}What each tool does:${c.reset}`);
|
|
829
|
+
log(` ${c.white}Playwright${c.reset} → E2E browser testing (required for /hustle-ui-create-page)`);
|
|
830
|
+
log(` ${c.white}Storybook${c.reset} → Component development & visual testing (required for /hustle-ui-create)`);
|
|
831
|
+
log(` ${c.white}Sandpack${c.reset} → Live code preview in browser (optional)\n`);
|
|
832
|
+
|
|
833
|
+
const selectedTools = await selectMany("Select testing tools to install", [
|
|
834
|
+
{ label: "Playwright (E2E testing)", value: "playwright", checked: true },
|
|
835
|
+
{ label: "Storybook (component development)", value: "storybook", checked: true },
|
|
836
|
+
{ label: "Sandpack (live code preview)", value: "sandpack", checked: true },
|
|
837
|
+
]);
|
|
838
|
+
|
|
839
|
+
config.withPlaywright = selectedTools.includes("playwright");
|
|
840
|
+
config.withStorybook = selectedTools.includes("storybook");
|
|
841
|
+
config.withSandpack = selectedTools.includes("sandpack");
|
|
842
|
+
|
|
843
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
844
|
+
// WIZARD STEP 4: NTFY Notifications
|
|
845
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
846
|
+
|
|
847
|
+
log(`\n${c.red}━━━ Push Notifications (NTFY) ━━━${c.reset}`);
|
|
848
|
+
log(`${c.dim}Get notified on your phone when long tasks complete${c.reset}\n`);
|
|
849
|
+
log(`${c.bold}How it works:${c.reset}`);
|
|
850
|
+
log(` 1. Install NTFY app: ${c.white}iOS${c.reset} App Store / ${c.white}Android${c.reset} Play Store`);
|
|
851
|
+
log(` 2. Subscribe to your topic in the app`);
|
|
852
|
+
log(` 3. Receive push notifications when builds/tests finish\n`);
|
|
853
|
+
log(`${c.dim}Free service, no account required. Learn more: https://ntfy.sh${c.reset}\n`);
|
|
854
|
+
|
|
855
|
+
config.ntfyEnabled = await confirm("Enable NTFY push notifications?", false);
|
|
856
|
+
|
|
857
|
+
if (config.ntfyEnabled) {
|
|
858
|
+
log(`\n${c.dim}Topic name = channel for your notifications (must match the app)${c.reset}`);
|
|
859
|
+
config.ntfyTopic = await textInput("NTFY Topic name", {
|
|
860
|
+
default: path.basename(targetDir) + "-alerts",
|
|
861
|
+
});
|
|
862
|
+
log(`${c.dim}Server URL = ntfy.sh (public) or your self-hosted server${c.reset}`);
|
|
863
|
+
config.ntfyServer = await textInput("NTFY Server URL", {
|
|
864
|
+
default: "ntfy.sh",
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
869
|
+
// WIZARD STEP 5: Brand Guide
|
|
870
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
871
|
+
|
|
872
|
+
log(`\n${c.red}━━━ Brand Guide ━━━${c.reset}`);
|
|
873
|
+
log(`${c.dim}Design system that enforces consistent UI across all components${c.reset}\n`);
|
|
874
|
+
log(`${c.bold}Why you need a brand guide:${c.reset}`);
|
|
875
|
+
log(` • Consistent look across all pages and components`);
|
|
876
|
+
log(` • Faster development (no color/font decisions each time)`);
|
|
877
|
+
log(` • Enforced by hooks during /hustle-ui-create`);
|
|
878
|
+
log(` • Professional, cohesive user experience\n`);
|
|
879
|
+
|
|
880
|
+
config.createBrandGuide = await confirm("Create brand guide?", true);
|
|
881
|
+
|
|
882
|
+
if (config.createBrandGuide) {
|
|
883
|
+
// Brand source selection
|
|
884
|
+
log(`\n${c.bold}How would you like to create your brand guide?${c.reset}\n`);
|
|
885
|
+
|
|
886
|
+
config.brandSource = await selectOne("Brand guide source", [
|
|
887
|
+
{ label: "Manual Interview - Answer questions about your brand preferences", value: "manual" },
|
|
888
|
+
{ label: "Brandfetch - Auto-fetch from company domain (requires API key)", value: "brandfetch" },
|
|
889
|
+
]);
|
|
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
|
+
|
|
903
|
+
if (config.brandSource === "brandfetch") {
|
|
904
|
+
log(`\n${c.bold}Brandfetch Integration${c.reset}`);
|
|
905
|
+
log(`${c.dim}Fetch brand assets to pre-populate the interview${c.reset}\n`);
|
|
906
|
+
|
|
907
|
+
if (!config.brandfetchApiKey) {
|
|
908
|
+
log(`${c.white}Get your free API key:${c.reset} https://brandfetch.com/developers`);
|
|
909
|
+
log(`${c.dim}Free tier includes: 50 requests/month, basic brand assets${c.reset}\n`);
|
|
910
|
+
config.brandfetchApiKey = await textInput("BRANDFETCH_API_KEY", {
|
|
911
|
+
default: process.env.BRANDFETCH_API_KEY || "",
|
|
912
|
+
mask: true,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
config.brandDomain = await textInput("Company domain to fetch brand from (e.g., stripe.com)", {
|
|
917
|
+
default: "",
|
|
918
|
+
});
|
|
919
|
+
|
|
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}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
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 {
|
|
961
|
+
log(`${c.dim}Let's define your brand's visual identity${c.reset}\n`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Basic identity
|
|
965
|
+
config.brandName = await textInput("Brand/Project name", {
|
|
966
|
+
default: brandDefaults.brandName,
|
|
967
|
+
});
|
|
968
|
+
|
|
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`);
|
|
973
|
+
|
|
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
|
+
}
|
|
982
|
+
|
|
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
|
+
);
|
|
1019
|
+
|
|
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"',
|
|
1026
|
+
});
|
|
1027
|
+
config.fontFamily = parseFont(fontInput, brandDefaults.fontFamily);
|
|
1028
|
+
log(` ${c.dim}→ Using: ${config.fontFamily}${c.reset}`);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
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
|
+
);
|
|
1043
|
+
|
|
1044
|
+
config.headingFont = await selectOne("Heading font", headingOptions);
|
|
1045
|
+
|
|
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}`);
|
|
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);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
log(`\n${c.red}━━━ Installing ━━━${c.reset}\n`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const totalSteps = 10;
|
|
226
1110
|
let currentStep = 0;
|
|
227
1111
|
|
|
228
1112
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -230,6 +1114,7 @@ async function main() {
|
|
|
230
1114
|
// ─────────────────────────────────────────────────────────────────────────
|
|
231
1115
|
|
|
232
1116
|
logStep(++currentStep, totalSteps, "Checking prerequisites");
|
|
1117
|
+
log(` ${c.dim}Verifying Node.js 18+ and Python 3 are installed${c.reset}`);
|
|
233
1118
|
|
|
234
1119
|
const node = checkNode();
|
|
235
1120
|
if (node.ok) {
|
|
@@ -252,6 +1137,7 @@ async function main() {
|
|
|
252
1137
|
// ─────────────────────────────────────────────────────────────────────────
|
|
253
1138
|
|
|
254
1139
|
logStep(++currentStep, totalSteps, "Installing slash commands");
|
|
1140
|
+
log(` ${c.dim}Copying 29 commands including /hustle-api-create, /hustle-ui-create${c.reset}`);
|
|
255
1141
|
|
|
256
1142
|
const commandsDir = path.join(claudeDir, "commands");
|
|
257
1143
|
const sourceCommandsDir = path.join(packageDir, "commands");
|
|
@@ -260,17 +1146,26 @@ async function main() {
|
|
|
260
1146
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
261
1147
|
}
|
|
262
1148
|
|
|
263
|
-
const
|
|
1149
|
+
const commandsResult = copyDir(sourceCommandsDir, commandsDir, {
|
|
264
1150
|
filter: (name) => name.endsWith(".md"),
|
|
265
1151
|
});
|
|
266
1152
|
|
|
267
|
-
|
|
1153
|
+
if (commandsResult.copied > 0) {
|
|
1154
|
+
logSuccess(`${commandsResult.copied} new commands installed to .claude/commands/`);
|
|
1155
|
+
}
|
|
1156
|
+
if (commandsResult.skipped > 0) {
|
|
1157
|
+
logInfo(`${commandsResult.skipped} commands already exist (preserved)`);
|
|
1158
|
+
}
|
|
1159
|
+
if (commandsResult.total === 0) {
|
|
1160
|
+
logWarn("No commands found in package");
|
|
1161
|
+
}
|
|
268
1162
|
|
|
269
1163
|
// ─────────────────────────────────────────────────────────────────────────
|
|
270
1164
|
// Step 3: Install Hooks
|
|
271
1165
|
// ─────────────────────────────────────────────────────────────────────────
|
|
272
1166
|
|
|
273
1167
|
logStep(++currentStep, totalSteps, "Installing enforcement hooks");
|
|
1168
|
+
log(` ${c.dim}Python hooks that enforce TDD workflow and prevent skipping phases${c.reset}`);
|
|
274
1169
|
|
|
275
1170
|
const sourceHooksDir = path.join(packageDir, "hooks");
|
|
276
1171
|
|
|
@@ -291,11 +1186,15 @@ async function main() {
|
|
|
291
1186
|
|
|
292
1187
|
// Copy Python hook files
|
|
293
1188
|
let hooksCopied = 0;
|
|
1189
|
+
let hooksSkipped = 0;
|
|
1190
|
+
let hooksTotal = 0;
|
|
294
1191
|
if (fs.existsSync(sourceHooksDir)) {
|
|
295
1192
|
const hookFiles = fs
|
|
296
1193
|
.readdirSync(sourceHooksDir)
|
|
297
1194
|
.filter((f) => f.endsWith(".py"));
|
|
298
1195
|
|
|
1196
|
+
hooksTotal = hookFiles.length;
|
|
1197
|
+
|
|
299
1198
|
for (const file of hookFiles) {
|
|
300
1199
|
const src = path.join(sourceHooksDir, file);
|
|
301
1200
|
const dest = path.join(hooksDir, file);
|
|
@@ -304,11 +1203,18 @@ async function main() {
|
|
|
304
1203
|
fs.copyFileSync(src, dest);
|
|
305
1204
|
fs.chmodSync(dest, "755");
|
|
306
1205
|
hooksCopied++;
|
|
1206
|
+
} else {
|
|
1207
|
+
hooksSkipped++;
|
|
307
1208
|
}
|
|
308
1209
|
}
|
|
309
1210
|
}
|
|
310
1211
|
|
|
311
|
-
|
|
1212
|
+
if (hooksCopied > 0) {
|
|
1213
|
+
logSuccess(`${hooksCopied} new hooks installed to .claude/hooks/`);
|
|
1214
|
+
}
|
|
1215
|
+
if (hooksSkipped > 0) {
|
|
1216
|
+
logInfo(`${hooksSkipped} hooks already exist (preserved)`);
|
|
1217
|
+
}
|
|
312
1218
|
logInfo("Includes: enforce-*, notify-*, track-token-usage.py");
|
|
313
1219
|
|
|
314
1220
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -316,6 +1222,7 @@ async function main() {
|
|
|
316
1222
|
// ─────────────────────────────────────────────────────────────────────────
|
|
317
1223
|
|
|
318
1224
|
logStep(++currentStep, totalSteps, "Installing subagents");
|
|
1225
|
+
log(` ${c.dim}AI agents for parallel research, schema generation, code review${c.reset}`);
|
|
319
1226
|
|
|
320
1227
|
const agentsDir = path.join(claudeDir, "agents");
|
|
321
1228
|
const sourceAgentsDir = path.join(packageDir, ".claude", "agents");
|
|
@@ -324,11 +1231,16 @@ async function main() {
|
|
|
324
1231
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
325
1232
|
}
|
|
326
1233
|
|
|
327
|
-
const
|
|
1234
|
+
const agentsResult = copyDir(sourceAgentsDir, agentsDir, {
|
|
328
1235
|
filter: (name) => name.endsWith(".md"),
|
|
329
1236
|
});
|
|
330
1237
|
|
|
331
|
-
|
|
1238
|
+
if (agentsResult.copied > 0) {
|
|
1239
|
+
logSuccess(`${agentsResult.copied} new subagents installed to .claude/agents/`);
|
|
1240
|
+
}
|
|
1241
|
+
if (agentsResult.skipped > 0) {
|
|
1242
|
+
logInfo(`${agentsResult.skipped} subagents already exist (preserved)`);
|
|
1243
|
+
}
|
|
332
1244
|
logInfo("Haiku: parallel-researcher, research-validator, docs-generator");
|
|
333
1245
|
logInfo(
|
|
334
1246
|
"Sonnet: schema-generator, test-writer, implementation-reviewer, code-reviewer",
|
|
@@ -339,6 +1251,7 @@ async function main() {
|
|
|
339
1251
|
// ─────────────────────────────────────────────────────────────────────────
|
|
340
1252
|
|
|
341
1253
|
logStep(++currentStep, totalSteps, "Setting up configuration");
|
|
1254
|
+
log(` ${c.dim}Creating settings.json, state tracking, and research cache${c.reset}`);
|
|
342
1255
|
|
|
343
1256
|
const sourceTemplatesDir = path.join(packageDir, "templates");
|
|
344
1257
|
|
|
@@ -384,6 +1297,15 @@ async function main() {
|
|
|
384
1297
|
logSuccess("Created registry.json");
|
|
385
1298
|
}
|
|
386
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
|
+
|
|
387
1309
|
// Research cache
|
|
388
1310
|
const researchDir = path.join(claudeDir, "research");
|
|
389
1311
|
if (!fs.existsSync(researchDir)) {
|
|
@@ -400,6 +1322,7 @@ async function main() {
|
|
|
400
1322
|
// ─────────────────────────────────────────────────────────────────────────
|
|
401
1323
|
|
|
402
1324
|
logStep(++currentStep, totalSteps, "Setting up environment");
|
|
1325
|
+
log(` ${c.dim}Creating .env.example template for API keys${c.reset}`);
|
|
403
1326
|
|
|
404
1327
|
const templatesDestDir = path.join(targetDir, "templates");
|
|
405
1328
|
if (!fs.existsSync(templatesDestDir)) {
|
|
@@ -415,19 +1338,54 @@ async function main() {
|
|
|
415
1338
|
logInfo("Copy to .env and configure your API keys");
|
|
416
1339
|
}
|
|
417
1340
|
|
|
1341
|
+
// Copy showcase template directories (for update-api-showcase.py and update-ui-showcase.py hooks)
|
|
1342
|
+
const showcaseDirs = ["api-showcase", "ui-showcase", "shared"];
|
|
1343
|
+
for (const dir of showcaseDirs) {
|
|
1344
|
+
const srcDir = path.join(sourceTemplatesDir, dir);
|
|
1345
|
+
const destDir = path.join(templatesDestDir, dir);
|
|
1346
|
+
if (fs.existsSync(srcDir)) {
|
|
1347
|
+
const result = copyDir(srcDir, destDir);
|
|
1348
|
+
if (result.copied > 0) {
|
|
1349
|
+
logSuccess(`Copied ${dir} templates (${result.copied} files)`);
|
|
1350
|
+
} else if (result.skipped > 0) {
|
|
1351
|
+
logInfo(`${dir} templates already exist (${result.skipped} files preserved)`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
418
1356
|
// ─────────────────────────────────────────────────────────────────────────
|
|
419
1357
|
// Step 7: Configure MCP Servers
|
|
420
1358
|
// ─────────────────────────────────────────────────────────────────────────
|
|
421
1359
|
|
|
422
1360
|
logStep(++currentStep, totalSteps, "Configuring MCP servers");
|
|
1361
|
+
log(` ${c.dim}Setting up AI-powered integrations for research and code review${c.reset}`);
|
|
423
1362
|
|
|
424
1363
|
const mcpServers = [
|
|
425
|
-
{
|
|
426
|
-
|
|
1364
|
+
{
|
|
1365
|
+
name: "context7",
|
|
1366
|
+
cmd: "npx -y @upstash/context7-mcp",
|
|
1367
|
+
description: "Live documentation lookup (npm, APIs, frameworks)",
|
|
1368
|
+
required: true,
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
name: "github",
|
|
1372
|
+
cmd: "npx -y @modelcontextprotocol/server-github",
|
|
1373
|
+
description: "GitHub integration (issues, PRs, code search)",
|
|
1374
|
+
required: true,
|
|
1375
|
+
},
|
|
427
1376
|
{
|
|
428
1377
|
name: "greptile",
|
|
429
1378
|
cmd: "npx -y @anthropics/mcp-greptile",
|
|
1379
|
+
description: "AI code review for Phase 11 verification",
|
|
430
1380
|
optional: true,
|
|
1381
|
+
requiresKey: "GREPTILE_API_KEY",
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
name: "brandfetch",
|
|
1385
|
+
cmd: "npx -y @anthropics/mcp-brandfetch",
|
|
1386
|
+
description: "Auto-fetch brand assets (logos, colors, fonts)",
|
|
1387
|
+
optional: true,
|
|
1388
|
+
requiresKey: "BRANDFETCH_API_KEY",
|
|
431
1389
|
},
|
|
432
1390
|
];
|
|
433
1391
|
|
|
@@ -438,6 +1396,7 @@ async function main() {
|
|
|
438
1396
|
stdio: ["pipe", "pipe", "pipe"],
|
|
439
1397
|
});
|
|
440
1398
|
logInfo(`${server.name} already configured`);
|
|
1399
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
441
1400
|
} catch (e) {
|
|
442
1401
|
try {
|
|
443
1402
|
execSync(`claude mcp add ${server.name} -- ${server.cmd}`, {
|
|
@@ -445,11 +1404,11 @@ async function main() {
|
|
|
445
1404
|
stdio: ["pipe", "pipe", "pipe"],
|
|
446
1405
|
});
|
|
447
1406
|
logSuccess(`Added ${server.name}`);
|
|
1407
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
448
1408
|
} catch (addErr) {
|
|
449
1409
|
if (server.optional) {
|
|
450
|
-
logInfo(
|
|
451
|
-
|
|
452
|
-
);
|
|
1410
|
+
logInfo(`${server.name} (optional) - requires ${server.requiresKey}`);
|
|
1411
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
453
1412
|
} else {
|
|
454
1413
|
logWarn(`Could not add ${server.name} - add manually`);
|
|
455
1414
|
}
|
|
@@ -457,99 +1416,590 @@ async function main() {
|
|
|
457
1416
|
}
|
|
458
1417
|
}
|
|
459
1418
|
|
|
460
|
-
|
|
1419
|
+
log(`\n ${c.bold}MCP Server Benefits:${c.reset}`);
|
|
1420
|
+
log(` ${c.dim}• Context7: Always get latest docs, no hallucinated APIs${c.reset}`);
|
|
1421
|
+
log(` ${c.dim}• GitHub: Create issues/PRs directly from Claude${c.reset}`);
|
|
1422
|
+
log(` ${c.dim}• Greptile: AI-powered code review catches bugs before merge${c.reset}`);
|
|
1423
|
+
log(` ${c.dim}• Brandfetch: Auto-generate brand guide from company domain${c.reset}`);
|
|
461
1424
|
|
|
462
1425
|
// ─────────────────────────────────────────────────────────────────────────
|
|
463
|
-
// Step 8:
|
|
1426
|
+
// Step 8: Create .env file with API keys (if provided)
|
|
464
1427
|
// ─────────────────────────────────────────────────────────────────────────
|
|
465
1428
|
|
|
466
|
-
logStep(++currentStep, totalSteps, "
|
|
1429
|
+
logStep(++currentStep, totalSteps, "Configuring environment");
|
|
1430
|
+
log(` ${c.dim}Writing API keys and notification settings to .env${c.reset}`);
|
|
467
1431
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1432
|
+
const envPath = path.join(targetDir, ".env");
|
|
1433
|
+
let envContent = "";
|
|
1434
|
+
|
|
1435
|
+
if (fs.existsSync(envPath)) {
|
|
1436
|
+
envContent = fs.readFileSync(envPath, "utf8");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
let envUpdated = false;
|
|
1440
|
+
|
|
1441
|
+
if (config.githubToken && !envContent.includes("GITHUB_TOKEN=")) {
|
|
1442
|
+
envContent += `\nGITHUB_TOKEN=${config.githubToken}`;
|
|
1443
|
+
envUpdated = true;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (config.greptileApiKey && !envContent.includes("GREPTILE_API_KEY=")) {
|
|
1447
|
+
envContent += `\nGREPTILE_API_KEY=${config.greptileApiKey}`;
|
|
1448
|
+
envUpdated = true;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (config.brandfetchApiKey && !envContent.includes("BRANDFETCH_API_KEY=")) {
|
|
1452
|
+
envContent += `\nBRANDFETCH_API_KEY=${config.brandfetchApiKey}`;
|
|
1453
|
+
envUpdated = true;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (config.ntfyEnabled) {
|
|
1457
|
+
if (!envContent.includes("NTFY_TOPIC=")) {
|
|
1458
|
+
envContent += `\nNTFY_TOPIC=${config.ntfyTopic}`;
|
|
1459
|
+
envUpdated = true;
|
|
1460
|
+
}
|
|
1461
|
+
if (!envContent.includes("NTFY_SERVER=")) {
|
|
1462
|
+
envContent += `\nNTFY_SERVER=${config.ntfyServer}`;
|
|
1463
|
+
envUpdated = true;
|
|
483
1464
|
}
|
|
1465
|
+
}
|
|
484
1466
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1467
|
+
if (envUpdated) {
|
|
1468
|
+
fs.writeFileSync(envPath, envContent.trim() + "\n");
|
|
1469
|
+
logSuccess("Updated .env with API keys");
|
|
1470
|
+
} else {
|
|
1471
|
+
logInfo("No API keys to add (configure in .env later)");
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1475
|
+
// Step 9: Create Brand Guide (if enabled)
|
|
1476
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1477
|
+
|
|
1478
|
+
logStep(++currentStep, totalSteps, "Brand guide");
|
|
1479
|
+
log(` ${c.dim}Creating comprehensive design system documentation${c.reset}`);
|
|
1480
|
+
|
|
1481
|
+
const brandGuidePath = path.join(claudeDir, "BRAND_GUIDE.md");
|
|
1482
|
+
|
|
1483
|
+
if (config.createBrandGuide && !fs.existsSync(brandGuidePath)) {
|
|
1484
|
+
const darkModeSection = config.darkMode
|
|
1485
|
+
? `
|
|
1486
|
+
|
|
1487
|
+
## Dark Mode
|
|
1488
|
+
|
|
1489
|
+
### Dark Theme Colors
|
|
1490
|
+
- **Background**: #0F172A
|
|
1491
|
+
- **Surface**: #1E293B
|
|
1492
|
+
- **Border**: #334155
|
|
1493
|
+
- **Text**: #F8FAFC
|
|
1494
|
+
- **Muted**: #94A3B8
|
|
1495
|
+
|
|
1496
|
+
### Implementation
|
|
1497
|
+
\`\`\`css
|
|
1498
|
+
@media (prefers-color-scheme: dark) {
|
|
1499
|
+
:root {
|
|
1500
|
+
--bg: #0F172A;
|
|
1501
|
+
--surface: #1E293B;
|
|
1502
|
+
--border: #334155;
|
|
1503
|
+
--text: #F8FAFC;
|
|
1504
|
+
--muted: #94A3B8;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
\`\`\`
|
|
1508
|
+
`
|
|
1509
|
+
: "";
|
|
1510
|
+
|
|
1511
|
+
// Build brandfetch note if using domain
|
|
1512
|
+
const brandfetchNote = config.brandSource === "brandfetch" && config.brandDomain
|
|
1513
|
+
? `\n> **Source**: Auto-fetched from ${config.brandDomain} via Brandfetch API\n> Run \`/hustle-brand-refresh\` to update from latest brand assets`
|
|
1514
|
+
: "";
|
|
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
|
+
|
|
1527
|
+
const brandGuideContent = `# ${config.brandName} Brand Guide
|
|
1528
|
+
|
|
1529
|
+
> Auto-generated by HUSTLE API Dev Tools v3.12.7
|
|
1530
|
+
> This guide ensures consistent UI across all components and pages.${brandfetchNote}
|
|
1531
|
+
|
|
1532
|
+
---
|
|
1533
|
+
|
|
1534
|
+
## Brand Identity
|
|
1535
|
+
|
|
1536
|
+
### Brand Name
|
|
1537
|
+
**${config.brandName}**
|
|
1538
|
+
${logoSection}
|
|
1539
|
+
|
|
1540
|
+
### Brand Colors
|
|
1541
|
+
| Role | Color | Hex | Usage |
|
|
1542
|
+
|------|-------|-----|-------|
|
|
1543
|
+
| Primary | 🔴 | \`${config.primaryColor}\` | CTAs, links, focus states |
|
|
1544
|
+
| Secondary | 🔵 | \`${config.secondaryColor}\` | Secondary actions, accents |
|
|
1545
|
+
| Accent | 🟣 | \`${config.accentColor}\` | Highlights, badges, special elements |
|
|
1546
|
+
| Success | 🟢 | \`${config.successColor}\` | Success states, confirmations |
|
|
1547
|
+
| Warning | 🟡 | \`${config.warningColor}\` | Warnings, pending states |
|
|
1548
|
+
| Error | 🔴 | \`${config.errorColor}\` | Errors, destructive actions |
|
|
1549
|
+
|
|
1550
|
+
---
|
|
1551
|
+
|
|
1552
|
+
## Color Palette
|
|
1553
|
+
|
|
1554
|
+
### Primary Colors
|
|
1555
|
+
\`\`\`css
|
|
1556
|
+
--primary: ${config.primaryColor};
|
|
1557
|
+
--primary-light: ${config.primaryColor}20; /* 20% opacity */
|
|
1558
|
+
--primary-dark: ${config.primaryColor};
|
|
1559
|
+
\`\`\`
|
|
1560
|
+
|
|
1561
|
+
### Secondary Colors
|
|
1562
|
+
\`\`\`css
|
|
1563
|
+
--secondary: ${config.secondaryColor};
|
|
1564
|
+
--secondary-light: ${config.secondaryColor}20;
|
|
1565
|
+
--secondary-dark: ${config.secondaryColor};
|
|
1566
|
+
\`\`\`
|
|
1567
|
+
|
|
1568
|
+
### Accent Colors
|
|
1569
|
+
\`\`\`css
|
|
1570
|
+
--accent: ${config.accentColor};
|
|
1571
|
+
--accent-light: ${config.accentColor}20;
|
|
1572
|
+
--accent-dark: ${config.accentColor};
|
|
1573
|
+
\`\`\`
|
|
1574
|
+
|
|
1575
|
+
### Neutral Colors (Light Theme)
|
|
1576
|
+
\`\`\`css
|
|
1577
|
+
--background: #FFFFFF;
|
|
1578
|
+
--surface: #F9FAFB;
|
|
1579
|
+
--border: #E5E7EB;
|
|
1580
|
+
--text: #111827;
|
|
1581
|
+
--text-muted: #6B7280;
|
|
1582
|
+
--text-light: #9CA3AF;
|
|
1583
|
+
\`\`\`
|
|
1584
|
+
|
|
1585
|
+
### Semantic Colors
|
|
1586
|
+
\`\`\`css
|
|
1587
|
+
--success: ${config.successColor};
|
|
1588
|
+
--success-light: ${config.successColor}20;
|
|
1589
|
+
--warning: ${config.warningColor};
|
|
1590
|
+
--warning-light: ${config.warningColor}20;
|
|
1591
|
+
--error: ${config.errorColor};
|
|
1592
|
+
--error-light: ${config.errorColor}20;
|
|
1593
|
+
--info: #3B82F6;
|
|
1594
|
+
--info-light: #DBEAFE;
|
|
1595
|
+
\`\`\`
|
|
1596
|
+
${darkModeSection}
|
|
1597
|
+
---
|
|
1598
|
+
|
|
1599
|
+
## Typography
|
|
1600
|
+
|
|
1601
|
+
### Font Families
|
|
1602
|
+
\`\`\`css
|
|
1603
|
+
--font-primary: "${config.fontFamily}", system-ui, -apple-system, sans-serif;
|
|
1604
|
+
--font-heading: "${config.headingFont}", system-ui, -apple-system, sans-serif;
|
|
1605
|
+
--font-mono: "${config.monoFont}", "Fira Code", monospace;
|
|
1606
|
+
\`\`\`
|
|
1607
|
+
|
|
1608
|
+
### Font Scale
|
|
1609
|
+
| Name | Size | Line Height | Usage |
|
|
1610
|
+
|------|------|-------------|-------|
|
|
1611
|
+
| xs | 0.75rem (12px) | 1rem | Labels, captions |
|
|
1612
|
+
| sm | 0.875rem (14px) | 1.25rem | Secondary text |
|
|
1613
|
+
| base | 1rem (16px) | 1.5rem | Body text |
|
|
1614
|
+
| lg | 1.125rem (18px) | 1.75rem | Lead paragraphs |
|
|
1615
|
+
| xl | 1.25rem (20px) | 1.75rem | H4 |
|
|
1616
|
+
| 2xl | 1.5rem (24px) | 2rem | H3 |
|
|
1617
|
+
| 3xl | 1.875rem (30px) | 2.25rem | H2 |
|
|
1618
|
+
| 4xl | 2.25rem (36px) | 2.5rem | H1 |
|
|
1619
|
+
|
|
1620
|
+
### Font Weights
|
|
1621
|
+
- **Regular**: 400 - Body text
|
|
1622
|
+
- **Medium**: 500 - Emphasis
|
|
1623
|
+
- **Semibold**: 600 - Headings
|
|
1624
|
+
- **Bold**: 700 - Strong emphasis
|
|
1625
|
+
|
|
1626
|
+
---
|
|
1627
|
+
|
|
1628
|
+
## Spacing
|
|
1629
|
+
|
|
1630
|
+
### Spacing Scale (Tailwind-compatible)
|
|
1631
|
+
| Token | Value | Pixels | Usage |
|
|
1632
|
+
|-------|-------|--------|-------|
|
|
1633
|
+
| 1 | 0.25rem | 4px | Tight spacing |
|
|
1634
|
+
| 2 | 0.5rem | 8px | Default gap |
|
|
1635
|
+
| 3 | 0.75rem | 12px | - |
|
|
1636
|
+
| 4 | 1rem | 16px | Section padding |
|
|
1637
|
+
| 6 | 1.5rem | 24px | Card padding |
|
|
1638
|
+
| 8 | 2rem | 32px | Section gaps |
|
|
1639
|
+
| 12 | 3rem | 48px | Large sections |
|
|
1640
|
+
| 16 | 4rem | 64px | Page sections |
|
|
1641
|
+
|
|
1642
|
+
---
|
|
1643
|
+
|
|
1644
|
+
## Border Radius
|
|
1645
|
+
|
|
1646
|
+
### Radius Scale
|
|
1647
|
+
\`\`\`css
|
|
1648
|
+
--radius-none: 0;
|
|
1649
|
+
--radius-sm: 2px;
|
|
1650
|
+
--radius-default: ${config.borderRadius};
|
|
1651
|
+
--radius-md: 6px;
|
|
1652
|
+
--radius-lg: 8px;
|
|
1653
|
+
--radius-xl: 12px;
|
|
1654
|
+
--radius-2xl: 16px;
|
|
1655
|
+
--radius-full: 9999px;
|
|
1656
|
+
\`\`\`
|
|
1657
|
+
|
|
1658
|
+
### Usage Guidelines
|
|
1659
|
+
- **Buttons**: Use \`--radius-default\` (${config.borderRadius})
|
|
1660
|
+
- **Cards**: Use \`--radius-lg\` or \`--radius-xl\`
|
|
1661
|
+
- **Inputs**: Use \`--radius-default\`
|
|
1662
|
+
- **Avatars**: Use \`--radius-full\`
|
|
1663
|
+
- **Modals**: Use \`--radius-xl\`
|
|
1664
|
+
|
|
1665
|
+
---
|
|
1666
|
+
|
|
1667
|
+
## Shadows
|
|
1668
|
+
|
|
1669
|
+
### Shadow Scale
|
|
1670
|
+
\`\`\`css
|
|
1671
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
1672
|
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
1673
|
+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1674
|
+
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
|
1675
|
+
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
|
|
1676
|
+
\`\`\`
|
|
1677
|
+
|
|
1678
|
+
---
|
|
1679
|
+
|
|
1680
|
+
## Component Patterns
|
|
1681
|
+
|
|
1682
|
+
### Buttons
|
|
1683
|
+
|
|
1684
|
+
#### Primary Button
|
|
1685
|
+
\`\`\`jsx
|
|
1686
|
+
<button className="bg-primary text-white px-4 py-2 rounded-[${config.borderRadius}] font-medium
|
|
1687
|
+
hover:opacity-90 focus:ring-2 focus:ring-primary focus:ring-offset-2
|
|
1688
|
+
disabled:opacity-50 disabled:cursor-not-allowed">
|
|
1689
|
+
Primary Action
|
|
1690
|
+
</button>
|
|
1691
|
+
\`\`\`
|
|
1692
|
+
|
|
1693
|
+
#### Secondary Button
|
|
1694
|
+
\`\`\`jsx
|
|
1695
|
+
<button className="bg-surface border border-border text-text px-4 py-2 rounded-[${config.borderRadius}]
|
|
1696
|
+
hover:bg-gray-100 focus:ring-2 focus:ring-gray-200">
|
|
1697
|
+
Secondary
|
|
1698
|
+
</button>
|
|
1699
|
+
\`\`\`
|
|
1700
|
+
|
|
1701
|
+
#### Ghost Button
|
|
1702
|
+
\`\`\`jsx
|
|
1703
|
+
<button className="text-text px-4 py-2 rounded-[${config.borderRadius}]
|
|
1704
|
+
hover:bg-surface focus:ring-2 focus:ring-gray-200">
|
|
1705
|
+
Ghost
|
|
1706
|
+
</button>
|
|
1707
|
+
\`\`\`
|
|
1708
|
+
|
|
1709
|
+
#### Destructive Button
|
|
1710
|
+
\`\`\`jsx
|
|
1711
|
+
<button className="bg-error text-white px-4 py-2 rounded-[${config.borderRadius}]
|
|
1712
|
+
hover:bg-red-600 focus:ring-2 focus:ring-error">
|
|
1713
|
+
Delete
|
|
1714
|
+
</button>
|
|
1715
|
+
\`\`\`
|
|
1716
|
+
|
|
1717
|
+
### Cards
|
|
1718
|
+
|
|
1719
|
+
\`\`\`jsx
|
|
1720
|
+
<div className="bg-white border border-border rounded-xl p-6 shadow-sm">
|
|
1721
|
+
<h3 className="text-lg font-semibold text-text">Card Title</h3>
|
|
1722
|
+
<p className="text-muted text-sm mt-2">Card description text.</p>
|
|
1723
|
+
</div>
|
|
1724
|
+
\`\`\`
|
|
1725
|
+
|
|
1726
|
+
### Form Inputs
|
|
1727
|
+
|
|
1728
|
+
\`\`\`jsx
|
|
1729
|
+
<div>
|
|
1730
|
+
<label className="text-sm font-medium text-text mb-1 block">
|
|
1731
|
+
Label
|
|
1732
|
+
</label>
|
|
1733
|
+
<input
|
|
1734
|
+
type="text"
|
|
1735
|
+
className="w-full border border-border rounded-[${config.borderRadius}] px-3 py-2
|
|
1736
|
+
focus:ring-2 focus:ring-primary focus:border-primary
|
|
1737
|
+
placeholder:text-muted"
|
|
1738
|
+
placeholder="Enter value..."
|
|
1739
|
+
/>
|
|
1740
|
+
<p className="text-error text-sm mt-1">Error message</p>
|
|
1741
|
+
</div>
|
|
1742
|
+
\`\`\`
|
|
1743
|
+
|
|
1744
|
+
### Badges
|
|
1745
|
+
|
|
1746
|
+
\`\`\`jsx
|
|
1747
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
1748
|
+
bg-primary-light text-primary">
|
|
1749
|
+
Badge
|
|
1750
|
+
</span>
|
|
1751
|
+
\`\`\`
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1755
|
+
## Accessibility
|
|
1756
|
+
|
|
1757
|
+
### Color Contrast
|
|
1758
|
+
- Text on background: minimum 4.5:1 ratio
|
|
1759
|
+
- Large text (18px+): minimum 3:1 ratio
|
|
1760
|
+
- Interactive elements: minimum 3:1 ratio
|
|
1761
|
+
|
|
1762
|
+
### Focus States
|
|
1763
|
+
All interactive elements must have visible focus states:
|
|
1764
|
+
\`\`\`css
|
|
1765
|
+
:focus-visible {
|
|
1766
|
+
outline: 2px solid ${config.primaryColor};
|
|
1767
|
+
outline-offset: 2px;
|
|
1768
|
+
}
|
|
1769
|
+
\`\`\`
|
|
1770
|
+
|
|
1771
|
+
### Touch Targets
|
|
1772
|
+
- Minimum size: 44x44 pixels
|
|
1773
|
+
- Minimum spacing: 8px between targets
|
|
1774
|
+
|
|
1775
|
+
---
|
|
1776
|
+
|
|
1777
|
+
## Icons
|
|
1778
|
+
|
|
1779
|
+
### Icon Style: ${config.iconStyle.charAt(0).toUpperCase() + config.iconStyle.slice(1)}
|
|
1780
|
+
${config.iconStyle === "outline" ? "Light, modern icons with stroke-only design. Best for clean, minimal interfaces." : ""}${config.iconStyle === "solid" ? "Bold, filled icons for high impact and strong visual hierarchy." : ""}${config.iconStyle === "duotone" ? "Two-tone icons with primary and secondary colors for distinctive branding." : ""}
|
|
1781
|
+
|
|
1782
|
+
### Recommended Icon Libraries
|
|
1783
|
+
${config.iconStyle === "outline" ? "- **Lucide React** (recommended) - Consistent, stroke-based\n- **Heroicons Outline** - Tailwind-compatible" : ""}${config.iconStyle === "solid" ? "- **Heroicons Solid** (recommended) - Bold, filled\n- **Phosphor Bold** - Flexible weights" : ""}${config.iconStyle === "duotone" ? "- **Phosphor Duotone** (recommended) - Two-tone design\n- **Font Awesome Duotone** - Wide selection" : ""}
|
|
1784
|
+
|
|
1785
|
+
### Icon Sizes
|
|
1786
|
+
| Size | Pixels | Usage |
|
|
1787
|
+
|------|--------|-------|
|
|
1788
|
+
| xs | 12px | Inline with small text |
|
|
1789
|
+
| sm | 16px | Buttons, inline |
|
|
1790
|
+
| md | 20px | Default |
|
|
1791
|
+
| lg | 24px | Headers, standalone |
|
|
1792
|
+
| xl | 32px | Hero sections |
|
|
1793
|
+
|
|
1794
|
+
---
|
|
1795
|
+
|
|
1796
|
+
## Visual Style
|
|
1797
|
+
|
|
1798
|
+
### Image Style: ${config.imageStyle.charAt(0).toUpperCase() + config.imageStyle.slice(1)}
|
|
1799
|
+
${config.imageStyle === "photography" ? "Real photographs for authentic, relatable content. Use high-quality, diverse imagery." : ""}${config.imageStyle === "illustration" ? "Custom illustrations for unique brand personality. Maintain consistent style across all graphics." : ""}${config.imageStyle === "abstract" ? "Geometric shapes, gradients, and patterns. Modern, tech-forward aesthetic." : ""}${config.imageStyle === "minimal" ? "Simple, clean graphics with minimal detail. Focus on whitespace and clarity." : ""}
|
|
1800
|
+
|
|
1801
|
+
### Card Style: ${config.cardStyle.charAt(0).toUpperCase() + config.cardStyle.slice(1)}
|
|
1802
|
+
\`\`\`jsx
|
|
1803
|
+
// ${config.cardStyle} card pattern
|
|
1804
|
+
<div className="${config.cardStyle === "flat" ? "bg-surface" : ""}${config.cardStyle === "bordered" ? "bg-white border border-border" : ""}${config.cardStyle === "elevated" ? "bg-white shadow-md" : ""} rounded-xl p-6">
|
|
1805
|
+
<h3 className="text-lg font-semibold">Card Title</h3>
|
|
1806
|
+
<p className="text-muted text-sm mt-2">Card content</p>
|
|
1807
|
+
</div>
|
|
1808
|
+
\`\`\`
|
|
1809
|
+
|
|
1810
|
+
---
|
|
1811
|
+
|
|
1812
|
+
## Animation
|
|
1813
|
+
|
|
1814
|
+
### Animation Level: ${config.animationLevel.charAt(0).toUpperCase() + config.animationLevel.slice(1)}
|
|
1815
|
+
${config.animationLevel === "none" ? "No animations. Static UI focused purely on function." : ""}${config.animationLevel === "subtle" ? "Micro-interactions only. Fade-ins, button states, minimal movement." : ""}${config.animationLevel === "moderate" ? "Page transitions, hover effects, loading states. Balanced motion." : ""}${config.animationLevel === "expressive" ? "Bold animations, personality-driven motion, delightful interactions." : ""}
|
|
1816
|
+
|
|
1817
|
+
### Timing
|
|
1818
|
+
\`\`\`css
|
|
1819
|
+
--duration-fast: 150ms;
|
|
1820
|
+
--duration-normal: 200ms;
|
|
1821
|
+
--duration-slow: 300ms;
|
|
1822
|
+
--easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1823
|
+
\`\`\`
|
|
1824
|
+
|
|
1825
|
+
${config.animationLevel !== "none" ? `### Common Animations
|
|
1826
|
+
\`\`\`css
|
|
1827
|
+
.fade-in { animation: fadeIn var(--duration-normal) var(--easing); }
|
|
1828
|
+
.slide-up { animation: slideUp var(--duration-normal) var(--easing); }
|
|
1829
|
+
.scale-in { animation: scaleIn var(--duration-fast) var(--easing); }
|
|
1830
|
+
\`\`\`` : "### No Animations\nAll animations are disabled. Use CSS \\`transition: none\\` globally."}
|
|
1831
|
+
|
|
1832
|
+
---
|
|
1833
|
+
|
|
1834
|
+
## Tailwind Config
|
|
1835
|
+
|
|
1836
|
+
\`\`\`js
|
|
1837
|
+
// tailwind.config.js
|
|
1838
|
+
module.exports = {
|
|
1839
|
+
theme: {
|
|
1840
|
+
extend: {
|
|
1841
|
+
colors: {
|
|
1842
|
+
primary: '${config.primaryColor}',
|
|
1843
|
+
secondary: '${config.secondaryColor}',
|
|
1844
|
+
accent: '${config.accentColor}',
|
|
1845
|
+
success: '${config.successColor}',
|
|
1846
|
+
warning: '${config.warningColor}',
|
|
1847
|
+
error: '${config.errorColor}',
|
|
1848
|
+
},
|
|
1849
|
+
fontFamily: {
|
|
1850
|
+
sans: ['${config.fontFamily}', 'system-ui', 'sans-serif'],
|
|
1851
|
+
heading: ['${config.headingFont}', 'system-ui', 'sans-serif'],
|
|
1852
|
+
mono: ['${config.monoFont}', 'Fira Code', 'monospace'],
|
|
1853
|
+
},
|
|
1854
|
+
borderRadius: {
|
|
1855
|
+
DEFAULT: '${config.borderRadius}',
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1858
|
+
},
|
|
1859
|
+
}
|
|
1860
|
+
\`\`\`
|
|
1861
|
+
|
|
1862
|
+
---
|
|
1863
|
+
|
|
1864
|
+
*Last updated: ${new Date().toISOString().split("T")[0]}*
|
|
1865
|
+
`;
|
|
1866
|
+
fs.writeFileSync(brandGuidePath, brandGuideContent);
|
|
1867
|
+
logSuccess("Created .claude/BRAND_GUIDE.md");
|
|
1868
|
+
} else if (fs.existsSync(brandGuidePath)) {
|
|
1869
|
+
logInfo("Brand guide already exists");
|
|
1870
|
+
} else {
|
|
1871
|
+
logInfo("Skipped brand guide creation");
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1875
|
+
// Step 10: Optional Tools
|
|
1876
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1877
|
+
|
|
1878
|
+
logStep(++currentStep, totalSteps, "Testing tools");
|
|
1879
|
+
|
|
1880
|
+
// Check prerequisites
|
|
1881
|
+
const hasPackageJson = fs.existsSync(path.join(targetDir, "package.json"));
|
|
1882
|
+
const hasPnpm = (() => {
|
|
1883
|
+
try {
|
|
1884
|
+
execSync("pnpm --version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
1885
|
+
return true;
|
|
1886
|
+
} catch {
|
|
1887
|
+
return false;
|
|
500
1888
|
}
|
|
1889
|
+
})();
|
|
1890
|
+
|
|
1891
|
+
if (!hasPackageJson) {
|
|
1892
|
+
log(` ${c.dim}No package.json found - skipping npm package installations${c.reset}`);
|
|
1893
|
+
logInfo("Run 'pnpm init' first, then re-run installer to add testing tools");
|
|
1894
|
+
} else if (!hasPnpm) {
|
|
1895
|
+
log(` ${c.dim}pnpm not found - skipping installations${c.reset}`);
|
|
1896
|
+
logInfo("Install pnpm: npm install -g pnpm");
|
|
1897
|
+
} else {
|
|
1898
|
+
log(` ${c.dim}Installing Playwright, Storybook, and Sandpack${c.reset}`);
|
|
1899
|
+
|
|
1900
|
+
if (config.withStorybook || config.withPlaywright || config.withSandpack) {
|
|
1901
|
+
if (config.withSandpack) {
|
|
1902
|
+
startSpinner("Installing Sandpack (live code preview)...");
|
|
1903
|
+
try {
|
|
1904
|
+
execSync("pnpm add @codesandbox/sandpack-react 2>&1", {
|
|
1905
|
+
cwd: targetDir,
|
|
1906
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1907
|
+
});
|
|
1908
|
+
stopSpinner(true, "Sandpack installed - enables live UI previews");
|
|
1909
|
+
} catch (e) {
|
|
1910
|
+
stopSpinner(false, "Sandpack failed");
|
|
1911
|
+
logInfo(" Run manually: pnpm add @codesandbox/sandpack-react");
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
501
1914
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1915
|
+
if (config.withStorybook) {
|
|
1916
|
+
// Check if Storybook is already initialized
|
|
1917
|
+
const hasStorybook = fs.existsSync(path.join(targetDir, ".storybook"));
|
|
1918
|
+
if (hasStorybook) {
|
|
1919
|
+
logInfo("Storybook already configured");
|
|
1920
|
+
} else {
|
|
1921
|
+
startSpinner("Initializing Storybook (component development)...");
|
|
1922
|
+
try {
|
|
1923
|
+
execSync("pnpm dlx storybook@latest init --yes 2>&1", {
|
|
1924
|
+
cwd: targetDir,
|
|
1925
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1926
|
+
timeout: 300000,
|
|
1927
|
+
});
|
|
1928
|
+
stopSpinner(true, "Storybook initialized - run 'pnpm storybook' to start");
|
|
1929
|
+
} catch (e) {
|
|
1930
|
+
stopSpinner(false, "Storybook failed");
|
|
1931
|
+
logInfo(" Run manually: pnpm dlx storybook@latest init");
|
|
1932
|
+
logInfo(" Requires: React/Vue/Angular/Svelte project");
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (config.withPlaywright) {
|
|
1938
|
+
// Check if Playwright is already initialized
|
|
1939
|
+
const hasPlaywright = fs.existsSync(path.join(targetDir, "playwright.config.ts")) ||
|
|
1940
|
+
fs.existsSync(path.join(targetDir, "playwright.config.js"));
|
|
1941
|
+
if (hasPlaywright) {
|
|
1942
|
+
logInfo("Playwright already configured");
|
|
1943
|
+
} else {
|
|
1944
|
+
startSpinner("Initializing Playwright (E2E testing)...");
|
|
1945
|
+
try {
|
|
1946
|
+
execSync("pnpm create playwright --yes 2>&1", {
|
|
1947
|
+
cwd: targetDir,
|
|
1948
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1949
|
+
timeout: 300000,
|
|
1950
|
+
});
|
|
1951
|
+
stopSpinner(true, "Playwright initialized - run 'pnpm exec playwright test' to start");
|
|
1952
|
+
} catch (e) {
|
|
1953
|
+
stopSpinner(false, "Playwright failed");
|
|
1954
|
+
logInfo(" Run manually: pnpm create playwright");
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
516
1957
|
}
|
|
1958
|
+
} else {
|
|
1959
|
+
logInfo("None selected");
|
|
1960
|
+
logInfo("Run wizard again with Custom Setup to add testing tools");
|
|
517
1961
|
}
|
|
518
|
-
} else {
|
|
519
|
-
logInfo("None selected");
|
|
520
|
-
logInfo(
|
|
521
|
-
"Add later with: --with-storybook --with-playwright --with-sandpack",
|
|
522
|
-
);
|
|
523
1962
|
}
|
|
524
1963
|
|
|
525
1964
|
// ─────────────────────────────────────────────────────────────────────────
|
|
526
1965
|
// Summary
|
|
527
1966
|
// ─────────────────────────────────────────────────────────────────────────
|
|
528
1967
|
|
|
1968
|
+
const check = `${c.white}✓${c.reset}`;
|
|
1969
|
+
const cross = `${c.dim}○${c.reset}`;
|
|
1970
|
+
|
|
529
1971
|
log(`
|
|
530
1972
|
${c.red}═══════════════════════════════════════════════════════════════${c.reset}
|
|
1973
|
+
${c.red}${c.bold} HUSTLE${c.reset}
|
|
531
1974
|
${c.bold} Installation Complete${c.reset}
|
|
532
1975
|
${c.red}═══════════════════════════════════════════════════════════════${c.reset}
|
|
533
1976
|
|
|
534
|
-
${c.bold}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
${
|
|
542
|
-
${
|
|
543
|
-
${
|
|
544
|
-
${
|
|
545
|
-
${
|
|
1977
|
+
${c.bold}Core Components:${c.reset}
|
|
1978
|
+
${check} Commands .claude/commands/ (29 slash commands)
|
|
1979
|
+
${check} Hooks .claude/hooks/ (enforcement hooks)
|
|
1980
|
+
${check} Subagents .claude/agents/ (parallel processing)
|
|
1981
|
+
${check} Config .claude/ (settings, state, registry)
|
|
1982
|
+
|
|
1983
|
+
${c.bold}Configuration:${c.reset}
|
|
1984
|
+
${config.githubToken ? check : cross} GITHUB_TOKEN ${config.githubToken ? "configured" : "not set"}
|
|
1985
|
+
${config.greptileApiKey ? check : cross} GREPTILE_API_KEY ${config.greptileApiKey ? "configured" : "not set"}
|
|
1986
|
+
${config.brandfetchApiKey ? check : cross} BRANDFETCH_API_KEY ${config.brandfetchApiKey ? "configured" : "not set"}
|
|
1987
|
+
${config.ntfyEnabled ? check : cross} NTFY Notifications ${config.ntfyEnabled ? config.ntfyTopic : "disabled"}
|
|
1988
|
+
${config.createBrandGuide ? check : cross} Brand Guide ${config.createBrandGuide ? (config.brandSource === "brandfetch" ? `from ${config.brandDomain}` : "manual") : "skipped"}
|
|
1989
|
+
|
|
1990
|
+
${c.bold}Testing Tools:${c.reset}
|
|
1991
|
+
${config.withPlaywright ? check : cross} Playwright ${config.withPlaywright ? "installed" : "not installed"}
|
|
1992
|
+
${config.withStorybook ? check : cross} Storybook ${config.withStorybook ? "installed" : "not installed"}
|
|
1993
|
+
${config.withSandpack ? check : cross} Sandpack ${config.withSandpack ? "installed" : "not installed"}
|
|
1994
|
+
|
|
1995
|
+
${c.bold}Ready to Use:${c.reset}
|
|
1996
|
+
${c.gray}$${c.reset} /hustle-api-create [endpoint] ${c.dim}# Build API endpoint${c.reset}
|
|
1997
|
+
${c.gray}$${c.reset} /hustle-ui-create [component] ${c.dim}# Build component${c.reset}
|
|
1998
|
+
${c.gray}$${c.reset} /hustle-ui-create-page [page] ${c.dim}# Build page${c.reset}
|
|
1999
|
+
${c.gray}$${c.reset} /hustle-combine [apis] ${c.dim}# Orchestrate APIs${c.reset}
|
|
546
2000
|
|
|
547
2001
|
${c.bold}Next Steps:${c.reset}
|
|
548
|
-
|
|
549
|
-
2. ${c.white}Configure your API keys in .env${c.reset}
|
|
550
|
-
3. ${c.white}Add GREPTILE_API_KEY + GITHUB_TOKEN${c.reset} for Phase 14 code review
|
|
551
|
-
4. ${c.white}/ntfy-setup${c.reset} for push notifications (optional)
|
|
552
|
-
5. ${c.white}Restart Claude Code${c.reset} for MCP tools
|
|
2002
|
+
${!config.githubToken ? `${c.white}→ Add GITHUB_TOKEN to .env${c.reset} (https://github.com/settings/tokens)\n ` : ""}${!config.greptileApiKey ? `${c.white}→ Add GREPTILE_API_KEY to .env${c.reset} (https://app.greptile.com/settings/api)\n ` : ""}${!config.brandfetchApiKey && config.brandSource === "brandfetch" ? `${c.white}→ Add BRANDFETCH_API_KEY to .env${c.reset} (https://brandfetch.com/developers)\n ` : ""}${c.white}→ Restart Claude Code${c.reset} to load MCP servers
|
|
553
2003
|
|
|
554
2004
|
${c.dim}Documentation: https://github.com/hustle-together/api-dev-tools${c.reset}
|
|
555
2005
|
`);
|