@hustle-together/api-dev-tools 3.11.1 → 3.12.2

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 +305 -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
@@ -7,9 +7,9 @@
7
7
  * @generated by @hustle-together/api-dev-tools v3.0
8
8
  */
9
9
 
10
- import { NextRequest, NextResponse } from 'next/server';
11
- import fs from 'fs';
12
- import path from 'path';
10
+ import { NextRequest, NextResponse } from "next/server";
11
+ import fs from "fs";
12
+ import path from "path";
13
13
 
14
14
  // ============================================
15
15
  // Types
@@ -18,7 +18,7 @@ import path from 'path';
18
18
  interface TestCase {
19
19
  name: string;
20
20
  line: number;
21
- status: 'pending' | 'passed' | 'failed' | 'skipped';
21
+ status: "pending" | "passed" | "failed" | "skipped";
22
22
  duration?: number;
23
23
  error?: string;
24
24
  }
@@ -58,7 +58,7 @@ interface TestStructure {
58
58
  * Uses brace counting to handle nested structures
59
59
  */
60
60
  function parseTestFile(content: string, filePath: string): TestFeature {
61
- const lines = content.split('\n');
61
+ const lines = content.split("\n");
62
62
  const rootGroups: TestGroup[] = [];
63
63
  const groupStack: TestGroup[] = [];
64
64
 
@@ -77,7 +77,7 @@ function parseTestFile(content: string, filePath: string): TestFeature {
77
77
  name: describeMatch[1],
78
78
  line: lineNum,
79
79
  tests: [],
80
- groups: []
80
+ groups: [],
81
81
  };
82
82
 
83
83
  if (groupStack.length > 0) {
@@ -97,12 +97,12 @@ function parseTestFile(content: string, filePath: string): TestFeature {
97
97
  const testCase: TestCase = {
98
98
  name: testMatch[1],
99
99
  line: lineNum,
100
- status: 'pending' // Will be updated by test runner results
100
+ status: "pending", // Will be updated by test runner results
101
101
  };
102
102
 
103
103
  // Check for .skip or .todo
104
- if (line.includes('.skip') || line.includes('.todo')) {
105
- testCase.status = 'skipped';
104
+ if (line.includes(".skip") || line.includes(".todo")) {
105
+ testCase.status = "skipped";
106
106
  }
107
107
 
108
108
  groupStack[groupStack.length - 1].tests.push(testCase);
@@ -114,7 +114,11 @@ function parseTestFile(content: string, filePath: string): TestFeature {
114
114
  braceCount += openBraces - closeBraces;
115
115
 
116
116
  // Pop group from stack when we close its scope
117
- if (inDescribe && braceCount <= currentDescribeBraceStart && groupStack.length > 0) {
117
+ if (
118
+ inDescribe &&
119
+ braceCount <= currentDescribeBraceStart &&
120
+ groupStack.length > 0
121
+ ) {
118
122
  groupStack.pop();
119
123
  if (groupStack.length === 0) {
120
124
  inDescribe = false;
@@ -126,15 +130,20 @@ function parseTestFile(content: string, filePath: string): TestFeature {
126
130
  }
127
131
 
128
132
  // Count tests
129
- const countTests = (groups: TestGroup[]): { total: number; passed: number; failed: number; skipped: number } => {
130
- let total = 0, passed = 0, failed = 0, skipped = 0;
133
+ const countTests = (
134
+ groups: TestGroup[],
135
+ ): { total: number; passed: number; failed: number; skipped: number } => {
136
+ let total = 0,
137
+ passed = 0,
138
+ failed = 0,
139
+ skipped = 0;
131
140
 
132
141
  for (const group of groups) {
133
142
  for (const test of group.tests) {
134
143
  total++;
135
- if (test.status === 'passed') passed++;
136
- else if (test.status === 'failed') failed++;
137
- else if (test.status === 'skipped') skipped++;
144
+ if (test.status === "passed") passed++;
145
+ else if (test.status === "failed") failed++;
146
+ else if (test.status === "skipped") skipped++;
138
147
  }
139
148
  const nested = countTests(group.groups);
140
149
  total += nested.total;
@@ -155,7 +164,7 @@ function parseTestFile(content: string, filePath: string): TestFeature {
155
164
  totalTests: counts.total,
156
165
  passedTests: counts.passed,
157
166
  failedTests: counts.failed,
158
- skippedTests: counts.skipped
167
+ skippedTests: counts.skipped,
159
168
  };
160
169
  }
161
170
 
@@ -165,12 +174,12 @@ function parseTestFile(content: string, filePath: string): TestFeature {
165
174
  function findTestFiles(baseDir: string): string[] {
166
175
  const testFiles: string[] = [];
167
176
  const testPatterns = [
168
- '**/*.test.ts',
169
- '**/*.test.tsx',
170
- '**/*.spec.ts',
171
- '**/*.spec.tsx',
172
- '**/__tests__/**/*.ts',
173
- '**/__tests__/**/*.tsx'
177
+ "**/*.test.ts",
178
+ "**/*.test.tsx",
179
+ "**/*.spec.ts",
180
+ "**/*.spec.tsx",
181
+ "**/__tests__/**/*.ts",
182
+ "**/__tests__/**/*.tsx",
174
183
  ];
175
184
 
176
185
  function walkDir(dir: string) {
@@ -182,17 +191,17 @@ function findTestFiles(baseDir: string): string[] {
182
191
 
183
192
  // Skip node_modules and hidden directories
184
193
  if (entry.isDirectory()) {
185
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
194
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
186
195
  continue;
187
196
  }
188
197
  walkDir(fullPath);
189
198
  } else if (entry.isFile()) {
190
199
  // Check if file matches test patterns
191
200
  if (
192
- entry.name.endsWith('.test.ts') ||
193
- entry.name.endsWith('.test.tsx') ||
194
- entry.name.endsWith('.spec.ts') ||
195
- entry.name.endsWith('.spec.tsx')
201
+ entry.name.endsWith(".test.ts") ||
202
+ entry.name.endsWith(".test.tsx") ||
203
+ entry.name.endsWith(".spec.ts") ||
204
+ entry.name.endsWith(".spec.tsx")
196
205
  ) {
197
206
  testFiles.push(fullPath);
198
207
  }
@@ -214,7 +223,7 @@ function findTestFiles(baseDir: string): string[] {
214
223
  export async function GET(request: NextRequest) {
215
224
  try {
216
225
  const { searchParams } = new URL(request.url);
217
- const testPath = searchParams.get('path');
226
+ const testPath = searchParams.get("path");
218
227
  const baseDir = process.cwd();
219
228
 
220
229
  let testFiles: string[];
@@ -225,7 +234,7 @@ export async function GET(request: NextRequest) {
225
234
  if (!fs.existsSync(fullPath)) {
226
235
  return NextResponse.json(
227
236
  { error: `Test file not found: ${testPath}` },
228
- { status: 404 }
237
+ { status: 404 },
229
238
  );
230
239
  }
231
240
  testFiles = [fullPath];
@@ -238,7 +247,7 @@ export async function GET(request: NextRequest) {
238
247
 
239
248
  for (const file of testFiles) {
240
249
  try {
241
- const content = fs.readFileSync(file, 'utf-8');
250
+ const content = fs.readFileSync(file, "utf-8");
242
251
  const relativePath = path.relative(baseDir, file);
243
252
  const feature = parseTestFile(content, relativePath);
244
253
  features.push(feature);
@@ -255,15 +264,15 @@ export async function GET(request: NextRequest) {
255
264
  passedTests: features.reduce((sum, f) => sum + f.passedTests, 0),
256
265
  failedTests: features.reduce((sum, f) => sum + f.failedTests, 0),
257
266
  skippedTests: features.reduce((sum, f) => sum + f.skippedTests, 0),
258
- parsedAt: new Date().toISOString()
267
+ parsedAt: new Date().toISOString(),
259
268
  };
260
269
 
261
270
  return NextResponse.json(structure);
262
271
  } catch (error) {
263
- console.error('Test structure parser error:', error);
272
+ console.error("Test structure parser error:", error);
264
273
  return NextResponse.json(
265
- { error: 'Failed to parse test structure', details: String(error) },
266
- { status: 500 }
274
+ { error: "Failed to parse test structure", details: String(error) },
275
+ { status: 500 },
267
276
  );
268
277
  }
269
278
  }
@@ -1,5 +1,5 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { __COMPONENT_NAME__ } from "./__COMPONENT_NAME__";
3
3
 
4
4
  /**
5
5
  * __COMPONENT_NAME__ - __COMPONENT_DESCRIPTION__
@@ -7,51 +7,51 @@ import { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
7
7
  * This component was created using the Hustle UI Create workflow.
8
8
  */
9
9
  const meta: Meta<typeof __COMPONENT_NAME__> = {
10
- title: 'Components/__COMPONENT_NAME__',
10
+ title: "Components/__COMPONENT_NAME__",
11
11
  component: __COMPONENT_NAME__,
12
12
  parameters: {
13
- layout: 'centered',
13
+ layout: "centered",
14
14
  docs: {
15
15
  description: {
16
- component: '__COMPONENT_DESCRIPTION__',
16
+ component: "__COMPONENT_DESCRIPTION__",
17
17
  },
18
18
  },
19
19
  },
20
- tags: ['autodocs'],
20
+ tags: ["autodocs"],
21
21
  argTypes: {
22
22
  variant: {
23
- control: 'select',
24
- options: ['primary', 'secondary', 'destructive', 'outline', 'ghost'],
25
- description: 'Visual style variant',
23
+ control: "select",
24
+ options: ["primary", "secondary", "destructive", "outline", "ghost"],
25
+ description: "Visual style variant",
26
26
  table: {
27
- defaultValue: { summary: 'primary' },
27
+ defaultValue: { summary: "primary" },
28
28
  },
29
29
  },
30
30
  size: {
31
- control: 'select',
32
- options: ['sm', 'md', 'lg'],
33
- description: 'Size variant',
31
+ control: "select",
32
+ options: ["sm", "md", "lg"],
33
+ description: "Size variant",
34
34
  table: {
35
- defaultValue: { summary: 'md' },
35
+ defaultValue: { summary: "md" },
36
36
  },
37
37
  },
38
38
  loading: {
39
- control: 'boolean',
40
- description: 'Shows loading spinner',
39
+ control: "boolean",
40
+ description: "Shows loading spinner",
41
41
  table: {
42
- defaultValue: { summary: 'false' },
42
+ defaultValue: { summary: "false" },
43
43
  },
44
44
  },
45
45
  disabled: {
46
- control: 'boolean',
47
- description: 'Disables the component',
46
+ control: "boolean",
47
+ description: "Disables the component",
48
48
  table: {
49
- defaultValue: { summary: 'false' },
49
+ defaultValue: { summary: "false" },
50
50
  },
51
51
  },
52
52
  children: {
53
- control: 'text',
54
- description: 'Content inside the component',
53
+ control: "text",
54
+ description: "Content inside the component",
55
55
  },
56
56
  },
57
57
  };
@@ -64,8 +64,8 @@ type Story = StoryObj<typeof meta>;
64
64
  */
65
65
  export const Primary: Story = {
66
66
  args: {
67
- variant: 'primary',
68
- children: 'Primary __COMPONENT_NAME__',
67
+ variant: "primary",
68
+ children: "Primary __COMPONENT_NAME__",
69
69
  },
70
70
  };
71
71
 
@@ -74,8 +74,8 @@ export const Primary: Story = {
74
74
  */
75
75
  export const Secondary: Story = {
76
76
  args: {
77
- variant: 'secondary',
78
- children: 'Secondary __COMPONENT_NAME__',
77
+ variant: "secondary",
78
+ children: "Secondary __COMPONENT_NAME__",
79
79
  },
80
80
  };
81
81
 
@@ -84,8 +84,8 @@ export const Secondary: Story = {
84
84
  */
85
85
  export const Destructive: Story = {
86
86
  args: {
87
- variant: 'destructive',
88
- children: 'Delete Item',
87
+ variant: "destructive",
88
+ children: "Delete Item",
89
89
  },
90
90
  };
91
91
 
@@ -94,8 +94,8 @@ export const Destructive: Story = {
94
94
  */
95
95
  export const Outline: Story = {
96
96
  args: {
97
- variant: 'outline',
98
- children: 'Outline __COMPONENT_NAME__',
97
+ variant: "outline",
98
+ children: "Outline __COMPONENT_NAME__",
99
99
  },
100
100
  };
101
101
 
@@ -104,8 +104,8 @@ export const Outline: Story = {
104
104
  */
105
105
  export const Ghost: Story = {
106
106
  args: {
107
- variant: 'ghost',
108
- children: 'Ghost __COMPONENT_NAME__',
107
+ variant: "ghost",
108
+ children: "Ghost __COMPONENT_NAME__",
109
109
  },
110
110
  };
111
111
 
@@ -114,8 +114,8 @@ export const Ghost: Story = {
114
114
  */
115
115
  export const Small: Story = {
116
116
  args: {
117
- size: 'sm',
118
- children: 'Small __COMPONENT_NAME__',
117
+ size: "sm",
118
+ children: "Small __COMPONENT_NAME__",
119
119
  },
120
120
  };
121
121
 
@@ -124,8 +124,8 @@ export const Small: Story = {
124
124
  */
125
125
  export const Large: Story = {
126
126
  args: {
127
- size: 'lg',
128
- children: 'Large __COMPONENT_NAME__',
127
+ size: "lg",
128
+ children: "Large __COMPONENT_NAME__",
129
129
  },
130
130
  };
131
131
 
@@ -135,7 +135,7 @@ export const Large: Story = {
135
135
  export const Loading: Story = {
136
136
  args: {
137
137
  loading: true,
138
- children: 'Loading...',
138
+ children: "Loading...",
139
139
  },
140
140
  };
141
141
 
@@ -145,7 +145,7 @@ export const Loading: Story = {
145
145
  export const Disabled: Story = {
146
146
  args: {
147
147
  disabled: true,
148
- children: 'Disabled __COMPONENT_NAME__',
148
+ children: "Disabled __COMPONENT_NAME__",
149
149
  },
150
150
  };
151
151
 
@@ -158,7 +158,9 @@ export const AllVariants: Story = {
158
158
  <div className="flex gap-2">
159
159
  <__COMPONENT_NAME__ variant="primary">Primary</__COMPONENT_NAME__>
160
160
  <__COMPONENT_NAME__ variant="secondary">Secondary</__COMPONENT_NAME__>
161
- <__COMPONENT_NAME__ variant="destructive">Destructive</__COMPONENT_NAME__>
161
+ <__COMPONENT_NAME__ variant="destructive">
162
+ Destructive
163
+ </__COMPONENT_NAME__>
162
164
  <__COMPONENT_NAME__ variant="outline">Outline</__COMPONENT_NAME__>
163
165
  <__COMPONENT_NAME__ variant="ghost">Ghost</__COMPONENT_NAME__>
164
166
  </div>