@hustle-together/api-dev-tools 3.11.1 → 3.12.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.
Files changed (139) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +228 -56
  9. package/.claude/commands/README.md +21 -10
  10. package/.claude/commands/add-command.md +8 -5
  11. package/.claude/commands/api-create.md +36 -25
  12. package/.claude/commands/api-env.md +1 -0
  13. package/.claude/commands/api-interview.md +32 -19
  14. package/.claude/commands/api-research.md +47 -21
  15. package/.claude/commands/api-status.md +21 -1
  16. package/.claude/commands/api-verify.md +14 -13
  17. package/.claude/commands/beepboop.md +4 -5
  18. package/.claude/commands/busycommit.md +2 -3
  19. package/.claude/commands/commit.md +2 -3
  20. package/.claude/commands/cycle.md +2 -7
  21. package/.claude/commands/gap.md +2 -3
  22. package/.claude/commands/green.md +2 -7
  23. package/.claude/commands/issue.md +3 -8
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +2 -3
  27. package/.claude/commands/pr.md +2 -3
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +2 -7
  30. package/.claude/commands/refactor.md +2 -7
  31. package/.claude/commands/spike.md +2 -7
  32. package/.claude/commands/summarize.md +2 -3
  33. package/.claude/commands/tdd.md +2 -7
  34. package/.claude/commands/worktree-add.md +208 -216
  35. package/.claude/commands/worktree-cleanup.md +172 -178
  36. package/.claude/settings.json +63 -12
  37. package/.claude/settings.local.json +2 -1
  38. package/.claude-plugin/marketplace.json +2 -11
  39. package/.skills/README.md +55 -53
  40. package/.skills/_shared/settings.json +1 -1
  41. package/.skills/add-command/SKILL.md +10 -5
  42. package/.skills/api-create/SKILL.md +146 -35
  43. package/.skills/api-env/SKILL.md +1 -0
  44. package/.skills/api-interview/SKILL.md +32 -19
  45. package/.skills/api-research/SKILL.md +47 -21
  46. package/.skills/api-status/SKILL.md +21 -1
  47. package/.skills/api-verify/SKILL.md +14 -13
  48. package/.skills/beepboop/SKILL.md +6 -5
  49. package/.skills/busycommit/SKILL.md +4 -3
  50. package/.skills/commit/SKILL.md +4 -3
  51. package/.skills/cycle/SKILL.md +4 -7
  52. package/.skills/gap/SKILL.md +4 -3
  53. package/.skills/green/SKILL.md +4 -7
  54. package/.skills/issue/SKILL.md +5 -8
  55. package/.skills/plan/SKILL.md +4 -3
  56. package/.skills/pr/SKILL.md +4 -3
  57. package/.skills/publish/SKILL.md +160 -0
  58. package/.skills/red/SKILL.md +4 -7
  59. package/.skills/refactor/SKILL.md +4 -7
  60. package/.skills/spike/SKILL.md +4 -7
  61. package/.skills/summarize/SKILL.md +4 -3
  62. package/.skills/tdd/SKILL.md +4 -7
  63. package/.skills/update-todos/SKILL.md +22 -0
  64. package/.skills/worktree-add/SKILL.md +210 -216
  65. package/.skills/worktree-cleanup/SKILL.md +183 -187
  66. package/CHANGELOG.md +97 -79
  67. package/README.md +161 -7142
  68. package/bin/cli.js +448 -805
  69. package/commands/README.md +66 -31
  70. package/commands/add-command.md +8 -5
  71. package/commands/beepboop.md +4 -5
  72. package/commands/busycommit.md +2 -3
  73. package/commands/commit.md +2 -3
  74. package/commands/cycle.md +2 -7
  75. package/commands/gap.md +2 -3
  76. package/commands/green.md +2 -7
  77. package/commands/hustle-api-continue.md +8 -5
  78. package/commands/hustle-api-create.md +70 -29
  79. package/commands/hustle-api-env.md +1 -0
  80. package/commands/hustle-api-interview.md +32 -19
  81. package/commands/hustle-api-research.md +47 -21
  82. package/commands/hustle-api-sessions.md +8 -7
  83. package/commands/hustle-api-status.md +21 -1
  84. package/commands/hustle-api-verify.md +14 -13
  85. package/commands/hustle-combine.md +488 -241
  86. package/commands/hustle-ui-create-page.md +113 -50
  87. package/commands/hustle-ui-create.md +179 -26
  88. package/commands/issue.md +3 -8
  89. package/commands/plan.md +2 -3
  90. package/commands/pr.md +2 -3
  91. package/commands/red.md +2 -7
  92. package/commands/refactor.md +2 -7
  93. package/commands/spike.md +2 -7
  94. package/commands/summarize.md +2 -3
  95. package/commands/tdd.md +2 -7
  96. package/commands/worktree-add.md +208 -216
  97. package/commands/worktree-cleanup.md +172 -178
  98. package/hooks/api-workflow-check.py +5 -3
  99. package/hooks/enforce-component-type-confirm.py +97 -0
  100. package/hooks/lib/__init__.py +1 -0
  101. package/hooks/lib/greptile.py +355 -0
  102. package/hooks/lib/ntfy.py +209 -0
  103. package/hooks/notify-input-needed.py +73 -0
  104. package/hooks/notify-phase-complete.py +90 -0
  105. package/hooks/run-code-review.py +246 -0
  106. package/hooks/track-token-usage.py +121 -0
  107. package/package.json +13 -3
  108. package/scripts/collect-test-results.ts +102 -77
  109. package/scripts/extract-parameters.ts +112 -70
  110. package/scripts/generate-test-manifest.ts +118 -77
  111. package/templates/.env.example +57 -0
  112. package/templates/BRAND_GUIDE.md +92 -52
  113. package/templates/CLAUDE-SECTION.md +40 -37
  114. package/templates/SPEC.json +186 -38
  115. package/templates/api-dev-state.json +33 -4
  116. package/templates/api-showcase/_components/APICard.tsx +22 -18
  117. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  118. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  119. package/templates/api-showcase/_components/APITester.tsx +128 -67
  120. package/templates/api-showcase/page.tsx +4 -4
  121. package/templates/api-test/page.tsx +51 -30
  122. package/templates/api-test/test-structure/route.ts +43 -34
  123. package/templates/component/Component.stories.tsx +41 -39
  124. package/templates/component/Component.test.tsx +96 -78
  125. package/templates/component/Component.tsx +63 -52
  126. package/templates/component/Component.types.ts +10 -6
  127. package/templates/component/Component.visual.spec.ts +170 -0
  128. package/templates/component/index.ts +2 -2
  129. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  130. package/templates/dev-tools/page.tsx +4 -3
  131. package/templates/mcp-servers.json +30 -2
  132. package/templates/page/page.e2e.test.ts +56 -48
  133. package/templates/page/page.tsx +3 -3
  134. package/templates/shared/HeroHeader.tsx +16 -15
  135. package/templates/shared/index.ts +1 -1
  136. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  137. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  138. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  139. package/templates/ui-showcase/page.tsx +4 -4
