@hustle-together/api-dev-tools 3.12.16 → 4.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
@@ -91,32 +91,38 @@ test.describe("__COMPONENT_NAME__ Visual Regression", () => {
91
91
  });
92
92
 
93
93
  // ===================================
94
- // Responsive Viewport Tests
94
+ // Responsive Viewport Tests (7 Viewports)
95
95
  // ===================================
96
96
 
97
- test("renders correctly on mobile viewport", async ({ page }) => {
98
- await page.setViewportSize({ width: 375, height: 667 });
99
- await page.goto(
100
- `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
101
- );
102
- await page.waitForLoadState("networkidle");
103
-
104
- await expect(page.locator("#storybook-root")).toHaveScreenshot(
105
- "__COMPONENT_NAME__-mobile.png",
106
- );
107
- });
108
-
109
- test("renders correctly on tablet viewport", async ({ page }) => {
110
- await page.setViewportSize({ width: 768, height: 1024 });
111
- await page.goto(
112
- `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
113
- );
114
- await page.waitForLoadState("networkidle");
115
-
116
- await expect(page.locator("#storybook-root")).toHaveScreenshot(
117
- "__COMPONENT_NAME__-tablet.png",
118
- );
119
- });
97
+ // All 7 viewports from performance-budgets.json
98
+ const viewports = [
99
+ { name: "mobile-portrait", width: 375, height: 667 },
100
+ { name: "mobile-notch", width: 393, height: 852 },
101
+ { name: "mobile-landscape", width: 667, height: 375 },
102
+ { name: "tablet-portrait", width: 768, height: 1024 },
103
+ { name: "tablet-landscape", width: 1024, height: 768 },
104
+ { name: "small-desktop", width: 1280, height: 720 },
105
+ { name: "desktop", width: 1920, height: 1080 },
106
+ ];
107
+
108
+ for (const viewport of viewports) {
109
+ test(`renders correctly on ${viewport.name} (${viewport.width}x${viewport.height})`, async ({
110
+ page,
111
+ }) => {
112
+ await page.setViewportSize({
113
+ width: viewport.width,
114
+ height: viewport.height,
115
+ });
116
+ await page.goto(
117
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
118
+ );
119
+ await page.waitForLoadState("networkidle");
120
+
121
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
122
+ `__COMPONENT_NAME__-${viewport.name}.png`,
123
+ );
124
+ });
125
+ }
120
126
 
121
127
  // ===================================
122
128
  // Interaction State Tests
