@agjs/tsforge 0.1.0

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 (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * THEMED COMPONENT MATERIALIZATION — the harness holds tested primitive STRUCTURE
3
+ * and a set of VIBE themes; the model calls one tool (`scaffold_ui`) to get a
4
+ * coherent component set without authoring (or re-authoring) any of it. Two layers,
5
+ * both hardcoded:
6
+ *
7
+ * 1. tokens — a design-token block (`:root` palette + `--radius`) written into
8
+ * `index.css`. THE big lever: every primitive references semantic classes
9
+ * (`bg-card`, `rounded-md`), so swapping the token preset reskins the whole set
10
+ * coherently (minimal = neutral; warm = amber + round; futuristic = cool-neon +
11
+ * sharp). One block decides ~90% of the look.
12
+ * 2. deltas — optional per-component extra classes, merged via `cn(...)` into the
13
+ * primitive's root for finer per-vibe identity. STRUCTURE is invariant; only
14
+ * these classes vary, and most are empty (the tokens carry the vibe).
15
+ *
16
+ * The SDK philosophy applied to UI: tested structure + a parameterized surface. The
17
+ * model writes app logic, never plumbing. All primitives are dependency-free (plain
18
+ * HTML elements + cva), so a scaffolded project needs no extra Radix install and the
19
+ * set stays offline-testable. They land under `src/components/ui/` (eslint-ignored,
20
+ * vite tree-shakes unused ones from the bundle).
21
+ */
22
+
23
+ /** The vibes the model can request. */
24
+ export type ThemeName = "minimal" | "warm" | "futuristic";
25
+
26
+ /** The primitives `scaffold_ui` can materialize. */
27
+ export type ComponentName =
28
+ // primitives (atoms)
29
+ | "button"
30
+ | "card"
31
+ | "input"
32
+ | "label"
33
+ | "textarea"
34
+ | "select"
35
+ | "badge"
36
+ | "separator"
37
+ | "table"
38
+ // composition blocks (molecules) — the view chrome the model otherwise re-rolls
39
+ | "app-shell"
40
+ | "page-header"
41
+ | "field"
42
+ | "form-actions"
43
+ | "toolbar"
44
+ | "empty-state";
45
+
46
+ export const COMPONENT_NAMES: readonly ComponentName[] = [
47
+ "button",
48
+ "card",
49
+ "input",
50
+ "label",
51
+ "textarea",
52
+ "select",
53
+ "badge",
54
+ "separator",
55
+ "table",
56
+ "app-shell",
57
+ "page-header",
58
+ "field",
59
+ "form-actions",
60
+ "toolbar",
61
+ "empty-state",
62
+ ];
63
+
64
+ export const THEME_NAMES: readonly ThemeName[] = [
65
+ "minimal",
66
+ "warm",
67
+ "futuristic",
68
+ ];
69
+
70
+ export interface ITheme {
71
+ name: ThemeName;
72
+ /** The `index.css` token block (Tailwind import + `:root` + `@theme`). */
73
+ tokens: string;
74
+ /** Per-component extra classes merged into the root via `cn`. Missing ⇒ none. */
75
+ deltas: Partial<Record<ComponentName, string>>;
76
+ }
77
+
78
+ // ─── token presets (the vibe lever) ──────────────────────────────────────────
79
+
80
+ function tokenBlock(vars: string): string {
81
+ return `@import "tailwindcss";
82
+
83
+ @custom-variant dark (&:is(.dark *));
84
+
85
+ :root {
86
+ ${vars}
87
+ }
88
+
89
+ @theme inline {
90
+ --radius-sm: calc(var(--radius) - 2px);
91
+ --radius-md: var(--radius);
92
+ --radius-lg: calc(var(--radius) + 2px);
93
+ --color-background: var(--background);
94
+ --color-foreground: var(--foreground);
95
+ --color-card: var(--card);
96
+ --color-card-foreground: var(--card-foreground);
97
+ --color-primary: var(--primary);
98
+ --color-primary-foreground: var(--primary-foreground);
99
+ --color-secondary: var(--secondary);
100
+ --color-secondary-foreground: var(--secondary-foreground);
101
+ --color-muted: var(--muted);
102
+ --color-muted-foreground: var(--muted-foreground);
103
+ --color-accent: var(--accent);
104
+ --color-accent-foreground: var(--accent-foreground);
105
+ --color-destructive: var(--destructive);
106
+ --color-border: var(--border);
107
+ --color-input: var(--input);
108
+ --color-ring: var(--ring);
109
+ }
110
+
111
+ @layer base {
112
+ * {
113
+ @apply border-border;
114
+ }
115
+ body {
116
+ @apply bg-background text-foreground;
117
+ }
118
+ }
119
+ `;
120
+ }
121
+
122
+ const VARS_MINIMAL = ` --radius: 0.5rem;
123
+ --background: oklch(1 0 0);
124
+ --foreground: oklch(0.145 0 0);
125
+ --card: oklch(1 0 0);
126
+ --card-foreground: oklch(0.145 0 0);
127
+ --primary: oklch(0.205 0 0);
128
+ --primary-foreground: oklch(0.985 0 0);
129
+ --secondary: oklch(0.97 0 0);
130
+ --secondary-foreground: oklch(0.205 0 0);
131
+ --muted: oklch(0.97 0 0);
132
+ --muted-foreground: oklch(0.556 0 0);
133
+ --accent: oklch(0.97 0 0);
134
+ --accent-foreground: oklch(0.205 0 0);
135
+ --destructive: oklch(0.577 0.245 27.325);
136
+ --border: oklch(0.922 0 0);
137
+ --input: oklch(0.922 0 0);
138
+ --ring: oklch(0.708 0 0);`;
139
+
140
+ const VARS_WARM = ` --radius: 0.875rem;
141
+ --background: oklch(0.99 0.01 85);
142
+ --foreground: oklch(0.24 0.03 50);
143
+ --card: oklch(0.98 0.02 85);
144
+ --card-foreground: oklch(0.24 0.03 50);
145
+ --primary: oklch(0.66 0.16 50);
146
+ --primary-foreground: oklch(0.99 0.01 85);
147
+ --secondary: oklch(0.94 0.04 80);
148
+ --secondary-foreground: oklch(0.34 0.05 50);
149
+ --muted: oklch(0.95 0.03 85);
150
+ --muted-foreground: oklch(0.52 0.04 55);
151
+ --accent: oklch(0.9 0.07 75);
152
+ --accent-foreground: oklch(0.32 0.06 50);
153
+ --destructive: oklch(0.58 0.22 25);
154
+ --border: oklch(0.89 0.04 80);
155
+ --input: oklch(0.89 0.04 80);
156
+ --ring: oklch(0.66 0.16 50);`;
157
+
158
+ const VARS_FUTURISTIC = ` --radius: 0rem;
159
+ --background: oklch(0.16 0.02 265);
160
+ --foreground: oklch(0.96 0.01 260);
161
+ --card: oklch(0.21 0.03 265);
162
+ --card-foreground: oklch(0.96 0.01 260);
163
+ --primary: oklch(0.72 0.18 195);
164
+ --primary-foreground: oklch(0.16 0.02 265);
165
+ --secondary: oklch(0.27 0.03 265);
166
+ --secondary-foreground: oklch(0.96 0.01 260);
167
+ --muted: oklch(0.27 0.03 265);
168
+ --muted-foreground: oklch(0.72 0.02 260);
169
+ --accent: oklch(0.32 0.06 300);
170
+ --accent-foreground: oklch(0.96 0.01 260);
171
+ --destructive: oklch(0.62 0.24 18);
172
+ --border: oklch(0.4 0.05 265);
173
+ --input: oklch(0.3 0.04 265);
174
+ --ring: oklch(0.72 0.18 195);`;
175
+
176
+ export const THEMES: Record<ThemeName, ITheme> = {
177
+ minimal: { name: "minimal", tokens: tokenBlock(VARS_MINIMAL), deltas: {} },
178
+ warm: {
179
+ name: "warm",
180
+ tokens: tokenBlock(VARS_WARM),
181
+ deltas: { card: "shadow-sm", button: "shadow-sm" },
182
+ },
183
+ futuristic: {
184
+ name: "futuristic",
185
+ tokens: tokenBlock(VARS_FUTURISTIC),
186
+ deltas: {
187
+ button: "uppercase tracking-wide",
188
+ card: "backdrop-blur-sm",
189
+ input: "font-mono",
190
+ textarea: "font-mono",
191
+ select: "font-mono",
192
+ label: "uppercase tracking-wider text-xs",
193
+ badge: "uppercase tracking-wide",
194
+ },
195
+ },
196
+ };
197
+
198
+ // ─── primitive STRUCTURE (invariant) — `$DELTA` becomes the theme's classes ─────
199
+
200
+ const PRIMITIVES: Record<ComponentName, string> = {
201
+ button: `import { cva, type VariantProps } from "class-variance-authority";
202
+ import { cn } from "@/lib/utils";
203
+
204
+ const buttonVariants = cva(
205
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 $DELTA",
206
+ {
207
+ variants: {
208
+ variant: {
209
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
210
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
211
+ destructive: "bg-destructive text-white hover:bg-destructive/90",
212
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
213
+ ghost: "hover:bg-accent hover:text-accent-foreground",
214
+ },
215
+ size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-6", icon: "size-9" },
216
+ },
217
+ defaultVariants: { variant: "default", size: "default" },
218
+ }
219
+ );
220
+
221
+ export interface IButtonProps
222
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
223
+ VariantProps<typeof buttonVariants> {}
224
+
225
+ export function Button({
226
+ className,
227
+ variant,
228
+ size,
229
+ ...props
230
+ }: IButtonProps): React.JSX.Element {
231
+ return (
232
+ <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
233
+ );
234
+ }
235
+ `,
236
+ card: `import { cn } from "@/lib/utils";
237
+
238
+ export function Card({
239
+ className,
240
+ ...props
241
+ }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
242
+ return (
243
+ <div
244
+ className={cn(
245
+ "rounded-lg border bg-card text-card-foreground p-6 $DELTA",
246
+ className
247
+ )}
248
+ {...props}
249
+ />
250
+ );
251
+ }
252
+ `,
253
+ input: `import { cn } from "@/lib/utils";
254
+
255
+ export function Input({
256
+ className,
257
+ ...props
258
+ }: React.InputHTMLAttributes<HTMLInputElement>): React.JSX.Element {
259
+ return (
260
+ <input
261
+ className={cn(
262
+ "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
263
+ className
264
+ )}
265
+ {...props}
266
+ />
267
+ );
268
+ }
269
+ `,
270
+ label: `import { cn } from "@/lib/utils";
271
+
272
+ export function Label({
273
+ className,
274
+ ...props
275
+ }: React.LabelHTMLAttributes<HTMLLabelElement>): React.JSX.Element {
276
+ return (
277
+ <label
278
+ className={cn(
279
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 $DELTA",
280
+ className
281
+ )}
282
+ {...props}
283
+ />
284
+ );
285
+ }
286
+ `,
287
+ textarea: `import { cn } from "@/lib/utils";
288
+
289
+ export function Textarea({
290
+ className,
291
+ ...props
292
+ }: React.TextareaHTMLAttributes<HTMLTextAreaElement>): React.JSX.Element {
293
+ return (
294
+ <textarea
295
+ className={cn(
296
+ "flex min-h-16 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
297
+ className
298
+ )}
299
+ {...props}
300
+ />
301
+ );
302
+ }
303
+ `,
304
+ select: `import { cn } from "@/lib/utils";
305
+
306
+ export function Select({
307
+ className,
308
+ ...props
309
+ }: React.SelectHTMLAttributes<HTMLSelectElement>): React.JSX.Element {
310
+ return (
311
+ <select
312
+ className={cn(
313
+ "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 $DELTA",
314
+ className
315
+ )}
316
+ {...props}
317
+ />
318
+ );
319
+ }
320
+ `,
321
+ badge: `import { cva, type VariantProps } from "class-variance-authority";
322
+ import { cn } from "@/lib/utils";
323
+
324
+ const badgeVariants = cva(
325
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-medium $DELTA",
326
+ {
327
+ variants: {
328
+ variant: {
329
+ default: "border-transparent bg-primary text-primary-foreground",
330
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
331
+ outline: "text-foreground",
332
+ destructive: "border-transparent bg-destructive text-white",
333
+ },
334
+ },
335
+ defaultVariants: { variant: "default" },
336
+ }
337
+ );
338
+
339
+ export interface IBadgeProps
340
+ extends React.HTMLAttributes<HTMLSpanElement>,
341
+ VariantProps<typeof badgeVariants> {}
342
+
343
+ export function Badge({
344
+ className,
345
+ variant,
346
+ ...props
347
+ }: IBadgeProps): React.JSX.Element {
348
+ return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
349
+ }
350
+ `,
351
+ separator: `import { cn } from "@/lib/utils";
352
+
353
+ export function Separator({
354
+ className,
355
+ ...props
356
+ }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
357
+ return (
358
+ <div
359
+ role="separator"
360
+ className={cn("shrink-0 bg-border h-px w-full $DELTA", className)}
361
+ {...props}
362
+ />
363
+ );
364
+ }
365
+ `,
366
+ table: `import { cn } from "@/lib/utils";
367
+
368
+ export function Table({
369
+ className,
370
+ ...props
371
+ }: React.TableHTMLAttributes<HTMLTableElement>): React.JSX.Element {
372
+ return (
373
+ <div className="relative w-full overflow-auto $DELTA">
374
+ <table className={cn("w-full caption-bottom text-sm", className)} {...props} />
375
+ </div>
376
+ );
377
+ }
378
+
379
+ export function TableHeader({
380
+ className,
381
+ ...props
382
+ }: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
383
+ return <thead className={cn("[&_tr]:border-b", className)} {...props} />;
384
+ }
385
+
386
+ export function TableBody({
387
+ className,
388
+ ...props
389
+ }: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
390
+ return <tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
391
+ }
392
+
393
+ export function TableRow({
394
+ className,
395
+ ...props
396
+ }: React.HTMLAttributes<HTMLTableRowElement>): React.JSX.Element {
397
+ return (
398
+ <tr
399
+ className={cn("border-b transition-colors hover:bg-muted/50", className)}
400
+ {...props}
401
+ />
402
+ );
403
+ }
404
+
405
+ export function TableHead({
406
+ className,
407
+ ...props
408
+ }: React.ThHTMLAttributes<HTMLTableCellElement>): React.JSX.Element {
409
+ return (
410
+ <th
411
+ className={cn(
412
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground",
413
+ className
414
+ )}
415
+ {...props}
416
+ />
417
+ );
418
+ }
419
+
420
+ export function TableCell({
421
+ className,
422
+ ...props
423
+ }: React.TdHTMLAttributes<HTMLTableCellElement>): React.JSX.Element {
424
+ return <td className={cn("p-2 align-middle", className)} {...props} />;
425
+ }
426
+ `,
427
+ // ─── composition blocks (molecules) ────────────────────────────────────────
428
+ "app-shell": `import { Link, Outlet, useLocation } from "@tanstack/react-router";
429
+ import { cn } from "@/lib/utils";
430
+
431
+ export interface INavItem {
432
+ to: string;
433
+ label: string;
434
+ }
435
+
436
+ export interface IAppShellProps {
437
+ title: string;
438
+ nav: readonly INavItem[];
439
+ children?: React.ReactNode;
440
+ }
441
+
442
+ export function AppShell({
443
+ title,
444
+ nav,
445
+ children,
446
+ }: IAppShellProps): React.JSX.Element {
447
+ const { pathname } = useLocation();
448
+
449
+ return (
450
+ <div className={cn("flex min-h-screen bg-background $DELTA")}>
451
+ <aside className="w-56 shrink-0 border-r border-border bg-card p-4">
452
+ <h1 className="mb-6 text-xl font-bold text-foreground">{title}</h1>
453
+ <nav className="flex flex-col gap-1">
454
+ {nav.map((item) => {
455
+ const active =
456
+ pathname === item.to ||
457
+ (item.to !== "/" && pathname.startsWith(\`\${item.to}/\`));
458
+
459
+ return (
460
+ <Link
461
+ key={item.to}
462
+ to={item.to}
463
+ className={cn(
464
+ "rounded-md px-3 py-2 text-sm font-medium transition-colors",
465
+ active
466
+ ? "bg-primary text-primary-foreground"
467
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
468
+ )}
469
+ >
470
+ {item.label}
471
+ </Link>
472
+ );
473
+ })}
474
+ </nav>
475
+ </aside>
476
+ <main className="flex-1 p-6">{children ?? <Outlet />}</main>
477
+ </div>
478
+ );
479
+ }
480
+ `,
481
+ "page-header": `import { cn } from "@/lib/utils";
482
+
483
+ export interface IPageHeaderProps {
484
+ title: string;
485
+ description?: string;
486
+ children?: React.ReactNode;
487
+ }
488
+
489
+ export function PageHeader({
490
+ title,
491
+ description,
492
+ children,
493
+ }: IPageHeaderProps): React.JSX.Element {
494
+ return (
495
+ <div className={cn("mb-6 flex items-center justify-between gap-4 $DELTA")}>
496
+ <div>
497
+ <h1 className="text-2xl font-semibold text-foreground">{title}</h1>
498
+ {description !== undefined && (
499
+ <p className="text-sm text-muted-foreground">{description}</p>
500
+ )}
501
+ </div>
502
+ {children !== undefined && (
503
+ <div className="flex items-center gap-2">{children}</div>
504
+ )}
505
+ </div>
506
+ );
507
+ }
508
+ `,
509
+ field: `import { cn } from "@/lib/utils";
510
+ import { Label } from "@/components/ui/label";
511
+
512
+ export interface IFieldProps {
513
+ label: string;
514
+ htmlFor?: string;
515
+ error?: string;
516
+ children: React.ReactNode;
517
+ }
518
+
519
+ export function Field({
520
+ label,
521
+ htmlFor,
522
+ error,
523
+ children,
524
+ }: IFieldProps): React.JSX.Element {
525
+ return (
526
+ <div className={cn("flex flex-col gap-1.5 $DELTA")}>
527
+ <Label htmlFor={htmlFor}>{label}</Label>
528
+ {children}
529
+ {error !== undefined && (
530
+ <p className="text-sm text-destructive">{error}</p>
531
+ )}
532
+ </div>
533
+ );
534
+ }
535
+ `,
536
+ "form-actions": `import { cn } from "@/lib/utils";
537
+
538
+ export function FormActions({
539
+ className,
540
+ ...props
541
+ }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
542
+ return (
543
+ <div
544
+ className={cn("flex items-center justify-end gap-2 pt-2 $DELTA", className)}
545
+ {...props}
546
+ />
547
+ );
548
+ }
549
+ `,
550
+ toolbar: `import { cn } from "@/lib/utils";
551
+
552
+ export function Toolbar({
553
+ className,
554
+ ...props
555
+ }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
556
+ return (
557
+ <div
558
+ className={cn(
559
+ "mb-4 flex flex-wrap items-center gap-2 $DELTA",
560
+ className
561
+ )}
562
+ {...props}
563
+ />
564
+ );
565
+ }
566
+ `,
567
+ "empty-state": `import { cn } from "@/lib/utils";
568
+
569
+ export interface IEmptyStateProps {
570
+ title: string;
571
+ description?: string;
572
+ children?: React.ReactNode;
573
+ }
574
+
575
+ export function EmptyState({
576
+ title,
577
+ description,
578
+ children,
579
+ }: IEmptyStateProps): React.JSX.Element {
580
+ return (
581
+ <div
582
+ className={cn(
583
+ "flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border p-12 text-center $DELTA"
584
+ )}
585
+ >
586
+ <p className="text-sm font-medium text-foreground">{title}</p>
587
+ {description !== undefined && (
588
+ <p className="text-sm text-muted-foreground">{description}</p>
589
+ )}
590
+ {children}
591
+ </div>
592
+ );
593
+ }
594
+ `,
595
+ };
596
+
597
+ /** The relative path a primitive is written to (the shadcn convention). */
598
+ export function componentPath(name: ComponentName): string {
599
+ return `src/components/ui/${name}.tsx`;
600
+ }
601
+
602
+ /** Parse/validate a requested theme; undefined if not a known vibe. */
603
+ export function asThemeName(value: unknown): ThemeName | undefined {
604
+ return THEME_NAMES.find((t) => t === value);
605
+ }
606
+
607
+ /** Keep only the valid, de-duplicated component names from a requested list. */
608
+ export function asComponentNames(value: unknown): ComponentName[] {
609
+ if (!Array.isArray(value)) {
610
+ return [];
611
+ }
612
+
613
+ return COMPONENT_NAMES.filter((c) => value.includes(c));
614
+ }
615
+
616
+ /**
617
+ * Materialize the requested primitives under a theme: a path→content map the
618
+ * scaffolder writes. Always includes `src/index.css` (the theme's token block) plus
619
+ * one file per component, with the theme's per-component delta baked into the
620
+ * structure (an empty/missing delta collapses cleanly — no stray double space).
621
+ */
622
+ export function materializeComponents(
623
+ theme: ThemeName,
624
+ components: readonly ComponentName[]
625
+ ): Record<string, string> {
626
+ const t = THEMES[theme];
627
+ const out: Record<string, string> = { "src/index.css": t.tokens };
628
+
629
+ for (const name of components) {
630
+ const delta = t.deltas[name] ?? "";
631
+
632
+ out[componentPath(name)] = PRIMITIVES[name]
633
+ .replace(" $DELTA", delta.length > 0 ? ` ${delta}` : "")
634
+ .replace("$DELTA", delta);
635
+ }
636
+
637
+ return out;
638
+ }