@@ -11,9 +11,9 @@
11
11
  * @generated by @hustle-together/api-dev-tools v3.0
12
12
  */
13
13
 
14
- import { execSync, spawn } from 'child_process';
15
- import fs from 'fs';
16
- import path from 'path';
14
+ import { execSync, spawn } from "child_process";
15
+ import fs from "fs";
16
+ import path from "path";
17
17
 
18
18
  // ============================================
19
19
  // Types
@@ -22,7 +22,7 @@ import path from 'path';
22
22
  interface TestResult {
23
23
  name: string;
24
24
  file: string;
25
- status: 'passed' | 'failed' | 'skipped';
25
+ status: "passed" | "failed" | "skipped";
26
26
  duration: number;
27
27
  error?: string;
28
28
  }
@@ -74,7 +74,7 @@ function parseVitestJson(jsonOutput: string): CollectedResults {
74
74
  passed: 0,
75
75
  failed: 0,
76
76
  skipped: 0,
77
- duration: fileResult.duration || 0
77
+ duration: fileResult.duration || 0,
78
78
  };
79
79
 
80
80
  if (fileResult.assertionResults) {
@@ -82,19 +82,23 @@ function parseVitestJson(jsonOutput: string): CollectedResults {
82
82
  const result: TestResult = {
83
83
  name: test.title || test.fullName,
84
84
  file: suite.file,
85
- status: test.status === 'passed' ? 'passed' :
86
- test.status === 'failed' ? 'failed' : 'skipped',
87
- duration: test.duration || 0
85
+ status:
86
+ test.status === "passed"
87
+ ? "passed"
88
+ : test.status === "failed"
89
+ ? "failed"
90
+ : "skipped",
91
+ duration: test.duration || 0,
88
92
  };
89
93
 
90
94
  if (test.failureMessages && test.failureMessages.length > 0) {
91
- result.error = test.failureMessages.join('\n');
95
+ result.error = test.failureMessages.join("\n");
92
96
  }
93
97
 
94
98
  suite.tests.push(result);
95
99
 
96
- if (result.status === 'passed') suite.passed++;
97
- else if (result.status === 'failed') suite.failed++;
100
+ if (result.status === "passed") suite.passed++;
101
+ else if (result.status === "failed") suite.failed++;
98
102
  else suite.skipped++;
99
103
  }
100
104
  }