@@ -0,0 +1,446 @@
1
+ /**
2
+ * ESLint Plugin for Zod Schema Linting
3
+ *
4
+ * Enforces best practices for Zod schemas:
5
+ * - require-description: All schemas should have .describe()
6
+ * - consistent-naming: Keys should follow naming convention
7
+ * - require-error-message: String validations should have error messages
8
+ * - no-unsafe-defaults: Avoid empty/zero defaults
9
+ * - prefer-strict: Objects should use .strict()
10
+ *
11
+ * Installation:
12
+ * 1. Copy this to your project's eslint-plugin-zod-schema/index.js
13
+ * 2. Add to eslint.config.js (see bottom of file)
14
+ *
15
+ * @version 1.0.0
16
+ * @see docs/SCHEMA-LINT.md
17
+ */
18
+
19
+ // Helper: Check if node is a Zod method call
20
+ function isZodCall(node) {
21
+ if (node.type !== 'CallExpression') return false;
22
+
23
+ // Check for z.string(), z.object(), etc.
24
+ if (
25
+ node.callee.type === 'MemberExpression' &&
26
+ node.callee.object.name === 'z'
27
+ ) {
28
+ return true;
29
+ }
30
+
31
+ // Check for chained calls like z.string().email()
32
+ if (
33
+ node.callee.type === 'MemberExpression' &&
34
+ node.callee.object.type === 'CallExpression'
35
+ ) {
36
+ return isZodCall(node.callee.object);
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ // Helper: Check if chain has .describe()
43
+ function hasDescribe(node) {
44
+ let current = node;
45
+ while (current) {
46
+ if (
47
+ current.type === 'CallExpression' &&
48
+ current.callee.type === 'MemberExpression' &&
49
+ current.callee.property.name === 'describe'
50
+ ) {
51
+ return true;
52
+ }
53
+ // Move up the chain
54
+ if (current.callee && current.callee.object) {
55
+ current = current.callee.object;
56
+ } else {
57
+ break;
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+
63
+ // Helper: Check naming convention
64
+ function matchesCase(name, caseType) {
65
+ if (caseType === 'camelCase') {
66
+ return /^[a-z][a-zA-Z0-9]*$/.test(name);
67
+ }
68
+ if (caseType === 'snake_case') {
69
+ return /^[a-z][a-z0-9_]*$/.test(name);
70
+ }
71
+ return true;
72
+ }
73
+
74
+ // Helper: Check if node is inside z.object()
75
+ function isInsideZodObject(node, context) {
76
+ const ancestors = context.getAncestors();
77
+ return ancestors.some(
78
+ (ancestor) =>
79
+ ancestor.type === 'CallExpression' &&
80
+ ancestor.callee.type === 'MemberExpression' &&
81
+ ancestor.callee.object.name === 'z' &&
82
+ ancestor.callee.property.name === 'object'
83
+ );
84
+ }
85
+
86
+ // Helper: Check if string validation has error message
87
+ function hasErrorMessage(node) {
88
+ // Check for { message: '...' } in validation call
89
+ if (node.arguments && node.arguments.length > 0) {
90
+ const lastArg = node.arguments[node.arguments.length - 1];
91
+ if (lastArg.type === 'ObjectExpression') {
92
+ return lastArg.properties.some(
93
+ (prop) => prop.key && prop.key.name === 'message'
94
+ );
95
+ }
96
+ // Some validations accept message as string directly
97
+ if (lastArg.type === 'Literal' && typeof lastArg.value === 'string') {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+
104
+ // Helper: Check for unsafe defaults
105
+ function isUnsafeDefault(node) {
106
+ if (
107
+ node.callee.type === 'MemberExpression' &&
108
+ node.callee.property.name === 'default'
109
+ ) {
110
+ const defaultArg = node.arguments[0];
111
+ if (!defaultArg) return true; // No argument is unsafe
112
+
113
+ // Empty string
114
+ if (defaultArg.type === 'Literal' && defaultArg.value === '') {
115
+ return true;
116
+ }
117
+ // Zero for numbers (might be intentional, but warn)
118
+ if (defaultArg.type === 'Literal' && defaultArg.value === 0) {
119
+ return true;
120
+ }
121
+ // null
122
+ if (defaultArg.type === 'Literal' && defaultArg.value === null) {
123
+ return true;
124
+ }
125
+ // Empty array
126
+ if (
127
+ defaultArg.type === 'ArrayExpression' &&
128
+ defaultArg.elements.length === 0
129
+ ) {
130
+ return true;
131
+ }
132
+ // Empty object
133
+ if (
134
+ defaultArg.type === 'ObjectExpression' &&
135
+ defaultArg.properties.length === 0
136
+ ) {
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+
143
+ // String validation methods that should have error messages
144
+ const STRING_VALIDATIONS = [
145
+ 'email',
146
+ 'url',
147
+ 'uuid',
148
+ 'cuid',
149
+ 'regex',
150
+ 'min',
151
+ 'max',
152
+ 'length',
153
+ 'startsWith',
154
+ 'endsWith',
155
+ 'includes',
156
+ ];
157
+
158
+ module.exports = {
159
+ meta: {
160
+ name: 'eslint-plugin-zod-schema',
161
+ version: '1.0.0',
162
+ },
163
+ rules: {
164
+ /**
165
+ * Require .describe() on all Zod schemas
166
+ */
167
+ 'require-description': {
168
+ meta: {
169
+ type: 'suggestion',
170
+ docs: {
171
+ description: 'Require .describe() on Zod schemas for documentation',
172
+ category: 'Best Practices',
173
+ recommended: true,
174
+ },
175
+ messages: {
176
+ missingDescribe:
177
+ 'Zod schema should have .describe() for documentation and OpenAPI generation',
178
+ },
179
+ schema: [],
180
+ },
181
+ create(context) {
182
+ return {
183
+ VariableDeclarator(node) {
184
+ // Only check schemas (variables ending with Schema or containing schema)
185
+ const varName = node.id.name || '';
186
+ if (
187
+ !varName.toLowerCase().includes('schema') &&
188
+ !varName.endsWith('Schema')
189
+ ) {
190
+ return;
191
+ }
192
+
193
+ if (node.init && isZodCall(node.init) && !hasDescribe(node.init)) {
194
+ context.report({
195
+ node: node.init,
196
+ messageId: 'missingDescribe',
197
+ });
198
+ }
199
+ },
200
+ };
201
+ },
202
+ },
203
+
204
+ /**
205
+ * Enforce consistent naming in z.object() keys
206
+ */
207
+ 'consistent-naming': {
208
+ meta: {
209
+ type: 'problem',
210
+ docs: {
211
+ description: 'Enforce consistent naming convention in schema keys',
212
+ category: 'Stylistic Issues',
213
+ recommended: true,
214
+ },
215
+ messages: {
216
+ inconsistentCase: "Schema key '{{name}}' should be {{case}}",
217
+ },
218
+ schema: [
219
+ {
220
+ type: 'object',
221
+ properties: {
222
+ case: {
223
+ enum: ['camelCase', 'snake_case'],
224
+ default: 'camelCase',
225
+ },
226
+ },
227
+ additionalProperties: false,
228
+ },
229
+ ],
230
+ },
231
+ create(context) {
232
+ const options = context.options[0] || {};
233
+ const caseType = options.case || 'camelCase';
234
+
235
+ return {
236
+ Property(node) {
237
+ // Only check inside z.object()
238
+ if (!isInsideZodObject(node, context)) {
239
+ return;
240
+ }
241
+
242
+ const keyName = node.key.name || node.key.value;
243
+ if (keyName && !matchesCase(keyName, caseType)) {
244
+ context.report({
245
+ node: node.key,
246
+ messageId: 'inconsistentCase',
247
+ data: {
248
+ name: keyName,
249
+ case: caseType,
250
+ },
251
+ });
252
+ }
253
+ },
254
+ };
255
+ },
256
+ },
257
+
258
+ /**
259
+ * Require custom error messages on string validations
260
+ */
261
+ 'require-error-message': {
262
+ meta: {
263
+ type: 'suggestion',
264
+ docs: {
265
+ description:
266
+ 'Require custom error messages on string validation methods',
267
+ category: 'Best Practices',
268
+ recommended: false,
269
+ },
270
+ messages: {
271
+ missingErrorMessage:
272
+ "String validation '{{method}}' should have a custom error message",
273
+ },
274
+ schema: [],
275
+ },
276
+ create(context) {
277
+ return {
278
+ CallExpression(node) {
279
+ if (
280
+ node.callee.type === 'MemberExpression' &&
281
+ STRING_VALIDATIONS.includes(node.callee.property.name) &&
282
+ isZodCall(node)
283
+ ) {
284
+ if (!hasErrorMessage(node)) {
285
+ context.report({
286
+ node,
287
+ messageId: 'missingErrorMessage',
288
+ data: {
289
+ method: node.callee.property.name,
290
+ },
291
+ });
292
+ }
293
+ }
294
+ },
295
+ };
296
+ },
297
+ },
298
+
299
+ /**
300
+ * Warn about potentially unsafe default values
301
+ */
302
+ 'no-unsafe-defaults': {
303
+ meta: {
304
+ type: 'suggestion',
305
+ docs: {
306
+ description:
307
+ 'Warn about empty/zero default values that might cause issues',
308
+ category: 'Best Practices',
309
+ recommended: true,
310
+ },
311
+ messages: {
312
+ unsafeDefault:
313
+ 'Default value might be unsafe. Empty strings, zeros, nulls, and empty arrays/objects can cause issues. Use .optional() instead or provide a meaningful default.',
314
+ },
315
+ schema: [],
316
+ },
317
+ create(context) {
318
+ return {
319
+ CallExpression(node) {
320
+ if (isZodCall(node) && isUnsafeDefault(node)) {
321
+ context.report({
322
+ node,
323
+ messageId: 'unsafeDefault',
324
+ });
325
+ }
326
+ },
327
+ };
328
+ },
329
+ },
330
+
331
+ /**
332
+ * Prefer .strict() on z.object() to prevent extra properties
333
+ */
334
+ 'prefer-strict': {
335
+ meta: {
336
+ type: 'suggestion',
337
+ docs: {
338
+ description:
339
+ 'Prefer .strict() on z.object() to reject extra properties',
340
+ category: 'Best Practices',
341
+ recommended: false,
342
+ },
343
+ messages: {
344
+ preferStrict:
345
+ 'Consider using .strict() on z.object() to reject extra properties and prevent data leaks',
346
+ },
347
+ schema: [],
348
+ },
349
+ create(context) {
350
+ return {
351
+ CallExpression(node) {
352
+ // Check for z.object() without .strict()
353
+ if (
354
+ node.callee.type === 'MemberExpression' &&
355
+ node.callee.object.name === 'z' &&
356
+ node.callee.property.name === 'object'
357
+ ) {
358
+ // Check if parent chain includes .strict()
359
+ const parent = node.parent;
360
+ if (
361
+ parent &&
362
+ parent.type === 'MemberExpression' &&
363
+ parent.property.name === 'strict'
364
+ ) {
365
+ return; // Has .strict()
366
+ }
367
+
368
+ // Check full chain
369
+ let current = node.parent;
370
+ while (current) {
371
+ if (
372
+ current.type === 'CallExpression' &&
373
+ current.callee.type === 'MemberExpression' &&
374
+ current.callee.property.name === 'strict'
375
+ ) {
376
+ return; // Has .strict() somewhere in chain
377
+ }
378
+ current = current.parent;
379
+ }
380
+
381
+ context.report({
382
+ node,
383
+ messageId: 'preferStrict',
384
+ });
385
+ }
386
+ },
387
+ };
388
+ },
389
+ },
390
+ },
391
+
392
+ // Recommended config
393
+ configs: {
394
+ recommended: {
395
+ plugins: ['zod-schema'],
396
+ rules: {
397
+ 'zod-schema/require-description': 'warn',
398
+ 'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
399
+ 'zod-schema/no-unsafe-defaults': 'warn',
400
+ 'zod-schema/require-error-message': 'off',
401
+ 'zod-schema/prefer-strict': 'off',
402
+ },
403
+ },
404
+ strict: {
405
+ plugins: ['zod-schema'],
406
+ rules: {
407
+ 'zod-schema/require-description': 'error',
408
+ 'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
409
+ 'zod-schema/no-unsafe-defaults': 'error',
410
+ 'zod-schema/require-error-message': 'warn',
411
+ 'zod-schema/prefer-strict': 'warn',
412
+ },
413
+ },
414
+ },
415
+ };
416
+
417
+ /*
418
+ * USAGE IN eslint.config.js:
419
+ *
420
+ * import zodSchemaPlugin from './eslint-plugin-zod-schema';
421
+ *
422
+ * export default [
423
+ * {
424
+ * files: ['**\/*.schema.ts', '**\/*.schemas.ts'],
425
+ * plugins: {
426
+ * 'zod-schema': zodSchemaPlugin,
427
+ * },
428
+ * rules: {
429
+ * 'zod-schema/require-description': 'warn',
430
+ * 'zod-schema/consistent-naming': ['error', { case: 'camelCase' }],
431
+ * 'zod-schema/no-unsafe-defaults': 'warn',
432
+ * },
433
+ * },
434
+ * ];
435
+ *
436
+ * Or use the recommended config:
437
+ *
438
+ * import zodSchemaPlugin from './eslint-plugin-zod-schema';
439
+ *
440
+ * export default [
441
+ * {
442
+ * files: ['**\/*.schema.ts'],
443
+ * ...zodSchemaPlugin.configs.recommended,
444
+ * },
445
+ * ];
446
+ */
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "eslint-plugin-zod-schema",
3
+ "version": "1.0.0",
4
+ "description": "ESLint plugin for Zod schema best practices",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "eslint",
8
+ "eslintplugin",
9
+ "eslint-plugin",
10
+ "zod",
11
+ "schema",
12
+ "validation"
13
+ ],
14
+ "author": "Hustle Together",
15
+ "license": "MIT",
16
+ "peerDependencies": {
17
+ "eslint": ">=8.0.0"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/hustle-together/api-dev-tools"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ }