@hustle-together/api-dev-tools 3.12.2 → 3.12.10
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/api-dev-state.json +224 -165
- package/CHANGELOG.md +29 -0
- package/README.md +58 -1
- package/bin/cli.js +1303 -89
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- 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 +132 -45
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
- 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,357 @@ 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
|
+
|
|
115
467
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
468
|
// File Copy Utilities
|
|
117
469
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -119,8 +471,10 @@ function logError(message) {
|
|
|
119
471
|
function copyDir(src, dest, options = {}) {
|
|
120
472
|
const { filter = () => true, overwrite = false } = options;
|
|
121
473
|
let copied = 0;
|
|
474
|
+
let skipped = 0;
|
|
475
|
+
let total = 0;
|
|
122
476
|
|
|
123
|
-
if (!fs.existsSync(src)) return copied;
|
|
477
|
+
if (!fs.existsSync(src)) return { copied: 0, skipped: 0, total: 0 };
|
|
124
478
|
|
|
125
479
|
if (!fs.existsSync(dest)) {
|
|
126
480
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -135,16 +489,22 @@ function copyDir(src, dest, options = {}) {
|
|
|
135
489
|
if (!filter(entry.name, srcPath)) continue;
|
|
136
490
|
|
|
137
491
|
if (entry.isDirectory()) {
|
|
138
|
-
|
|
492
|
+
const result = copyDir(srcPath, destPath, options);
|
|
493
|
+
copied += result.copied;
|
|
494
|
+
skipped += result.skipped;
|
|
495
|
+
total += result.total;
|
|
139
496
|
} else {
|
|
497
|
+
total++;
|
|
140
498
|
if (!fs.existsSync(destPath) || overwrite) {
|
|
141
499
|
fs.copyFileSync(srcPath, destPath);
|
|
142
500
|
copied++;
|
|
501
|
+
} else {
|
|
502
|
+
skipped++;
|
|
143
503
|
}
|
|
144
504
|
}
|
|
145
505
|
}
|
|
146
506
|
|
|
147
|
-
return copied;
|
|
507
|
+
return { copied, skipped, total };
|
|
148
508
|
}
|
|
149
509
|
|
|
150
510
|
function copyFile(src, dest, options = {}) {
|
|
@@ -205,10 +565,8 @@ function checkNode() {
|
|
|
205
565
|
async function main() {
|
|
206
566
|
// Parse arguments
|
|
207
567
|
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
568
|
const silent = args.includes("--silent") || args.includes("-s");
|
|
569
|
+
const quickMode = args.includes("--quick") || args.includes("-q");
|
|
212
570
|
|
|
213
571
|
// Show banner
|
|
214
572
|
if (!silent) {
|
|
@@ -222,7 +580,318 @@ async function main() {
|
|
|
222
580
|
const claudeDir = path.join(targetDir, ".claude");
|
|
223
581
|
const hooksDir = path.join(claudeDir, "hooks");
|
|
224
582
|
|
|
225
|
-
|
|
583
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
584
|
+
// WIZARD STEP 1: Choose Setup Mode
|
|
585
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
let config = {
|
|
588
|
+
withStorybook: true,
|
|
589
|
+
withPlaywright: true,
|
|
590
|
+
withSandpack: true,
|
|
591
|
+
githubToken: "",
|
|
592
|
+
greptileApiKey: "",
|
|
593
|
+
brandfetchApiKey: "",
|
|
594
|
+
ntfyEnabled: false,
|
|
595
|
+
ntfyTopic: "",
|
|
596
|
+
ntfyServer: "ntfy.sh",
|
|
597
|
+
createBrandGuide: true,
|
|
598
|
+
brandSource: "manual", // "brandfetch" or "manual"
|
|
599
|
+
brandDomain: "",
|
|
600
|
+
brandName: path.basename(targetDir),
|
|
601
|
+
primaryColor: "#E11D48",
|
|
602
|
+
secondaryColor: "#1E40AF",
|
|
603
|
+
accentColor: "#8B5CF6",
|
|
604
|
+
successColor: "#10B981",
|
|
605
|
+
warningColor: "#F59E0B",
|
|
606
|
+
errorColor: "#EF4444",
|
|
607
|
+
fontFamily: "Inter",
|
|
608
|
+
headingFont: "Inter",
|
|
609
|
+
monoFont: "JetBrains Mono",
|
|
610
|
+
borderRadius: "8px",
|
|
611
|
+
darkMode: true,
|
|
612
|
+
imageStyle: "photography", // photography, illustration, abstract, minimal
|
|
613
|
+
iconStyle: "outline", // outline, solid, duotone
|
|
614
|
+
buttonStyle: "rounded", // sharp, subtle, rounded, pill
|
|
615
|
+
cardStyle: "elevated", // flat, bordered, elevated
|
|
616
|
+
animationLevel: "subtle", // none, subtle, moderate, expressive
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
if (!silent && !quickMode) {
|
|
620
|
+
const setupMode = await selectOne("Choose installation mode", [
|
|
621
|
+
{ label: "Quick Setup (recommended defaults)", value: "quick" },
|
|
622
|
+
{ label: "Custom Setup (configure each option)", value: "custom" },
|
|
623
|
+
]);
|
|
624
|
+
|
|
625
|
+
if (setupMode === "custom") {
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
627
|
+
// WIZARD STEP 2: API Keys
|
|
628
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
log(`\n${c.red}━━━ API Keys ━━━${c.reset}`);
|
|
631
|
+
log(`${c.dim}These keys enable advanced features like code review and brand fetching${c.reset}\n`);
|
|
632
|
+
log(`${c.bold}Get your keys (all have free tiers):${c.reset}\n`);
|
|
633
|
+
log(` ${c.white}GITHUB_TOKEN${c.reset}`);
|
|
634
|
+
log(` ${c.dim}→${c.reset} https://github.com/settings/tokens`);
|
|
635
|
+
log(` ${c.dim}Purpose: Create issues, PRs, search code${c.reset}`);
|
|
636
|
+
log(` ${c.dim}Scope needed: 'repo' for private repos${c.reset}\n`);
|
|
637
|
+
log(` ${c.white}GREPTILE_API_KEY${c.reset}`);
|
|
638
|
+
log(` ${c.dim}→${c.reset} https://app.greptile.com/settings/api`);
|
|
639
|
+
log(` ${c.dim}Purpose: AI code review in Phase 11${c.reset}`);
|
|
640
|
+
log(` ${c.dim}Free tier: 100 reviews/month${c.reset}\n`);
|
|
641
|
+
log(` ${c.white}BRANDFETCH_API_KEY${c.reset}`);
|
|
642
|
+
log(` ${c.dim}→${c.reset} https://brandfetch.com/developers`);
|
|
643
|
+
log(` ${c.dim}Purpose: Auto-fetch logos, colors, fonts from domains${c.reset}`);
|
|
644
|
+
log(` ${c.dim}Free tier: 50 requests/month (basic assets)${c.reset}\n`);
|
|
645
|
+
|
|
646
|
+
const configureKeys = await selectOne("Configure API keys now?", [
|
|
647
|
+
{ label: "Yes, enter them now", value: true },
|
|
648
|
+
{ label: "No, I'll add to .env later", value: false },
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
if (configureKeys) {
|
|
652
|
+
config.githubToken = await textInput("GITHUB_TOKEN", {
|
|
653
|
+
default: process.env.GITHUB_TOKEN || "",
|
|
654
|
+
mask: true,
|
|
655
|
+
});
|
|
656
|
+
config.greptileApiKey = await textInput("GREPTILE_API_KEY", {
|
|
657
|
+
default: process.env.GREPTILE_API_KEY || "",
|
|
658
|
+
mask: true,
|
|
659
|
+
});
|
|
660
|
+
config.brandfetchApiKey = await textInput("BRANDFETCH_API_KEY (optional)", {
|
|
661
|
+
default: process.env.BRANDFETCH_API_KEY || "",
|
|
662
|
+
mask: true,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
667
|
+
// WIZARD STEP 3: Testing Tools
|
|
668
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
log(`\n${c.red}━━━ Testing Tools ━━━${c.reset}`);
|
|
671
|
+
log(`${c.dim}Required for component development and E2E testing${c.reset}\n`);
|
|
672
|
+
log(`${c.bold}What each tool does:${c.reset}`);
|
|
673
|
+
log(` ${c.white}Playwright${c.reset} → E2E browser testing (required for /hustle-ui-create-page)`);
|
|
674
|
+
log(` ${c.white}Storybook${c.reset} → Component development & visual testing (required for /hustle-ui-create)`);
|
|
675
|
+
log(` ${c.white}Sandpack${c.reset} → Live code preview in browser (optional)\n`);
|
|
676
|
+
|
|
677
|
+
const selectedTools = await selectMany("Select testing tools to install", [
|
|
678
|
+
{ label: "Playwright (E2E testing)", value: "playwright", checked: true },
|
|
679
|
+
{ label: "Storybook (component development)", value: "storybook", checked: true },
|
|
680
|
+
{ label: "Sandpack (live code preview)", value: "sandpack", checked: true },
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
config.withPlaywright = selectedTools.includes("playwright");
|
|
684
|
+
config.withStorybook = selectedTools.includes("storybook");
|
|
685
|
+
config.withSandpack = selectedTools.includes("sandpack");
|
|
686
|
+
|
|
687
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
688
|
+
// WIZARD STEP 4: NTFY Notifications
|
|
689
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
log(`\n${c.red}━━━ Push Notifications (NTFY) ━━━${c.reset}`);
|
|
692
|
+
log(`${c.dim}Get notified on your phone when long tasks complete${c.reset}\n`);
|
|
693
|
+
log(`${c.bold}How it works:${c.reset}`);
|
|
694
|
+
log(` 1. Install NTFY app: ${c.white}iOS${c.reset} App Store / ${c.white}Android${c.reset} Play Store`);
|
|
695
|
+
log(` 2. Subscribe to your topic in the app`);
|
|
696
|
+
log(` 3. Receive push notifications when builds/tests finish\n`);
|
|
697
|
+
log(`${c.dim}Free service, no account required. Learn more: https://ntfy.sh${c.reset}\n`);
|
|
698
|
+
|
|
699
|
+
config.ntfyEnabled = await confirm("Enable NTFY push notifications?", false);
|
|
700
|
+
|
|
701
|
+
if (config.ntfyEnabled) {
|
|
702
|
+
log(`\n${c.dim}Topic name = channel for your notifications (must match the app)${c.reset}`);
|
|
703
|
+
config.ntfyTopic = await textInput("NTFY Topic name", {
|
|
704
|
+
default: path.basename(targetDir) + "-alerts",
|
|
705
|
+
});
|
|
706
|
+
log(`${c.dim}Server URL = ntfy.sh (public) or your self-hosted server${c.reset}`);
|
|
707
|
+
config.ntfyServer = await textInput("NTFY Server URL", {
|
|
708
|
+
default: "ntfy.sh",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
713
|
+
// WIZARD STEP 5: Brand Guide
|
|
714
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
715
|
+
|
|
716
|
+
log(`\n${c.red}━━━ Brand Guide ━━━${c.reset}`);
|
|
717
|
+
log(`${c.dim}Design system that enforces consistent UI across all components${c.reset}\n`);
|
|
718
|
+
log(`${c.bold}Why you need a brand guide:${c.reset}`);
|
|
719
|
+
log(` • Consistent look across all pages and components`);
|
|
720
|
+
log(` • Faster development (no color/font decisions each time)`);
|
|
721
|
+
log(` • Enforced by hooks during /hustle-ui-create`);
|
|
722
|
+
log(` • Professional, cohesive user experience\n`);
|
|
723
|
+
|
|
724
|
+
config.createBrandGuide = await confirm("Create brand guide?", true);
|
|
725
|
+
|
|
726
|
+
if (config.createBrandGuide) {
|
|
727
|
+
// Brand source selection
|
|
728
|
+
log(`\n${c.bold}How would you like to create your brand guide?${c.reset}\n`);
|
|
729
|
+
|
|
730
|
+
config.brandSource = await selectOne("Brand guide source", [
|
|
731
|
+
{ label: "Manual Interview - Answer questions about your brand preferences", value: "manual" },
|
|
732
|
+
{ label: "Brandfetch - Auto-fetch from company domain (requires API key)", value: "brandfetch" },
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
if (config.brandSource === "brandfetch") {
|
|
736
|
+
log(`\n${c.bold}Brandfetch Integration${c.reset}`);
|
|
737
|
+
log(`${c.dim}Automatically pulls logos, colors, and fonts from any company domain${c.reset}\n`);
|
|
738
|
+
|
|
739
|
+
if (!config.brandfetchApiKey) {
|
|
740
|
+
log(`${c.white}Get your free API key:${c.reset} https://brandfetch.com/developers`);
|
|
741
|
+
log(`${c.dim}Free tier includes: 50 requests/month, basic brand assets${c.reset}\n`);
|
|
742
|
+
config.brandfetchApiKey = await textInput("BRANDFETCH_API_KEY", {
|
|
743
|
+
default: process.env.BRANDFETCH_API_KEY || "",
|
|
744
|
+
mask: true,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
config.brandDomain = await textInput("Company domain to fetch brand from (e.g., stripe.com)", {
|
|
749
|
+
default: "",
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
if (!config.brandDomain) {
|
|
753
|
+
log(`\n${c.dim}No domain provided - falling back to manual interview${c.reset}`);
|
|
754
|
+
config.brandSource = "manual";
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (config.brandSource === "manual") {
|
|
759
|
+
log(`\n${c.bold}━━━ Brand Interview ━━━${c.reset}`);
|
|
760
|
+
log(`${c.dim}Let's define your brand's visual identity${c.reset}\n`);
|
|
761
|
+
|
|
762
|
+
// Basic identity
|
|
763
|
+
config.brandName = await textInput("Brand/Project name", {
|
|
764
|
+
default: path.basename(targetDir),
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Color palette
|
|
768
|
+
log(`\n${c.bold}Color Palette${c.reset}`);
|
|
769
|
+
log(`${c.dim}Define colors that represent your brand${c.reset}`);
|
|
770
|
+
log(`${c.dim}Enter hex (#E11D48) or color name (red, blue, coral, navy, etc.)${c.reset}\n`);
|
|
771
|
+
|
|
772
|
+
let primaryInput = await textInput("Primary color (main CTAs, links)", {
|
|
773
|
+
default: "#E11D48",
|
|
774
|
+
hint: "hex or name",
|
|
775
|
+
});
|
|
776
|
+
config.primaryColor = parseColor(primaryInput, "#E11D48");
|
|
777
|
+
if (primaryInput && primaryInput !== config.primaryColor) {
|
|
778
|
+
log(` ${c.dim}→ Resolved to ${config.primaryColor}${c.reset}`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
let secondaryInput = await textInput("Secondary color (accents)", {
|
|
782
|
+
default: "#1E40AF",
|
|
783
|
+
hint: "hex or name",
|
|
784
|
+
});
|
|
785
|
+
config.secondaryColor = parseColor(secondaryInput, "#1E40AF");
|
|
786
|
+
if (secondaryInput && secondaryInput !== config.secondaryColor) {
|
|
787
|
+
log(` ${c.dim}→ Resolved to ${config.secondaryColor}${c.reset}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
let accentInput = await textInput("Accent color (highlights, badges)", {
|
|
791
|
+
default: "#8B5CF6",
|
|
792
|
+
hint: "hex or name",
|
|
793
|
+
});
|
|
794
|
+
config.accentColor = parseColor(accentInput, "#8B5CF6");
|
|
795
|
+
if (accentInput && accentInput !== config.accentColor) {
|
|
796
|
+
log(` ${c.dim}→ Resolved to ${config.accentColor}${c.reset}`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Typography
|
|
800
|
+
log(`\n${c.bold}Typography${c.reset}`);
|
|
801
|
+
log(`${c.dim}Fonts define your brand's personality${c.reset}`);
|
|
802
|
+
log(`${c.dim}Select from presets or describe what you want (e.g., "elegant serif", "modern sans")${c.reset}\n`);
|
|
803
|
+
|
|
804
|
+
config.fontFamily = await selectOne("Primary body font", [
|
|
805
|
+
{ label: "Inter - Clean, modern, highly readable", value: "Inter" },
|
|
806
|
+
{ label: "Geist - GitHub/Vercel aesthetic", value: "Geist" },
|
|
807
|
+
{ label: "Plus Jakarta Sans - Friendly, approachable", value: "Plus Jakarta Sans" },
|
|
808
|
+
{ label: "DM Sans - Geometric, professional", value: "DM Sans" },
|
|
809
|
+
{ label: "IBM Plex Sans - Technical, serious", value: "IBM Plex Sans" },
|
|
810
|
+
{ label: "Other - Describe or enter font name", value: "custom" },
|
|
811
|
+
]);
|
|
812
|
+
|
|
813
|
+
if (config.fontFamily === "custom") {
|
|
814
|
+
let fontInput = await textInput("Font name or description", {
|
|
815
|
+
default: "Inter",
|
|
816
|
+
hint: 'e.g., "Playfair" or "elegant serif"',
|
|
817
|
+
});
|
|
818
|
+
config.fontFamily = parseFont(fontInput, "Inter");
|
|
819
|
+
log(` ${c.dim}→ Using: ${config.fontFamily}${c.reset}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
config.headingFont = await selectOne("Heading font", [
|
|
823
|
+
{ label: "Same as body font", value: config.fontFamily },
|
|
824
|
+
{ label: "Playfair Display - Elegant, sophisticated", value: "Playfair Display" },
|
|
825
|
+
{ label: "Cal Sans - Bold, impactful", value: "Cal Sans" },
|
|
826
|
+
{ label: "Clash Display - Modern, striking", value: "Clash Display" },
|
|
827
|
+
{ label: "Other - Describe or enter font name", value: "custom" },
|
|
828
|
+
]);
|
|
829
|
+
|
|
830
|
+
if (config.headingFont === "custom") {
|
|
831
|
+
let headingInput = await textInput("Heading font name or description", {
|
|
832
|
+
default: config.fontFamily,
|
|
833
|
+
hint: 'e.g., "sans-serif that pairs nicely"',
|
|
834
|
+
});
|
|
835
|
+
config.headingFont = parseFont(headingInput, config.fontFamily);
|
|
836
|
+
log(` ${c.dim}→ Using: ${config.headingFont}${c.reset}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// UI Style preferences
|
|
840
|
+
log(`\n${c.bold}UI Style Preferences${c.reset}`);
|
|
841
|
+
log(`${c.dim}Define the overall look and feel${c.reset}\n`);
|
|
842
|
+
|
|
843
|
+
config.buttonStyle = await selectOne("Button style", [
|
|
844
|
+
{ label: "Sharp (0px) - Modern, minimal tech aesthetic", value: "sharp" },
|
|
845
|
+
{ label: "Subtle (4px) - Professional, slightly softened", value: "subtle" },
|
|
846
|
+
{ label: "Rounded (8px) - Friendly, approachable", value: "rounded" },
|
|
847
|
+
{ label: "Pill (9999px) - Playful, fully rounded", value: "pill" },
|
|
848
|
+
]);
|
|
849
|
+
|
|
850
|
+
// Map button style to border radius
|
|
851
|
+
const radiusMap = { sharp: "0", subtle: "4px", rounded: "8px", pill: "9999px" };
|
|
852
|
+
config.borderRadius = radiusMap[config.buttonStyle];
|
|
853
|
+
|
|
854
|
+
config.cardStyle = await selectOne("Card style", [
|
|
855
|
+
{ label: "Flat - Minimal, no depth", value: "flat" },
|
|
856
|
+
{ label: "Bordered - Subtle outline separation", value: "bordered" },
|
|
857
|
+
{ label: "Elevated - Shadow for depth", value: "elevated" },
|
|
858
|
+
]);
|
|
859
|
+
|
|
860
|
+
// Visual content style
|
|
861
|
+
log(`\n${c.bold}Visual Content${c.reset}`);
|
|
862
|
+
log(`${c.dim}Preferences for images and icons${c.reset}\n`);
|
|
863
|
+
|
|
864
|
+
config.imageStyle = await selectOne("Preferred image style", [
|
|
865
|
+
{ label: "Photography - Real photos, authentic feel", value: "photography" },
|
|
866
|
+
{ label: "Illustrations - Custom drawn, unique personality", value: "illustration" },
|
|
867
|
+
{ label: "Abstract - Shapes, gradients, patterns", value: "abstract" },
|
|
868
|
+
{ label: "Minimal - Clean, simple graphics", value: "minimal" },
|
|
869
|
+
]);
|
|
870
|
+
|
|
871
|
+
config.iconStyle = await selectOne("Icon style", [
|
|
872
|
+
{ label: "Outline - Light, modern (Lucide, Heroicons)", value: "outline" },
|
|
873
|
+
{ label: "Solid - Bold, impactful (Phosphor filled)", value: "solid" },
|
|
874
|
+
{ label: "Duotone - Two-tone, distinctive", value: "duotone" },
|
|
875
|
+
]);
|
|
876
|
+
|
|
877
|
+
// Animation preferences
|
|
878
|
+
config.animationLevel = await selectOne("Animation level", [
|
|
879
|
+
{ label: "None - Static UI, pure function", value: "none" },
|
|
880
|
+
{ label: "Subtle - Micro-interactions, fade-ins", value: "subtle" },
|
|
881
|
+
{ label: "Moderate - Page transitions, hovers", value: "moderate" },
|
|
882
|
+
{ label: "Expressive - Bold animations, personality", value: "expressive" },
|
|
883
|
+
]);
|
|
884
|
+
|
|
885
|
+
// Dark mode
|
|
886
|
+
config.darkMode = await confirm("Include dark mode support?", true);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
log(`\n${c.red}━━━ Installing ━━━${c.reset}\n`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const totalSteps = 10;
|
|
226
895
|
let currentStep = 0;
|
|
227
896
|
|
|
228
897
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -230,6 +899,7 @@ async function main() {
|
|
|
230
899
|
// ─────────────────────────────────────────────────────────────────────────
|
|
231
900
|
|
|
232
901
|
logStep(++currentStep, totalSteps, "Checking prerequisites");
|
|
902
|
+
log(` ${c.dim}Verifying Node.js 18+ and Python 3 are installed${c.reset}`);
|
|
233
903
|
|
|
234
904
|
const node = checkNode();
|
|
235
905
|
if (node.ok) {
|
|
@@ -252,25 +922,35 @@ async function main() {
|
|
|
252
922
|
// ─────────────────────────────────────────────────────────────────────────
|
|
253
923
|
|
|
254
924
|
logStep(++currentStep, totalSteps, "Installing slash commands");
|
|
925
|
+
log(` ${c.dim}Copying 29 commands including /hustle-api-create, /hustle-ui-create${c.reset}`);
|
|
255
926
|
|
|
256
927
|
const commandsDir = path.join(claudeDir, "commands");
|
|
257
|
-
const sourceCommandsDir = path.join(packageDir, "
|
|
928
|
+
const sourceCommandsDir = path.join(packageDir, "commands");
|
|
258
929
|
|
|
259
930
|
if (!fs.existsSync(commandsDir)) {
|
|
260
931
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
261
932
|
}
|
|
262
933
|
|
|
263
|
-
const
|
|
934
|
+
const commandsResult = copyDir(sourceCommandsDir, commandsDir, {
|
|
264
935
|
filter: (name) => name.endsWith(".md"),
|
|
265
936
|
});
|
|
266
937
|
|
|
267
|
-
|
|
938
|
+
if (commandsResult.copied > 0) {
|
|
939
|
+
logSuccess(`${commandsResult.copied} new commands installed to .claude/commands/`);
|
|
940
|
+
}
|
|
941
|
+
if (commandsResult.skipped > 0) {
|
|
942
|
+
logInfo(`${commandsResult.skipped} commands already exist (preserved)`);
|
|
943
|
+
}
|
|
944
|
+
if (commandsResult.total === 0) {
|
|
945
|
+
logWarn("No commands found in package");
|
|
946
|
+
}
|
|
268
947
|
|
|
269
948
|
// ─────────────────────────────────────────────────────────────────────────
|
|
270
949
|
// Step 3: Install Hooks
|
|
271
950
|
// ─────────────────────────────────────────────────────────────────────────
|
|
272
951
|
|
|
273
952
|
logStep(++currentStep, totalSteps, "Installing enforcement hooks");
|
|
953
|
+
log(` ${c.dim}Python hooks that enforce TDD workflow and prevent skipping phases${c.reset}`);
|
|
274
954
|
|
|
275
955
|
const sourceHooksDir = path.join(packageDir, "hooks");
|
|
276
956
|
|
|
@@ -291,11 +971,15 @@ async function main() {
|
|
|
291
971
|
|
|
292
972
|
// Copy Python hook files
|
|
293
973
|
let hooksCopied = 0;
|
|
974
|
+
let hooksSkipped = 0;
|
|
975
|
+
let hooksTotal = 0;
|
|
294
976
|
if (fs.existsSync(sourceHooksDir)) {
|
|
295
977
|
const hookFiles = fs
|
|
296
978
|
.readdirSync(sourceHooksDir)
|
|
297
979
|
.filter((f) => f.endsWith(".py"));
|
|
298
980
|
|
|
981
|
+
hooksTotal = hookFiles.length;
|
|
982
|
+
|
|
299
983
|
for (const file of hookFiles) {
|
|
300
984
|
const src = path.join(sourceHooksDir, file);
|
|
301
985
|
const dest = path.join(hooksDir, file);
|
|
@@ -304,11 +988,18 @@ async function main() {
|
|
|
304
988
|
fs.copyFileSync(src, dest);
|
|
305
989
|
fs.chmodSync(dest, "755");
|
|
306
990
|
hooksCopied++;
|
|
991
|
+
} else {
|
|
992
|
+
hooksSkipped++;
|
|
307
993
|
}
|
|
308
994
|
}
|
|
309
995
|
}
|
|
310
996
|
|
|
311
|
-
|
|
997
|
+
if (hooksCopied > 0) {
|
|
998
|
+
logSuccess(`${hooksCopied} new hooks installed to .claude/hooks/`);
|
|
999
|
+
}
|
|
1000
|
+
if (hooksSkipped > 0) {
|
|
1001
|
+
logInfo(`${hooksSkipped} hooks already exist (preserved)`);
|
|
1002
|
+
}
|
|
312
1003
|
logInfo("Includes: enforce-*, notify-*, track-token-usage.py");
|
|
313
1004
|
|
|
314
1005
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -316,6 +1007,7 @@ async function main() {
|
|
|
316
1007
|
// ─────────────────────────────────────────────────────────────────────────
|
|
317
1008
|
|
|
318
1009
|
logStep(++currentStep, totalSteps, "Installing subagents");
|
|
1010
|
+
log(` ${c.dim}AI agents for parallel research, schema generation, code review${c.reset}`);
|
|
319
1011
|
|
|
320
1012
|
const agentsDir = path.join(claudeDir, "agents");
|
|
321
1013
|
const sourceAgentsDir = path.join(packageDir, ".claude", "agents");
|
|
@@ -324,11 +1016,16 @@ async function main() {
|
|
|
324
1016
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
325
1017
|
}
|
|
326
1018
|
|
|
327
|
-
const
|
|
1019
|
+
const agentsResult = copyDir(sourceAgentsDir, agentsDir, {
|
|
328
1020
|
filter: (name) => name.endsWith(".md"),
|
|
329
1021
|
});
|
|
330
1022
|
|
|
331
|
-
|
|
1023
|
+
if (agentsResult.copied > 0) {
|
|
1024
|
+
logSuccess(`${agentsResult.copied} new subagents installed to .claude/agents/`);
|
|
1025
|
+
}
|
|
1026
|
+
if (agentsResult.skipped > 0) {
|
|
1027
|
+
logInfo(`${agentsResult.skipped} subagents already exist (preserved)`);
|
|
1028
|
+
}
|
|
332
1029
|
logInfo("Haiku: parallel-researcher, research-validator, docs-generator");
|
|
333
1030
|
logInfo(
|
|
334
1031
|
"Sonnet: schema-generator, test-writer, implementation-reviewer, code-reviewer",
|
|
@@ -339,6 +1036,7 @@ async function main() {
|
|
|
339
1036
|
// ─────────────────────────────────────────────────────────────────────────
|
|
340
1037
|
|
|
341
1038
|
logStep(++currentStep, totalSteps, "Setting up configuration");
|
|
1039
|
+
log(` ${c.dim}Creating settings.json, state tracking, and research cache${c.reset}`);
|
|
342
1040
|
|
|
343
1041
|
const sourceTemplatesDir = path.join(packageDir, "templates");
|
|
344
1042
|
|
|
@@ -400,6 +1098,7 @@ async function main() {
|
|
|
400
1098
|
// ─────────────────────────────────────────────────────────────────────────
|
|
401
1099
|
|
|
402
1100
|
logStep(++currentStep, totalSteps, "Setting up environment");
|
|
1101
|
+
log(` ${c.dim}Creating .env.example template for API keys${c.reset}`);
|
|
403
1102
|
|
|
404
1103
|
const templatesDestDir = path.join(targetDir, "templates");
|
|
405
1104
|
if (!fs.existsSync(templatesDestDir)) {
|
|
@@ -415,19 +1114,54 @@ async function main() {
|
|
|
415
1114
|
logInfo("Copy to .env and configure your API keys");
|
|
416
1115
|
}
|
|
417
1116
|
|
|
1117
|
+
// Copy showcase template directories (for update-api-showcase.py and update-ui-showcase.py hooks)
|
|
1118
|
+
const showcaseDirs = ["api-showcase", "ui-showcase", "shared"];
|
|
1119
|
+
for (const dir of showcaseDirs) {
|
|
1120
|
+
const srcDir = path.join(sourceTemplatesDir, dir);
|
|
1121
|
+
const destDir = path.join(templatesDestDir, dir);
|
|
1122
|
+
if (fs.existsSync(srcDir)) {
|
|
1123
|
+
const result = copyDir(srcDir, destDir);
|
|
1124
|
+
if (result.copied > 0) {
|
|
1125
|
+
logSuccess(`Copied ${dir} templates (${result.copied} files)`);
|
|
1126
|
+
} else if (result.skipped > 0) {
|
|
1127
|
+
logInfo(`${dir} templates already exist (${result.skipped} files preserved)`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
418
1132
|
// ─────────────────────────────────────────────────────────────────────────
|
|
419
1133
|
// Step 7: Configure MCP Servers
|
|
420
1134
|
// ─────────────────────────────────────────────────────────────────────────
|
|
421
1135
|
|
|
422
1136
|
logStep(++currentStep, totalSteps, "Configuring MCP servers");
|
|
1137
|
+
log(` ${c.dim}Setting up AI-powered integrations for research and code review${c.reset}`);
|
|
423
1138
|
|
|
424
1139
|
const mcpServers = [
|
|
425
|
-
{
|
|
426
|
-
|
|
1140
|
+
{
|
|
1141
|
+
name: "context7",
|
|
1142
|
+
cmd: "npx -y @upstash/context7-mcp",
|
|
1143
|
+
description: "Live documentation lookup (npm, APIs, frameworks)",
|
|
1144
|
+
required: true,
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "github",
|
|
1148
|
+
cmd: "npx -y @modelcontextprotocol/server-github",
|
|
1149
|
+
description: "GitHub integration (issues, PRs, code search)",
|
|
1150
|
+
required: true,
|
|
1151
|
+
},
|
|
427
1152
|
{
|
|
428
1153
|
name: "greptile",
|
|
429
1154
|
cmd: "npx -y @anthropics/mcp-greptile",
|
|
1155
|
+
description: "AI code review for Phase 11 verification",
|
|
1156
|
+
optional: true,
|
|
1157
|
+
requiresKey: "GREPTILE_API_KEY",
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
name: "brandfetch",
|
|
1161
|
+
cmd: "npx -y @anthropics/mcp-brandfetch",
|
|
1162
|
+
description: "Auto-fetch brand assets (logos, colors, fonts)",
|
|
430
1163
|
optional: true,
|
|
1164
|
+
requiresKey: "BRANDFETCH_API_KEY",
|
|
431
1165
|
},
|
|
432
1166
|
];
|
|
433
1167
|
|
|
@@ -438,6 +1172,7 @@ async function main() {
|
|
|
438
1172
|
stdio: ["pipe", "pipe", "pipe"],
|
|
439
1173
|
});
|
|
440
1174
|
logInfo(`${server.name} already configured`);
|
|
1175
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
441
1176
|
} catch (e) {
|
|
442
1177
|
try {
|
|
443
1178
|
execSync(`claude mcp add ${server.name} -- ${server.cmd}`, {
|
|
@@ -445,11 +1180,11 @@ async function main() {
|
|
|
445
1180
|
stdio: ["pipe", "pipe", "pipe"],
|
|
446
1181
|
});
|
|
447
1182
|
logSuccess(`Added ${server.name}`);
|
|
1183
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
448
1184
|
} catch (addErr) {
|
|
449
1185
|
if (server.optional) {
|
|
450
|
-
logInfo(
|
|
451
|
-
|
|
452
|
-
);
|
|
1186
|
+
logInfo(`${server.name} (optional) - requires ${server.requiresKey}`);
|
|
1187
|
+
log(` ${c.dim}${server.description}${c.reset}`);
|
|
453
1188
|
} else {
|
|
454
1189
|
logWarn(`Could not add ${server.name} - add manually`);
|
|
455
1190
|
}
|
|
@@ -457,99 +1192,578 @@ async function main() {
|
|
|
457
1192
|
}
|
|
458
1193
|
}
|
|
459
1194
|
|
|
460
|
-
|
|
1195
|
+
log(`\n ${c.bold}MCP Server Benefits:${c.reset}`);
|
|
1196
|
+
log(` ${c.dim}• Context7: Always get latest docs, no hallucinated APIs${c.reset}`);
|
|
1197
|
+
log(` ${c.dim}• GitHub: Create issues/PRs directly from Claude${c.reset}`);
|
|
1198
|
+
log(` ${c.dim}• Greptile: AI-powered code review catches bugs before merge${c.reset}`);
|
|
1199
|
+
log(` ${c.dim}• Brandfetch: Auto-generate brand guide from company domain${c.reset}`);
|
|
461
1200
|
|
|
462
1201
|
// ─────────────────────────────────────────────────────────────────────────
|
|
463
|
-
// Step 8:
|
|
1202
|
+
// Step 8: Create .env file with API keys (if provided)
|
|
464
1203
|
// ─────────────────────────────────────────────────────────────────────────
|
|
465
1204
|
|
|
466
|
-
logStep(++currentStep, totalSteps, "
|
|
1205
|
+
logStep(++currentStep, totalSteps, "Configuring environment");
|
|
1206
|
+
log(` ${c.dim}Writing API keys and notification settings to .env${c.reset}`);
|
|
467
1207
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1208
|
+
const envPath = path.join(targetDir, ".env");
|
|
1209
|
+
let envContent = "";
|
|
1210
|
+
|
|
1211
|
+
if (fs.existsSync(envPath)) {
|
|
1212
|
+
envContent = fs.readFileSync(envPath, "utf8");
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
let envUpdated = false;
|
|
1216
|
+
|
|
1217
|
+
if (config.githubToken && !envContent.includes("GITHUB_TOKEN=")) {
|
|
1218
|
+
envContent += `\nGITHUB_TOKEN=${config.githubToken}`;
|
|
1219
|
+
envUpdated = true;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (config.greptileApiKey && !envContent.includes("GREPTILE_API_KEY=")) {
|
|
1223
|
+
envContent += `\nGREPTILE_API_KEY=${config.greptileApiKey}`;
|
|
1224
|
+
envUpdated = true;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (config.brandfetchApiKey && !envContent.includes("BRANDFETCH_API_KEY=")) {
|
|
1228
|
+
envContent += `\nBRANDFETCH_API_KEY=${config.brandfetchApiKey}`;
|
|
1229
|
+
envUpdated = true;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (config.ntfyEnabled) {
|
|
1233
|
+
if (!envContent.includes("NTFY_TOPIC=")) {
|
|
1234
|
+
envContent += `\nNTFY_TOPIC=${config.ntfyTopic}`;
|
|
1235
|
+
envUpdated = true;
|
|
1236
|
+
}
|
|
1237
|
+
if (!envContent.includes("NTFY_SERVER=")) {
|
|
1238
|
+
envContent += `\nNTFY_SERVER=${config.ntfyServer}`;
|
|
1239
|
+
envUpdated = true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (envUpdated) {
|
|
1244
|
+
fs.writeFileSync(envPath, envContent.trim() + "\n");
|
|
1245
|
+
logSuccess("Updated .env with API keys");
|
|
1246
|
+
} else {
|
|
1247
|
+
logInfo("No API keys to add (configure in .env later)");
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1251
|
+
// Step 9: Create Brand Guide (if enabled)
|
|
1252
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1253
|
+
|
|
1254
|
+
logStep(++currentStep, totalSteps, "Brand guide");
|
|
1255
|
+
log(` ${c.dim}Creating comprehensive design system documentation${c.reset}`);
|
|
1256
|
+
|
|
1257
|
+
const brandGuidePath = path.join(claudeDir, "BRAND_GUIDE.md");
|
|
1258
|
+
|
|
1259
|
+
if (config.createBrandGuide && !fs.existsSync(brandGuidePath)) {
|
|
1260
|
+
const darkModeSection = config.darkMode
|
|
1261
|
+
? `
|
|
1262
|
+
|
|
1263
|
+
## Dark Mode
|
|
1264
|
+
|
|
1265
|
+
### Dark Theme Colors
|
|
1266
|
+
- **Background**: #0F172A
|
|
1267
|
+
- **Surface**: #1E293B
|
|
1268
|
+
- **Border**: #334155
|
|
1269
|
+
- **Text**: #F8FAFC
|
|
1270
|
+
- **Muted**: #94A3B8
|
|
1271
|
+
|
|
1272
|
+
### Implementation
|
|
1273
|
+
\`\`\`css
|
|
1274
|
+
@media (prefers-color-scheme: dark) {
|
|
1275
|
+
:root {
|
|
1276
|
+
--bg: #0F172A;
|
|
1277
|
+
--surface: #1E293B;
|
|
1278
|
+
--border: #334155;
|
|
1279
|
+
--text: #F8FAFC;
|
|
1280
|
+
--muted: #94A3B8;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
\`\`\`
|
|
1284
|
+
`
|
|
1285
|
+
: "";
|
|
1286
|
+
|
|
1287
|
+
// Build brandfetch note if using domain
|
|
1288
|
+
const brandfetchNote = config.brandSource === "brandfetch" && config.brandDomain
|
|
1289
|
+
? `\n> **Source**: Auto-fetched from ${config.brandDomain} via Brandfetch API\n> Run \`/hustle-brand-refresh\` to update from latest brand assets`
|
|
1290
|
+
: "";
|
|
1291
|
+
|
|
1292
|
+
const brandGuideContent = `# ${config.brandName} Brand Guide
|
|
1293
|
+
|
|
1294
|
+
> Auto-generated by HUSTLE API Dev Tools v3.12.7
|
|
1295
|
+
> This guide ensures consistent UI across all components and pages.${brandfetchNote}
|
|
1296
|
+
|
|
1297
|
+
---
|
|
1298
|
+
|
|
1299
|
+
## Brand Identity
|
|
1300
|
+
|
|
1301
|
+
### Brand Name
|
|
1302
|
+
**${config.brandName}**
|
|
1303
|
+
|
|
1304
|
+
### Brand Colors
|
|
1305
|
+
| Role | Color | Hex | Usage |
|
|
1306
|
+
|------|-------|-----|-------|
|
|
1307
|
+
| Primary | 🔴 | \`${config.primaryColor}\` | CTAs, links, focus states |
|
|
1308
|
+
| Secondary | 🔵 | \`${config.secondaryColor}\` | Secondary actions, accents |
|
|
1309
|
+
| Accent | 🟣 | \`${config.accentColor}\` | Highlights, badges, special elements |
|
|
1310
|
+
| Success | 🟢 | \`${config.successColor}\` | Success states, confirmations |
|
|
1311
|
+
| Warning | 🟡 | \`${config.warningColor}\` | Warnings, pending states |
|
|
1312
|
+
| Error | 🔴 | \`${config.errorColor}\` | Errors, destructive actions |
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
## Color Palette
|
|
1317
|
+
|
|
1318
|
+
### Primary Colors
|
|
1319
|
+
\`\`\`css
|
|
1320
|
+
--primary: ${config.primaryColor};
|
|
1321
|
+
--primary-light: ${config.primaryColor}20; /* 20% opacity */
|
|
1322
|
+
--primary-dark: ${config.primaryColor};
|
|
1323
|
+
\`\`\`
|
|
1324
|
+
|
|
1325
|
+
### Secondary Colors
|
|
1326
|
+
\`\`\`css
|
|
1327
|
+
--secondary: ${config.secondaryColor};
|
|
1328
|
+
--secondary-light: ${config.secondaryColor}20;
|
|
1329
|
+
--secondary-dark: ${config.secondaryColor};
|
|
1330
|
+
\`\`\`
|
|
1331
|
+
|
|
1332
|
+
### Accent Colors
|
|
1333
|
+
\`\`\`css
|
|
1334
|
+
--accent: ${config.accentColor};
|
|
1335
|
+
--accent-light: ${config.accentColor}20;
|
|
1336
|
+
--accent-dark: ${config.accentColor};
|
|
1337
|
+
\`\`\`
|
|
1338
|
+
|
|
1339
|
+
### Neutral Colors (Light Theme)
|
|
1340
|
+
\`\`\`css
|
|
1341
|
+
--background: #FFFFFF;
|
|
1342
|
+
--surface: #F9FAFB;
|
|
1343
|
+
--border: #E5E7EB;
|
|
1344
|
+
--text: #111827;
|
|
1345
|
+
--text-muted: #6B7280;
|
|
1346
|
+
--text-light: #9CA3AF;
|
|
1347
|
+
\`\`\`
|
|
1348
|
+
|
|
1349
|
+
### Semantic Colors
|
|
1350
|
+
\`\`\`css
|
|
1351
|
+
--success: ${config.successColor};
|
|
1352
|
+
--success-light: ${config.successColor}20;
|
|
1353
|
+
--warning: ${config.warningColor};
|
|
1354
|
+
--warning-light: ${config.warningColor}20;
|
|
1355
|
+
--error: ${config.errorColor};
|
|
1356
|
+
--error-light: ${config.errorColor}20;
|
|
1357
|
+
--info: #3B82F6;
|
|
1358
|
+
--info-light: #DBEAFE;
|
|
1359
|
+
\`\`\`
|
|
1360
|
+
${darkModeSection}
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1363
|
+
## Typography
|
|
1364
|
+
|
|
1365
|
+
### Font Families
|
|
1366
|
+
\`\`\`css
|
|
1367
|
+
--font-primary: "${config.fontFamily}", system-ui, -apple-system, sans-serif;
|
|
1368
|
+
--font-heading: "${config.headingFont}", system-ui, -apple-system, sans-serif;
|
|
1369
|
+
--font-mono: "${config.monoFont}", "Fira Code", monospace;
|
|
1370
|
+
\`\`\`
|
|
1371
|
+
|
|
1372
|
+
### Font Scale
|
|
1373
|
+
| Name | Size | Line Height | Usage |
|
|
1374
|
+
|------|------|-------------|-------|
|
|
1375
|
+
| xs | 0.75rem (12px) | 1rem | Labels, captions |
|
|
1376
|
+
| sm | 0.875rem (14px) | 1.25rem | Secondary text |
|
|
1377
|
+
| base | 1rem (16px) | 1.5rem | Body text |
|
|
1378
|
+
| lg | 1.125rem (18px) | 1.75rem | Lead paragraphs |
|
|
1379
|
+
| xl | 1.25rem (20px) | 1.75rem | H4 |
|
|
1380
|
+
| 2xl | 1.5rem (24px) | 2rem | H3 |
|
|
1381
|
+
| 3xl | 1.875rem (30px) | 2.25rem | H2 |
|
|
1382
|
+
| 4xl | 2.25rem (36px) | 2.5rem | H1 |
|
|
1383
|
+
|
|
1384
|
+
### Font Weights
|
|
1385
|
+
- **Regular**: 400 - Body text
|
|
1386
|
+
- **Medium**: 500 - Emphasis
|
|
1387
|
+
- **Semibold**: 600 - Headings
|
|
1388
|
+
- **Bold**: 700 - Strong emphasis
|
|
1389
|
+
|
|
1390
|
+
---
|
|
1391
|
+
|
|
1392
|
+
## Spacing
|
|
1393
|
+
|
|
1394
|
+
### Spacing Scale (Tailwind-compatible)
|
|
1395
|
+
| Token | Value | Pixels | Usage |
|
|
1396
|
+
|-------|-------|--------|-------|
|
|
1397
|
+
| 1 | 0.25rem | 4px | Tight spacing |
|
|
1398
|
+
| 2 | 0.5rem | 8px | Default gap |
|
|
1399
|
+
| 3 | 0.75rem | 12px | - |
|
|
1400
|
+
| 4 | 1rem | 16px | Section padding |
|
|
1401
|
+
| 6 | 1.5rem | 24px | Card padding |
|
|
1402
|
+
| 8 | 2rem | 32px | Section gaps |
|
|
1403
|
+
| 12 | 3rem | 48px | Large sections |
|
|
1404
|
+
| 16 | 4rem | 64px | Page sections |
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
|
|
1408
|
+
## Border Radius
|
|
1409
|
+
|
|
1410
|
+
### Radius Scale
|
|
1411
|
+
\`\`\`css
|
|
1412
|
+
--radius-none: 0;
|
|
1413
|
+
--radius-sm: 2px;
|
|
1414
|
+
--radius-default: ${config.borderRadius};
|
|
1415
|
+
--radius-md: 6px;
|
|
1416
|
+
--radius-lg: 8px;
|
|
1417
|
+
--radius-xl: 12px;
|
|
1418
|
+
--radius-2xl: 16px;
|
|
1419
|
+
--radius-full: 9999px;
|
|
1420
|
+
\`\`\`
|
|
1421
|
+
|
|
1422
|
+
### Usage Guidelines
|
|
1423
|
+
- **Buttons**: Use \`--radius-default\` (${config.borderRadius})
|
|
1424
|
+
- **Cards**: Use \`--radius-lg\` or \`--radius-xl\`
|
|
1425
|
+
- **Inputs**: Use \`--radius-default\`
|
|
1426
|
+
- **Avatars**: Use \`--radius-full\`
|
|
1427
|
+
- **Modals**: Use \`--radius-xl\`
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## Shadows
|
|
1432
|
+
|
|
1433
|
+
### Shadow Scale
|
|
1434
|
+
\`\`\`css
|
|
1435
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
1436
|
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
1437
|
+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1438
|
+
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
|
1439
|
+
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
|
|
1440
|
+
\`\`\`
|
|
1441
|
+
|
|
1442
|
+
---
|
|
1443
|
+
|
|
1444
|
+
## Component Patterns
|
|
1445
|
+
|
|
1446
|
+
### Buttons
|
|
1447
|
+
|
|
1448
|
+
#### Primary Button
|
|
1449
|
+
\`\`\`jsx
|
|
1450
|
+
<button className="bg-primary text-white px-4 py-2 rounded-[${config.borderRadius}] font-medium
|
|
1451
|
+
hover:opacity-90 focus:ring-2 focus:ring-primary focus:ring-offset-2
|
|
1452
|
+
disabled:opacity-50 disabled:cursor-not-allowed">
|
|
1453
|
+
Primary Action
|
|
1454
|
+
</button>
|
|
1455
|
+
\`\`\`
|
|
1456
|
+
|
|
1457
|
+
#### Secondary Button
|
|
1458
|
+
\`\`\`jsx
|
|
1459
|
+
<button className="bg-surface border border-border text-text px-4 py-2 rounded-[${config.borderRadius}]
|
|
1460
|
+
hover:bg-gray-100 focus:ring-2 focus:ring-gray-200">
|
|
1461
|
+
Secondary
|
|
1462
|
+
</button>
|
|
1463
|
+
\`\`\`
|
|
1464
|
+
|
|
1465
|
+
#### Ghost Button
|
|
1466
|
+
\`\`\`jsx
|
|
1467
|
+
<button className="text-text px-4 py-2 rounded-[${config.borderRadius}]
|
|
1468
|
+
hover:bg-surface focus:ring-2 focus:ring-gray-200">
|
|
1469
|
+
Ghost
|
|
1470
|
+
</button>
|
|
1471
|
+
\`\`\`
|
|
1472
|
+
|
|
1473
|
+
#### Destructive Button
|
|
1474
|
+
\`\`\`jsx
|
|
1475
|
+
<button className="bg-error text-white px-4 py-2 rounded-[${config.borderRadius}]
|
|
1476
|
+
hover:bg-red-600 focus:ring-2 focus:ring-error">
|
|
1477
|
+
Delete
|
|
1478
|
+
</button>
|
|
1479
|
+
\`\`\`
|
|
1480
|
+
|
|
1481
|
+
### Cards
|
|
1482
|
+
|
|
1483
|
+
\`\`\`jsx
|
|
1484
|
+
<div className="bg-white border border-border rounded-xl p-6 shadow-sm">
|
|
1485
|
+
<h3 className="text-lg font-semibold text-text">Card Title</h3>
|
|
1486
|
+
<p className="text-muted text-sm mt-2">Card description text.</p>
|
|
1487
|
+
</div>
|
|
1488
|
+
\`\`\`
|
|
1489
|
+
|
|
1490
|
+
### Form Inputs
|
|
1491
|
+
|
|
1492
|
+
\`\`\`jsx
|
|
1493
|
+
<div>
|
|
1494
|
+
<label className="text-sm font-medium text-text mb-1 block">
|
|
1495
|
+
Label
|
|
1496
|
+
</label>
|
|
1497
|
+
<input
|
|
1498
|
+
type="text"
|
|
1499
|
+
className="w-full border border-border rounded-[${config.borderRadius}] px-3 py-2
|
|
1500
|
+
focus:ring-2 focus:ring-primary focus:border-primary
|
|
1501
|
+
placeholder:text-muted"
|
|
1502
|
+
placeholder="Enter value..."
|
|
1503
|
+
/>
|
|
1504
|
+
<p className="text-error text-sm mt-1">Error message</p>
|
|
1505
|
+
</div>
|
|
1506
|
+
\`\`\`
|
|
1507
|
+
|
|
1508
|
+
### Badges
|
|
1509
|
+
|
|
1510
|
+
\`\`\`jsx
|
|
1511
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
1512
|
+
bg-primary-light text-primary">
|
|
1513
|
+
Badge
|
|
1514
|
+
</span>
|
|
1515
|
+
\`\`\`
|
|
1516
|
+
|
|
1517
|
+
---
|
|
1518
|
+
|
|
1519
|
+
## Accessibility
|
|
1520
|
+
|
|
1521
|
+
### Color Contrast
|
|
1522
|
+
- Text on background: minimum 4.5:1 ratio
|
|
1523
|
+
- Large text (18px+): minimum 3:1 ratio
|
|
1524
|
+
- Interactive elements: minimum 3:1 ratio
|
|
1525
|
+
|
|
1526
|
+
### Focus States
|
|
1527
|
+
All interactive elements must have visible focus states:
|
|
1528
|
+
\`\`\`css
|
|
1529
|
+
:focus-visible {
|
|
1530
|
+
outline: 2px solid ${config.primaryColor};
|
|
1531
|
+
outline-offset: 2px;
|
|
1532
|
+
}
|
|
1533
|
+
\`\`\`
|
|
1534
|
+
|
|
1535
|
+
### Touch Targets
|
|
1536
|
+
- Minimum size: 44x44 pixels
|
|
1537
|
+
- Minimum spacing: 8px between targets
|
|
1538
|
+
|
|
1539
|
+
---
|
|
1540
|
+
|
|
1541
|
+
## Icons
|
|
1542
|
+
|
|
1543
|
+
### Icon Style: ${config.iconStyle.charAt(0).toUpperCase() + config.iconStyle.slice(1)}
|
|
1544
|
+
${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." : ""}
|
|
1545
|
+
|
|
1546
|
+
### Recommended Icon Libraries
|
|
1547
|
+
${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" : ""}
|
|
1548
|
+
|
|
1549
|
+
### Icon Sizes
|
|
1550
|
+
| Size | Pixels | Usage |
|
|
1551
|
+
|------|--------|-------|
|
|
1552
|
+
| xs | 12px | Inline with small text |
|
|
1553
|
+
| sm | 16px | Buttons, inline |
|
|
1554
|
+
| md | 20px | Default |
|
|
1555
|
+
| lg | 24px | Headers, standalone |
|
|
1556
|
+
| xl | 32px | Hero sections |
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
## Visual Style
|
|
1561
|
+
|
|
1562
|
+
### Image Style: ${config.imageStyle.charAt(0).toUpperCase() + config.imageStyle.slice(1)}
|
|
1563
|
+
${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." : ""}
|
|
1564
|
+
|
|
1565
|
+
### Card Style: ${config.cardStyle.charAt(0).toUpperCase() + config.cardStyle.slice(1)}
|
|
1566
|
+
\`\`\`jsx
|
|
1567
|
+
// ${config.cardStyle} card pattern
|
|
1568
|
+
<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">
|
|
1569
|
+
<h3 className="text-lg font-semibold">Card Title</h3>
|
|
1570
|
+
<p className="text-muted text-sm mt-2">Card content</p>
|
|
1571
|
+
</div>
|
|
1572
|
+
\`\`\`
|
|
1573
|
+
|
|
1574
|
+
---
|
|
1575
|
+
|
|
1576
|
+
## Animation
|
|
1577
|
+
|
|
1578
|
+
### Animation Level: ${config.animationLevel.charAt(0).toUpperCase() + config.animationLevel.slice(1)}
|
|
1579
|
+
${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." : ""}
|
|
1580
|
+
|
|
1581
|
+
### Timing
|
|
1582
|
+
\`\`\`css
|
|
1583
|
+
--duration-fast: 150ms;
|
|
1584
|
+
--duration-normal: 200ms;
|
|
1585
|
+
--duration-slow: 300ms;
|
|
1586
|
+
--easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1587
|
+
\`\`\`
|
|
1588
|
+
|
|
1589
|
+
${config.animationLevel !== "none" ? `### Common Animations
|
|
1590
|
+
\`\`\`css
|
|
1591
|
+
.fade-in { animation: fadeIn var(--duration-normal) var(--easing); }
|
|
1592
|
+
.slide-up { animation: slideUp var(--duration-normal) var(--easing); }
|
|
1593
|
+
.scale-in { animation: scaleIn var(--duration-fast) var(--easing); }
|
|
1594
|
+
\`\`\`` : "### No Animations\nAll animations are disabled. Use CSS \\`transition: none\\` globally."}
|
|
1595
|
+
|
|
1596
|
+
---
|
|
1597
|
+
|
|
1598
|
+
## Tailwind Config
|
|
1599
|
+
|
|
1600
|
+
\`\`\`js
|
|
1601
|
+
// tailwind.config.js
|
|
1602
|
+
module.exports = {
|
|
1603
|
+
theme: {
|
|
1604
|
+
extend: {
|
|
1605
|
+
colors: {
|
|
1606
|
+
primary: '${config.primaryColor}',
|
|
1607
|
+
secondary: '${config.secondaryColor}',
|
|
1608
|
+
accent: '${config.accentColor}',
|
|
1609
|
+
success: '${config.successColor}',
|
|
1610
|
+
warning: '${config.warningColor}',
|
|
1611
|
+
error: '${config.errorColor}',
|
|
1612
|
+
},
|
|
1613
|
+
fontFamily: {
|
|
1614
|
+
sans: ['${config.fontFamily}', 'system-ui', 'sans-serif'],
|
|
1615
|
+
heading: ['${config.headingFont}', 'system-ui', 'sans-serif'],
|
|
1616
|
+
mono: ['${config.monoFont}', 'Fira Code', 'monospace'],
|
|
1617
|
+
},
|
|
1618
|
+
borderRadius: {
|
|
1619
|
+
DEFAULT: '${config.borderRadius}',
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
}
|
|
1624
|
+
\`\`\`
|
|
1625
|
+
|
|
1626
|
+
---
|
|
1627
|
+
|
|
1628
|
+
*Last updated: ${new Date().toISOString().split("T")[0]}*
|
|
1629
|
+
`;
|
|
1630
|
+
fs.writeFileSync(brandGuidePath, brandGuideContent);
|
|
1631
|
+
logSuccess("Created .claude/BRAND_GUIDE.md");
|
|
1632
|
+
} else if (fs.existsSync(brandGuidePath)) {
|
|
1633
|
+
logInfo("Brand guide already exists");
|
|
1634
|
+
} else {
|
|
1635
|
+
logInfo("Skipped brand guide creation");
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1639
|
+
// Step 10: Optional Tools
|
|
1640
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1641
|
+
|
|
1642
|
+
logStep(++currentStep, totalSteps, "Testing tools");
|
|
1643
|
+
|
|
1644
|
+
// Check prerequisites
|
|
1645
|
+
const hasPackageJson = fs.existsSync(path.join(targetDir, "package.json"));
|
|
1646
|
+
const hasPnpm = (() => {
|
|
1647
|
+
try {
|
|
1648
|
+
execSync("pnpm --version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
1649
|
+
return true;
|
|
1650
|
+
} catch {
|
|
1651
|
+
return false;
|
|
483
1652
|
}
|
|
1653
|
+
})();
|
|
1654
|
+
|
|
1655
|
+
if (!hasPackageJson) {
|
|
1656
|
+
log(` ${c.dim}No package.json found - skipping npm package installations${c.reset}`);
|
|
1657
|
+
logInfo("Run 'pnpm init' first, then re-run installer to add testing tools");
|
|
1658
|
+
} else if (!hasPnpm) {
|
|
1659
|
+
log(` ${c.dim}pnpm not found - skipping installations${c.reset}`);
|
|
1660
|
+
logInfo("Install pnpm: npm install -g pnpm");
|
|
1661
|
+
} else {
|
|
1662
|
+
log(` ${c.dim}Installing Playwright, Storybook, and Sandpack${c.reset}`);
|
|
1663
|
+
|
|
1664
|
+
if (config.withStorybook || config.withPlaywright || config.withSandpack) {
|
|
1665
|
+
if (config.withSandpack) {
|
|
1666
|
+
startSpinner("Installing Sandpack (live code preview)...");
|
|
1667
|
+
try {
|
|
1668
|
+
execSync("pnpm add @codesandbox/sandpack-react 2>&1", {
|
|
1669
|
+
cwd: targetDir,
|
|
1670
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1671
|
+
});
|
|
1672
|
+
stopSpinner(true, "Sandpack installed - enables live UI previews");
|
|
1673
|
+
} catch (e) {
|
|
1674
|
+
stopSpinner(false, "Sandpack failed");
|
|
1675
|
+
logInfo(" Run manually: pnpm add @codesandbox/sandpack-react");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
484
1678
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1679
|
+
if (config.withStorybook) {
|
|
1680
|
+
// Check if Storybook is already initialized
|
|
1681
|
+
const hasStorybook = fs.existsSync(path.join(targetDir, ".storybook"));
|
|
1682
|
+
if (hasStorybook) {
|
|
1683
|
+
logInfo("Storybook already configured");
|
|
1684
|
+
} else {
|
|
1685
|
+
startSpinner("Initializing Storybook (component development)...");
|
|
1686
|
+
try {
|
|
1687
|
+
execSync("pnpm dlx storybook@latest init --yes 2>&1", {
|
|
1688
|
+
cwd: targetDir,
|
|
1689
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1690
|
+
timeout: 300000,
|
|
1691
|
+
});
|
|
1692
|
+
stopSpinner(true, "Storybook initialized - run 'pnpm storybook' to start");
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
stopSpinner(false, "Storybook failed");
|
|
1695
|
+
logInfo(" Run manually: pnpm dlx storybook@latest init");
|
|
1696
|
+
logInfo(" Requires: React/Vue/Angular/Svelte project");
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
499
1699
|
}
|
|
500
|
-
}
|
|
501
1700
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1701
|
+
if (config.withPlaywright) {
|
|
1702
|
+
// Check if Playwright is already initialized
|
|
1703
|
+
const hasPlaywright = fs.existsSync(path.join(targetDir, "playwright.config.ts")) ||
|
|
1704
|
+
fs.existsSync(path.join(targetDir, "playwright.config.js"));
|
|
1705
|
+
if (hasPlaywright) {
|
|
1706
|
+
logInfo("Playwright already configured");
|
|
1707
|
+
} else {
|
|
1708
|
+
startSpinner("Initializing Playwright (E2E testing)...");
|
|
1709
|
+
try {
|
|
1710
|
+
execSync("pnpm create playwright --yes 2>&1", {
|
|
1711
|
+
cwd: targetDir,
|
|
1712
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1713
|
+
timeout: 300000,
|
|
1714
|
+
});
|
|
1715
|
+
stopSpinner(true, "Playwright initialized - run 'pnpm exec playwright test' to start");
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
stopSpinner(false, "Playwright failed");
|
|
1718
|
+
logInfo(" Run manually: pnpm create playwright");
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
516
1721
|
}
|
|
1722
|
+
} else {
|
|
1723
|
+
logInfo("None selected");
|
|
1724
|
+
logInfo("Run wizard again with Custom Setup to add testing tools");
|
|
517
1725
|
}
|
|
518
|
-
} else {
|
|
519
|
-
logInfo("None selected");
|
|
520
|
-
logInfo(
|
|
521
|
-
"Add later with: --with-storybook --with-playwright --with-sandpack",
|
|
522
|
-
);
|
|
523
1726
|
}
|
|
524
1727
|
|
|
525
1728
|
// ─────────────────────────────────────────────────────────────────────────
|
|
526
1729
|
// Summary
|
|
527
1730
|
// ─────────────────────────────────────────────────────────────────────────
|
|
528
1731
|
|
|
1732
|
+
const check = `${c.white}✓${c.reset}`;
|
|
1733
|
+
const cross = `${c.dim}○${c.reset}`;
|
|
1734
|
+
|
|
529
1735
|
log(`
|
|
530
1736
|
${c.red}═══════════════════════════════════════════════════════════════${c.reset}
|
|
1737
|
+
${c.red}${c.bold} HUSTLE${c.reset}
|
|
531
1738
|
${c.bold} Installation Complete${c.reset}
|
|
532
1739
|
${c.red}═══════════════════════════════════════════════════════════════${c.reset}
|
|
533
1740
|
|
|
534
|
-
${c.bold}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
${
|
|
542
|
-
${
|
|
543
|
-
${
|
|
544
|
-
${
|
|
545
|
-
${
|
|
1741
|
+
${c.bold}Core Components:${c.reset}
|
|
1742
|
+
${check} Commands .claude/commands/ (29 slash commands)
|
|
1743
|
+
${check} Hooks .claude/hooks/ (enforcement hooks)
|
|
1744
|
+
${check} Subagents .claude/agents/ (parallel processing)
|
|
1745
|
+
${check} Config .claude/ (settings, state, registry)
|
|
1746
|
+
|
|
1747
|
+
${c.bold}Configuration:${c.reset}
|
|
1748
|
+
${config.githubToken ? check : cross} GITHUB_TOKEN ${config.githubToken ? "configured" : "not set"}
|
|
1749
|
+
${config.greptileApiKey ? check : cross} GREPTILE_API_KEY ${config.greptileApiKey ? "configured" : "not set"}
|
|
1750
|
+
${config.brandfetchApiKey ? check : cross} BRANDFETCH_API_KEY ${config.brandfetchApiKey ? "configured" : "not set"}
|
|
1751
|
+
${config.ntfyEnabled ? check : cross} NTFY Notifications ${config.ntfyEnabled ? config.ntfyTopic : "disabled"}
|
|
1752
|
+
${config.createBrandGuide ? check : cross} Brand Guide ${config.createBrandGuide ? (config.brandSource === "brandfetch" ? `from ${config.brandDomain}` : "manual") : "skipped"}
|
|
1753
|
+
|
|
1754
|
+
${c.bold}Testing Tools:${c.reset}
|
|
1755
|
+
${config.withPlaywright ? check : cross} Playwright ${config.withPlaywright ? "installed" : "not installed"}
|
|
1756
|
+
${config.withStorybook ? check : cross} Storybook ${config.withStorybook ? "installed" : "not installed"}
|
|
1757
|
+
${config.withSandpack ? check : cross} Sandpack ${config.withSandpack ? "installed" : "not installed"}
|
|
1758
|
+
|
|
1759
|
+
${c.bold}Ready to Use:${c.reset}
|
|
1760
|
+
${c.gray}$${c.reset} /hustle-api-create [endpoint] ${c.dim}# Build API endpoint${c.reset}
|
|
1761
|
+
${c.gray}$${c.reset} /hustle-ui-create [component] ${c.dim}# Build component${c.reset}
|
|
1762
|
+
${c.gray}$${c.reset} /hustle-ui-create-page [page] ${c.dim}# Build page${c.reset}
|
|
1763
|
+
${c.gray}$${c.reset} /hustle-combine [apis] ${c.dim}# Orchestrate APIs${c.reset}
|
|
546
1764
|
|
|
547
1765
|
${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
|
|
1766
|
+
${!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
1767
|
|
|
554
1768
|
${c.dim}Documentation: https://github.com/hustle-together/api-dev-tools${c.reset}
|
|
555
1769
|
`);
|