@@ -109,7 +113,7 @@ function parseVitestJson(jsonOutput: string): CollectedResults {
109
113
  }
110
114
 
111
115
  return {
112
- version: '3.0.0',
116
+ version: "3.0.0",
113
117
  collectedAt: new Date().toISOString(),
114
118
  suites,
115
119
  summary: {
@@ -119,8 +123,8 @@ function parseVitestJson(jsonOutput: string): CollectedResults {
119
123
  failed: totalFailed,
120
124
  skipped: totalSkipped,
121
125
  duration: totalDuration,
122
- success: totalFailed === 0
123
- }
126
+ success: totalFailed === 0,
127
+ },
124
128
  };
125
129
  } catch (error) {
126
130
  throw new Error(`Failed to parse Vitest JSON output: ${error}`);
@@ -135,11 +139,13 @@ function parseVitestConsole(output: string): CollectedResults {
135
139
  const suites: TestSuiteResult[] = [];
136
140
  let currentSuite: TestSuiteResult | null = null;
137
141
 
138
- const lines = output.split('\n');
142
+ const lines = output.split("\n");
139
143
 
140
144
  for (const line of lines) {
141
145
  // Match file header: ✓ src/path/file.test.ts (5 tests) 123ms
142
- const fileMatch = line.match(/[✓✗◯]\s+([^\s]+\.(?:test|spec)\.tsx?)\s+\((\d+)\s+tests?\)/);
146
+ const fileMatch = line.match(
147
+ /[✓✗◯]\s+([^\s]+\.(?:test|spec)\.tsx?)\s+\((\d+)\s+tests?\)/,
148
+ );
143
149
  if (fileMatch) {
144
150
  if (currentSuite) {
145
151
  suites.push(currentSuite);
@@ -153,7 +159,7 @@ function parseVitestConsole(output: string): CollectedResults {
153
159
  passed: 0,
154
160
  failed: 0,
155
161
  skipped: 0,
156
- duration: durationMatch ? parseInt(durationMatch[1]) : 0
162
+ duration: durationMatch ? parseInt(durationMatch[1]) : 0,
157
163
  };
158
164
  continue;
159
165
  }
@@ -163,19 +169,18 @@ function parseVitestConsole(output: string): CollectedResults {
163
169
  if (testMatch && currentSuite) {
164
170
  const [, icon, name, duration] = testMatch;
165
171
 
166
- const status: 'passed' | 'failed' | 'skipped' =
167
- icon === '' ? 'passed' :
168
- icon === '✗' ? 'failed' : 'skipped';
172
+ const status: "passed" | "failed" | "skipped" =
173
+ icon === "" ? "passed" : icon === "✗" ? "failed" : "skipped";
169
174
 
170
175
  currentSuite.tests.push({
171
176
  name,
172
177
  file: currentSuite.file,
173
178
  status,
174
- duration: duration ? parseInt(duration) : 0
179
+ duration: duration ? parseInt(duration) : 0,
175
180
  });
176
181
 
177
- if (status === 'passed') currentSuite.passed++;
178
- else if (status === 'failed') currentSuite.failed++;
182
+ if (status === "passed") currentSuite.passed++;
183
+ else if (status === "failed") currentSuite.failed++;
179
184
  else currentSuite.skipped++;
180
185
  }
181
186
  }
@@ -185,29 +190,32 @@ function parseVitestConsole(output: string): CollectedResults {
185
190
  }
186
191
 
187
192
  // Calculate summary
188
- const summary = suites.reduce((acc, suite) => ({
189
- totalSuites: acc.totalSuites + 1,
190
- totalTests: acc.totalTests + suite.tests.length,
191
- passed: acc.passed + suite.passed,
192
- failed: acc.failed + suite.failed,
193
- skipped: acc.skipped + suite.skipped,
194
- duration: acc.duration + suite.duration,
195
- success: acc.success && suite.failed === 0
196
- }), {
197
- totalSuites: 0,
198
- totalTests: 0,
199
- passed: 0,
200
- failed: 0,
201
- skipped: 0,
202
- duration: 0,
203
- success: true
204
- });
193
+ const summary = suites.reduce(
194
+ (acc, suite) => ({
195
+ totalSuites: acc.totalSuites + 1,
196
+ totalTests: acc.totalTests + suite.tests.length,
197
+ passed: acc.passed + suite.passed,
198
+ failed: acc.failed + suite.failed,
199
+ skipped: acc.skipped + suite.skipped,
200
+ duration: acc.duration + suite.duration,
201
+ success: acc.success && suite.failed === 0,
202
+ }),
203
+ {
204
+ totalSuites: 0,
205
+ totalTests: 0,
206
+ passed: 0,
207
+ failed: 0,
208
+ skipped: 0,
209
+ duration: 0,
210
+ success: true,
211
+ },
212
+ );
205
213
 
206
214
  return {
207
- version: '3.0.0',
215
+ version: "3.0.0",
208
216
  collectedAt: new Date().toISOString(),
209
217
  suites,
210
- summary
218
+ summary,
211
219
  };
212
220
  }
213
221
 
@@ -216,20 +224,20 @@ function parseVitestConsole(output: string): CollectedResults {
216
224
  // ============================================
217
225
 
218
226
  function runVitest(baseDir: string, filter?: string): CollectedResults {
219
- console.log('🧪 Running Vitest...');
227
+ console.log("🧪 Running Vitest...");
220
228
 
221
- const vitestArgs = ['vitest', 'run', '--reporter=json'];
229
+ const vitestArgs = ["vitest", "run", "--reporter=json"];
222
230
  if (filter) {
223
231
  vitestArgs.push(filter);
224
232
  }
225
233
 
226
234
  try {
227
235
  // Try running with JSON reporter
228
- const result = execSync(`npx ${vitestArgs.join(' ')}`, {
236
+ const result = execSync(`npx ${vitestArgs.join(" ")}`, {
229
237
  cwd: baseDir,
230
- encoding: 'utf-8',
231
- stdio: ['pipe', 'pipe', 'pipe'],
232
- maxBuffer: 50 * 1024 * 1024 // 50MB buffer
238
+ encoding: "utf-8",
239
+ stdio: ["pipe", "pipe", "pipe"],
240
+ maxBuffer: 50 * 1024 * 1024, // 50MB buffer
233
241
  });
234
242
 
235
243
  return parseVitestJson(result);
@@ -247,13 +255,13 @@ function runVitest(baseDir: string, filter?: string): CollectedResults {
247
255
  }
248
256
 
249
257
  // Try fallback: run without JSON reporter
250
- console.log(' ⚠️ JSON reporter failed, trying console output...');
258
+ console.log(" ⚠️ JSON reporter failed, trying console output...");
251
259
 
252
260
  try {
253
- const consoleResult = execSync(`npx vitest run ${filter || ''}`, {
261
+ const consoleResult = execSync(`npx vitest run ${filter || ""}`, {
254
262
  cwd: baseDir,
255
- encoding: 'utf-8',
256
- stdio: ['pipe', 'pipe', 'pipe']
263
+ encoding: "utf-8",
264
+ stdio: ["pipe", "pipe", "pipe"],
257
265
  });
258
266
 
259
267
  return parseVitestConsole(consoleResult);
@@ -273,11 +281,11 @@ function runVitest(baseDir: string, filter?: string): CollectedResults {
273
281
 
274
282
  function updateManifest(manifestPath: string, results: CollectedResults): void {
275
283
  if (!fs.existsSync(manifestPath)) {
276
- console.log(' ⚠️ Manifest not found, skipping update');
284
+ console.log(" ⚠️ Manifest not found, skipping update");
277
285
  return;
278
286
  }
279
287
 
280
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
288
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
281
289
 
282
290
  // Create a map of test results by file
283
291
  const resultsByFile = new Map<string, TestSuiteResult>();
@@ -289,7 +297,7 @@ function updateManifest(manifestPath: string, results: CollectedResults): void {
289
297
  // Update each endpoint's test status
290
298
  if (manifest.endpoints) {
291
299
  for (const endpoint of manifest.endpoints) {
292
- const testBasename = path.basename(endpoint.testFile || '');
300
+ const testBasename = path.basename(endpoint.testFile || "");
293
301
  const suiteResult = resultsByFile.get(testBasename);
294
302
 
295
303
  if (suiteResult) {
@@ -298,7 +306,7 @@ function updateManifest(manifestPath: string, results: CollectedResults): void {
298
306
  failed: suiteResult.failed,
299
307
  skipped: suiteResult.skipped,
300
308
  duration: suiteResult.duration,
301
- lastRun: results.collectedAt
309
+ lastRun: results.collectedAt,
302
310
  };
303
311
  }
304
312
  }
@@ -307,7 +315,7 @@ function updateManifest(manifestPath: string, results: CollectedResults): void {
307
315
  // Update summary
308
316
  manifest.lastTestRun = {
309
317
  ...results.summary,
310
- timestamp: results.collectedAt
318
+ timestamp: results.collectedAt,
311
319
  };
312
320
 
313
321
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
@@ -322,13 +330,25 @@ function main() {
322
330
  const args = process.argv.slice(2);
323
331
  const baseDir = args[0] || process.cwd();
324
332
  const filter = args[1] || undefined;
325
- const outputPath = args[2] || path.join(baseDir, 'src', 'app', 'api-test', 'test-results.json');
326
- const manifestPath = path.join(baseDir, 'src', 'app', 'api-test', 'api-tests-manifest.json');
327
-
328
- console.log('═══════════════════════════════════════════════════════════════');
329
- console.log(' 🧪 Test Results Collector');
330
- console.log(' @hustle-together/api-dev-tools v3.0');
331
- console.log('═══════════════════════════════════════════════════════════════');
333
+ const outputPath =
334
+ args[2] ||
335
+ path.join(baseDir, "src", "app", "api-test", "test-results.json");
336
+ const manifestPath = path.join(
337
+ baseDir,
338
+ "src",
339
+ "app",
340
+ "api-test",
341
+ "api-tests-manifest.json",
342
+ );
343
+
344
+ console.log(
345
+ "═══════════════════════════════════════════════════════════════",
346
+ );
347
+ console.log(" 🧪 Test Results Collector");
348
+ console.log(" @hustle-together/api-dev-tools v3.0");
349
+ console.log(
350
+ "═══════════════════════════════════════════════════════════════",
351
+ );
332
352
  console.log(`\n📁 Base directory: ${baseDir}`);
333
353
  if (filter) {
334
354
  console.log(`🔍 Filter: ${filter}`);
@@ -350,13 +370,17 @@ function main() {
350
370
  // Update manifest with results
351
371
  updateManifest(manifestPath, results);
352
372
 
353
- console.log('\n═══════════════════════════════════════════════════════════════');
373
+ console.log(
374
+ "\n═══════════════════════════════════════════════════════════════",
375
+ );
354
376
  if (results.summary.success) {
355
- console.log(' ✅ All tests passed!');
377
+ console.log(" ✅ All tests passed!");
356
378
  } else {
357
- console.log(' ❌ Some tests failed');
379
+ console.log(" ❌ Some tests failed");
358
380
  }
359
- console.log('═══════════════════════════════════════════════════════════════');
381
+ console.log(
382
+ "═══════════════════════════════════════════════════════════════",
383
+ );
360
384
 
361
385
  console.log(`\n📊 Summary:`);
362
386
  console.log(` • Suites: ${results.summary.totalSuites}`);
@@ -371,12 +395,14 @@ function main() {
371
395
  console.log(` • Duration: ${results.summary.duration}ms`);
372
396
 
373
397
  // List failed tests
374
- const failedTests = results.suites.flatMap(suite =>
375
- suite.tests.filter(t => t.status === 'failed').map(t => ({
376
- file: suite.file,
377
- name: t.name,
378
- error: t.error
379
- }))
398
+ const failedTests = results.suites.flatMap((suite) =>
399
+ suite.tests
400
+ .filter((t) => t.status === "failed")
401
+ .map((t) => ({
402
+ file: suite.file,
403
+ name: t.name,
404
+ error: t.error,
405
+ })),
380
406
  );
381
407
 
382
408
  if (failedTests.length > 0) {
@@ -384,7 +410,7 @@ function main() {
384
410
  for (const test of failedTests) {
385
411
  console.log(` • ${test.file}: ${test.name}`);
386
412
  if (test.error) {
387
- console.log(` ${test.error.split('\n')[0]}`);
413
+ console.log(` ${test.error.split("\n")[0]}`);
388
414
  }
389
415
  }
390
416
  }
@@ -394,9 +420,8 @@ function main() {
394
420
 
395
421
  // Exit with appropriate code
396
422
  process.exit(results.summary.success ? 0 : 1);
397
-
398
423
  } catch (error) {
399
- console.error('\n❌ Failed to collect test results:', error);
424
+ console.error("\n❌ Failed to collect test results:", error);
400
425
  process.exit(1);
401
426
  }
402
427
  }