@helloleo/plugins 0.1.13 → 0.1.15

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 (129) hide show
  1. package/dist/index.js +203 -1220
  2. package/dist/templates/tanstack-router-2026-06-15/components.json +22 -0
  3. package/dist/templates/tanstack-router-2026-06-15/index.html +13 -0
  4. package/dist/templates/tanstack-router-2026-06-15/package.json +94 -0
  5. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
  6. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
  7. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
  8. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
  9. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
  10. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
  11. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
  12. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
  13. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
  14. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
  15. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
  16. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
  17. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
  18. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
  19. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
  20. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
  21. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
  22. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
  23. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
  24. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
  25. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
  26. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
  27. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
  28. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
  29. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
  30. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
  31. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
  32. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
  33. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
  34. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
  35. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
  36. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
  37. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
  38. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
  39. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
  40. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
  41. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
  42. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
  43. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
  44. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
  45. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
  46. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
  47. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
  48. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
  49. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
  50. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
  51. package/dist/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
  52. package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
  53. package/dist/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
  54. package/dist/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
  55. package/dist/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
  56. package/dist/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
  57. package/dist/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
  58. package/dist/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
  59. package/dist/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
  60. package/dist/templates/tanstack-router-2026-06-15/tsconfig.json +28 -0
  61. package/dist/templates/tanstack-router-2026-06-15/tsr.config.json +3 -0
  62. package/dist/templates/tanstack-router-2026-06-15/vite.config.ts +21 -0
  63. package/package.json +3 -2
  64. package/src/index.ts +0 -1
  65. package/src/template-render.ts +48 -0
  66. package/src/templates/tanstack-router-2026-06-15/components.json +22 -0
  67. package/src/templates/tanstack-router-2026-06-15/index.html +13 -0
  68. package/src/templates/tanstack-router-2026-06-15/package.json +94 -0
  69. package/src/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +51 -0
  70. package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +115 -0
  71. package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +49 -0
  72. package/src/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +5 -0
  73. package/src/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +47 -0
  74. package/src/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +32 -0
  75. package/src/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +101 -0
  76. package/src/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +49 -0
  77. package/src/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +177 -0
  78. package/src/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +55 -0
  79. package/src/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +240 -0
  80. package/src/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +331 -0
  81. package/src/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +26 -0
  82. package/src/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +11 -0
  83. package/src/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +143 -0
  84. package/src/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +187 -0
  85. package/src/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +104 -0
  86. package/src/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +98 -0
  87. package/src/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +188 -0
  88. package/src/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +171 -0
  89. package/src/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +27 -0
  90. package/src/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +69 -0
  91. package/src/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +22 -0
  92. package/src/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +21 -0
  93. package/src/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +229 -0
  94. package/src/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +120 -0
  95. package/src/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +98 -0
  96. package/src/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +31 -0
  97. package/src/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +25 -0
  98. package/src/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +36 -0
  99. package/src/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +37 -0
  100. package/src/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +44 -0
  101. package/src/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +152 -0
  102. package/src/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +24 -0
  103. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +122 -0
  104. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +744 -0
  105. package/src/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +7 -0
  106. package/src/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +23 -0
  107. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +23 -0
  108. package/src/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +27 -0
  109. package/src/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +94 -0
  110. package/src/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +53 -0
  111. package/src/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +21 -0
  112. package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +57 -0
  113. package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +42 -0
  114. package/src/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +32 -0
  115. package/src/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +19 -0
  116. package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +83 -0
  117. package/src/templates/tanstack-router-2026-06-15/src/lib/utils.ts +6 -0
  118. package/src/templates/tanstack-router-2026-06-15/src/main.tsx +33 -0
  119. package/src/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +59 -0
  120. package/src/templates/tanstack-router-2026-06-15/src/router.tsx +23 -0
  121. package/src/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +16 -0
  122. package/src/templates/tanstack-router-2026-06-15/src/routes/index.tsx +14 -0
  123. package/src/templates/tanstack-router-2026-06-15/src/styles.css +214 -0
  124. package/src/templates/tanstack-router-2026-06-15/tsconfig.json +28 -0
  125. package/src/templates/tanstack-router-2026-06-15/tsr.config.json +3 -0
  126. package/src/templates/tanstack-router-2026-06-15/vite.config.ts +21 -0
  127. package/src/tools/scaffold.ts +62 -1340
  128. package/src/tools/setup-netsuite.ts +161 -80
  129. package/src/tools/start-scaffold.ts +0 -1343
@@ -2,1385 +2,107 @@ import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Plugin } from "@opencode-ai/plugin";
4
4
  import { tool } from "@opencode-ai/plugin";
5
+ import { renderTemplate } from "../template-render.ts";
5
6
 
6
- function packageJson(name: string) {
7
- return JSON.stringify(
8
- {
9
- name,
10
- private: true,
11
- version: "0.0.0",
12
- type: "module",
13
- scripts: {
14
- dev: "vite",
15
- start: "vite",
16
- build: "vite build",
17
- preview: "vite preview",
18
- },
19
- dependencies: {
20
- react: "^18.3.1",
21
- "react-dom": "^18.3.1",
22
- clsx: "^2.1.1",
23
- "tailwind-merge": "^2.6.0",
24
- "lucide-react": "^1.14.0",
25
- },
26
- devDependencies: {
27
- "@types/react": "^18.3.12",
28
- "@types/react-dom": "^18.3.1",
29
- "@vitejs/plugin-react": "^4.3.4",
30
- autoprefixer: "^10.4.20",
31
- postcss: "^8.4.49",
32
- tailwindcss: "^3.4.17",
33
- typescript: "^5.6.3",
34
- vite: "^6.0.5",
35
- },
36
- },
37
- null,
38
- 2,
39
- );
40
- }
41
-
42
- function indexHtml(fontFamily: string) {
43
- const fontParam = fontFamily.replace(/\s+/g, "+");
44
- return `<!DOCTYPE html>
45
- <html lang="en">
46
- <head>
47
- <meta charset="UTF-8" />
48
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
49
- <link rel="preconnect" href="https://fonts.googleapis.com">
50
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
51
- <link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
52
- <title>App</title>
53
- </head>
54
- <body>
55
- <div id="root"></div>
56
- <script type="module" src="/src/main.tsx"></script>
57
- </body>
58
- </html>`;
59
- }
60
-
61
- function viteConfig() {
62
- return `import { defineConfig } from 'vite'
63
- import react from '@vitejs/plugin-react'
64
- import path from 'path'
65
-
66
- export default defineConfig({
67
- plugins: [react()],
68
- server: {
69
- allowedHosts: true,
70
- },
71
- resolve: {
72
- alias: {
73
- '@': path.resolve(__dirname, './src'),
74
- },
75
- },
76
- })`;
77
- }
78
-
79
- export function tailwindConfig() {
80
- return `export default {
81
- darkMode: ["class"],
82
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
83
- theme: {
84
- extend: {
85
- colors: {
86
- background: "hsl(var(--background))",
87
- foreground: "hsl(var(--foreground))",
88
- card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
89
- primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
90
- secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
91
- muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
92
- accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
93
- destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
94
- success: { DEFAULT: "hsl(var(--success))", foreground: "hsl(var(--success-foreground))" },
95
- warning: { DEFAULT: "hsl(var(--warning))", foreground: "hsl(var(--warning-foreground))" },
96
- border: "hsl(var(--border))",
97
- input: "hsl(var(--input))",
98
- ring: "hsl(var(--ring))",
99
- },
100
- fontFamily: {
101
- sans: "var(--font-sans)",
102
- heading: "var(--font-heading)",
103
- },
104
- borderRadius: {
105
- lg: "var(--radius)",
106
- md: "calc(var(--radius) - 2px)",
107
- sm: "calc(var(--radius) - 4px)",
108
- },
109
- borderWidth: {
110
- DEFAULT: "var(--border-width)",
111
- },
112
- boxShadow: {
113
- DEFAULT: "var(--shadow)",
114
- lg: "var(--shadow-lg)",
115
- },
116
- },
117
- },
118
- plugins: [],
119
- }`;
120
- }
121
-
122
- export function postcssConfig() {
123
- return `export default {
124
- plugins: {
125
- tailwindcss: {},
126
- autoprefixer: {},
127
- },
128
- }`;
129
- }
130
-
131
- export function indexCss(
132
- primaryColor: string,
133
- fontFamily: string,
134
- borderRadius: string,
135
- ) {
136
- const fontValue = `'${fontFamily}', ui-sans-serif, system-ui, sans-serif`;
137
- return `/*
138
- * HelloLeo Design System -- READ BEFORE STYLING.
139
- *
140
- * The CSS variables below are the single source of truth for the project's
141
- * design tokens. They power the no-code Design panel in the HelloLeo webapp:
142
- * the user edits colors / fonts / radius / shadows visually, and the panel
143
- * writes back to THIS file. Keeping the conventions intact is what unlocks
144
- * that round-trip.
145
- *
146
- * Conventions (do not break):
147
- * - All colors stored as raw HSL triplets (no hsl() wrapper, no #hex).
148
- * Tailwind classes wrap them via hsl(var(--token)). Never inline #hex
149
- * in components -- always use design tokens (bg-primary, text-foreground,
150
- * border-border, etc.).
151
- * - --primary / --secondary / --accent / --destructive / --success /
152
- * --warning each have a paired -foreground for text on that surface.
153
- * - --radius drives lg/md/sm via calc() in tailwind.config.js. Edit here,
154
- * not in component classes.
155
- * - --font-sans for body, --font-heading for headings. The webfont is loaded
156
- * via a Google Fonts <link> — in index.html (SPA) or the root route head
157
- * (SSR). Keep the family here in sync with that <link>.
158
- * - .dark variants mirror :root one-for-one. When adding a token, add it
159
- * to BOTH blocks or dark mode breaks.
160
- *
161
- * UI primitives (Button, Card, Input, Modal, Table, Badge, Alert, Spinner)
162
- * MUST live in src/components/ui/ and consume these tokens via Tailwind
163
- * classes -- never hardcoded colors. One responsibility per file, <50 lines
164
- * per component.
165
- */
166
-
167
- @tailwind base;
168
- @tailwind components;
169
- @tailwind utilities;
170
-
171
- @layer base {
172
- :root {
173
- --background: 0 0% 100%;
174
- --foreground: 222.2 84% 4.9%;
175
- --card: 0 0% 100%;
176
- --card-foreground: 222.2 84% 4.9%;
177
- --primary: ${primaryColor};
178
- --primary-foreground: 210 40% 98%;
179
- --secondary: 210 40% 96.1%;
180
- --secondary-foreground: 222.2 47.4% 11.2%;
181
- --muted: 210 40% 96.1%;
182
- --muted-foreground: 215.4 16.3% 46.9%;
183
- --accent: 210 40% 96.1%;
184
- --accent-foreground: 222.2 47.4% 11.2%;
185
- --destructive: 0 84.2% 60.2%;
186
- --destructive-foreground: 210 40% 98%;
187
- --success: 142 76% 36%;
188
- --success-foreground: 210 40% 98%;
189
- --warning: 38 92% 50%;
190
- --warning-foreground: 222.2 47.4% 11.2%;
191
- --border: 214.3 31.8% 91.4%;
192
- --input: 214.3 31.8% 91.4%;
193
- --ring: 221.2 83.2% 53.3%;
194
- --radius: ${borderRadius};
195
- --border-width: 1px;
196
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
197
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
198
- --font-sans: ${fontValue};
199
- --font-heading: ${fontValue};
200
- }
201
-
202
- .dark {
203
- --background: 222.2 84% 4.9%;
204
- --foreground: 210 40% 98%;
205
- --card: 222.2 84% 4.9%;
206
- --card-foreground: 210 40% 98%;
207
- --primary: 217.2 91.2% 59.8%;
208
- --primary-foreground: 222.2 47.4% 11.2%;
209
- --secondary: 217.2 32.6% 17.5%;
210
- --secondary-foreground: 210 40% 98%;
211
- --muted: 217.2 32.6% 17.5%;
212
- --muted-foreground: 215 20.2% 65.1%;
213
- --accent: 217.2 32.6% 17.5%;
214
- --accent-foreground: 210 40% 98%;
215
- --destructive: 0 62.8% 30.6%;
216
- --destructive-foreground: 210 40% 98%;
217
- --success: 142 76% 36%;
218
- --success-foreground: 210 40% 98%;
219
- --warning: 38 92% 50%;
220
- --warning-foreground: 222.2 47.4% 11.2%;
221
- --border: 217.2 32.6% 17.5%;
222
- --input: 217.2 32.6% 17.5%;
223
- --ring: 224.3 76.3% 48%;
224
- --border-width: 1px;
225
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
226
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
227
- --font-sans: ${fontValue};
228
- --font-heading: ${fontValue};
229
- }
230
- }
231
-
232
- body {
233
- @apply bg-background text-foreground font-sans;
234
- }`;
235
- }
236
-
237
- export function utilsTs() {
238
- return `import { clsx, type ClassValue } from 'clsx'
239
- import { twMerge } from 'tailwind-merge'
240
-
241
- export function cn(...inputs: ClassValue[]) {
242
- return twMerge(clsx(inputs))
243
- }`;
244
- }
245
-
246
- export function proxyTs() {
247
- return `/*
248
- * HelloLeo Integration Proxy -- runtime helper for connected integrations.
249
- *
250
- * All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
251
- * Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
252
- * this single proxy. The proxy handles auth and CORS using credentials the
253
- * user already configured in Project Settings -- you NEVER pass an API key
254
- * from frontend code, and you NEVER call third-party hosts directly.
255
- *
256
- * Two modes:
257
- * A) Frontend-only project -> import callIntegration() from this file.
258
- * B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
259
- * Use Cloud whenever the project needs backend logic, automations,
260
- * webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
261
- *
262
- * Integration slugs:
263
- * Use the exact slug from the Enabled MCP list with the "pipedream-"
264
- * prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
265
- * Native integrations (odoo, airtable) keep their plain name.
266
- *
267
- * Response shape (always wrapped):
268
- * { success: boolean, status: number, data: <provider payload> }
269
- *
270
- * Unwrap "data" by provider:
271
- * odoo -> data.result
272
- * airtable -> data.records
273
- * notion -> data.results
274
- * microsoft 365 -> data.value
275
- * klaviyo -> data.data
276
- * supabase / dimomaint -> data
277
- *
278
- * Pagination: put query params directly in the endpoint string,
279
- * e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
280
- * Read pagination metadata from the response to build the next call.
281
- * Never .filter() large lists in the frontend -- paginate server-side.
282
- *
283
- * Secrets for non-integration APIs (e.g. raw OpenAI): store them in
284
- * HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
285
- * from a Worker. Never embed secrets in frontend code.
7
+ /**
8
+ * Identifier for the scaffold template a project was generated from. It is
9
+ * also the directory name under src/templates/. Written to .leo/project.json
10
+ * so the webapp / leo-core can tell which template (and therefore which
11
+ * conventions: TanStack Router file routing, Tailwind v4, shadcn/ui) a
12
+ * workspace was bootstrapped with. Bump this (and add a new templates/<id>/
13
+ * dir) when the scaffold's shape changes.
286
14
  */
15
+ export const SCAFFOLD_VERSION = "tanstack-router-2026-06-15";
287
16
 
288
- const PROJECT_UUID = import.meta.env.VITE_PROJECT_UUID ?? '';
289
- export const INTEGRATION_PROXY_URL =
290
- import.meta.env.VITE_INTEGRATION_PROXY_URL ??
291
- \`https://api.app.helloleo.dev/api/integration-proxy/\${PROJECT_UUID}\`;
292
-
293
- export interface ProxyRequest {
294
- integration: string;
295
- endpoint: string;
296
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
297
- body?: unknown;
298
- headers?: Record<string, string>;
299
- }
300
-
301
- export interface ProxyResponse<T = unknown> {
302
- success: boolean;
303
- status: number;
304
- data: T;
305
- }
306
-
307
- export async function callIntegration<T = unknown>(
308
- req: ProxyRequest,
309
- ): Promise<ProxyResponse<T>> {
310
- const res = await fetch(INTEGRATION_PROXY_URL, {
311
- method: 'POST',
312
- headers: { 'Content-Type': 'application/json' },
313
- body: JSON.stringify({ method: 'POST', ...req }),
314
- });
315
- return res.json();
316
- }
317
-
318
- /* Example -- Odoo search_read:
319
- *
320
- * const r = await callIntegration({
321
- * integration: 'odoo',
322
- * endpoint: '/jsonrpc',
323
- * body: {
324
- * model: 'product.template',
325
- * method: 'search_read',
326
- * args: [[['type', '=', 'consu']]],
327
- * kwargs: { fields: ['name', 'list_price'], limit: 50 },
328
- * },
329
- * });
330
- * const products = r.data?.result ?? [];
17
+ /**
18
+ * The project workspace directory is named after the project UUID -- the
19
+ * webapp generates `<slugified-name>-<uuidv4>` (actions.ts createProject)
20
+ * and leo-core mounts the project at /workspace/<uuid>. Baking it into
21
+ * proxy.ts at scaffold time removes the VITE_PROJECT_UUID env var, which
22
+ * nothing in the infrastructure ever set and which Leo kept mis-generating.
331
23
  */
332
- `;
333
- }
334
-
335
- function hexToHslTriplet(hex: string): string {
336
- let h = hex.trim().replace(/^#/, "");
337
- if (h.length === 3) {
338
- h = h
339
- .split("")
340
- .map((c) => c + c)
341
- .join("");
342
- }
343
- if (!/^[0-9a-fA-F]{6}$/.test(h)) return "";
344
- const r = Number.parseInt(h.slice(0, 2), 16) / 255;
345
- const g = Number.parseInt(h.slice(2, 4), 16) / 255;
346
- const b = Number.parseInt(h.slice(4, 6), 16) / 255;
347
- const max = Math.max(r, g, b);
348
- const min = Math.min(r, g, b);
349
- const l = (max + min) / 2;
350
- let s = 0;
351
- let hue = 0;
352
- if (max !== min) {
353
- const d = max - min;
354
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
355
- switch (max) {
356
- case r:
357
- hue = (g - b) / d + (g < b ? 6 : 0);
358
- break;
359
- case g:
360
- hue = (b - r) / d + 2;
361
- break;
362
- default:
363
- hue = (r - g) / d + 4;
364
- }
365
- hue *= 60;
366
- }
367
- return `${Math.round(hue)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
368
- }
369
-
370
- export function normalizePrimaryColor(value: string): string {
371
- const trimmed = value.trim();
372
- if (/^(#|[0-9a-fA-F]{3}$|[0-9a-fA-F]{6}$)/.test(trimmed)) {
373
- const hsl = hexToHslTriplet(trimmed);
374
- if (hsl) return hsl;
375
- }
376
- return trimmed;
377
- }
378
-
379
- function mainTsx() {
380
- return `import React from 'react'
381
- import ReactDOM from 'react-dom/client'
382
- import App from './App'
383
- import './index.css'
384
-
385
- ReactDOM.createRoot(document.getElementById('root')!).render(
386
- <React.StrictMode>
387
- <App />
388
- </React.StrictMode>,
389
- )`;
24
+ export function projectUuidFromDir(dir: string): string {
25
+ return path.basename(dir);
390
26
  }
391
27
 
392
28
  export interface BaseScaffoldOptions {
393
29
  name?: string;
394
- primaryColor?: string;
395
- fontFamily?: string;
396
- borderRadius?: string;
397
30
  }
398
31
 
399
32
  /**
400
- * Write the 9 base scaffold files (no integrations). Shared between
401
- * `scaffold` (frontend-only) and `setup_netsuite` (base + OAuth files).
402
- * Returns the list of relative file paths written.
33
+ * Write the base scaffold by rendering the TanStack Router template into dir,
34
+ * then adding .leo/project.json (template metadata the tool owns, not part of
35
+ * the template tree). Shared between `scaffold` (frontend-only) and
36
+ * `setup_netsuite` (base + OAuth files). Returns the relative paths written.
403
37
  */
404
38
  export async function writeBaseScaffold(
405
39
  opts: BaseScaffoldOptions,
406
40
  dir: string,
407
41
  ): Promise<string[]> {
408
42
  const name = opts.name ?? "leo-project";
409
- const primary = normalizePrimaryColor(
410
- opts.primaryColor ?? "221.2 83.2% 53.3%",
411
- );
412
- const font = opts.fontFamily ?? "Inter";
413
- const radius = opts.borderRadius ?? "0.5rem";
414
-
415
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
416
-
417
- const files: [string, string][] = [
418
- ["package.json", packageJson(name)],
419
- ["index.html", indexHtml(font)],
420
- ["vite.config.js", viteConfig()],
421
- ["tailwind.config.js", tailwindConfig()],
422
- ["postcss.config.js", postcssConfig()],
423
- [path.join("src", "index.css"), indexCss(primary, font, radius)],
424
- [path.join("src", "lib", "utils.ts"), utilsTs()],
425
- [path.join("src", "lib", "proxy.ts"), proxyTs()],
426
- [path.join("src", "main.tsx"), mainTsx()],
427
- ];
428
-
429
- const created: string[] = [];
430
- for (const [filePath, content] of files) {
431
- await Bun.write(path.join(dir, filePath), content);
432
- created.push(filePath);
433
- }
434
- return created;
435
- }
436
-
437
- // NOTE: `writeBaseScaffold` + the design-system helpers above are the legacy
438
- // React+Vite base, still consumed by `setup_netsuite`. The `scaffold` TOOL below
439
- // is the current TanStack scaffolder (spa/ssr) — it replaced the old React tool.
440
-
441
- /*
442
- * scaffold_app -- next-gen HelloLeo scaffold.
443
- *
444
- * One tool, two modes (both TanStack, both Vite, same design system + tooling):
445
- * - mode "spa" -> TanStack Router SPA (client-only). Runs `vite dev`; deploys
446
- * as a static Vite app on the preview sandbox, like today.
447
- * - mode "ssr" -> TanStack Start (Node server target, validated by
448
- * cf-sandbox-tanstack-poc). SSR loaders + server functions +
449
- * Drizzle. Runs `vite dev`; on the preview sandbox the Node
450
- * server runs inside the CF Sandbox container (the PM2
451
- * replacement). DB adapter swaps libsql (dev) -> D1 (prod);
452
- * wrangler.jsonc carries the Workers-for-Platforms prod target.
453
- * (Executing SSR *on workerd itself* needs a newer TanStack/CF
454
- * integration than the pinned 1.121.x — tracked as follow-up.)
455
- *
456
- * Shared across both:
457
- * - Tailwind v3 + the HelloLeo design-system tokens (src/styles/app.css) so
458
- * the no-code Design panel round-trip keeps working (same tokens as the
459
- * legacy `scaffold`).
460
- * - ShadcnUI primitives (components.json + src/components/ui/*) consuming the
461
- * design tokens -- never hardcoded colors.
462
- * - Biome (biome.json) for format + lint. No ESLint/Prettier.
463
- * - Strict TypeScript (tsconfig.json: strict, bundler resolution, @/* paths)
464
- * so the editor / OpenCode LSP gives full type info.
465
- *
466
- * Version pins mirror the validated cf-sandbox-tanstack-poc:
467
- * TanStack 1.121.x, React 19, Vite 6, @cloudflare/* current.
468
- */
469
-
470
- const REACT = "^19.0.0";
471
- const VITE = "^6.0.5";
472
- const TS = "^5.6.3";
473
- // Pin EXACT (no caret): @tanstack/react-router and @tanstack/react-start must
474
- // share one @tanstack/router-core. react-router floats ahead of react-start's
475
- // release line, so a caret skews router-core and SSR breaks (missing
476
- // processRouteTree export). 1.131.50 is the matched set react-start ships with.
477
- const TANSTACK = "1.131.50";
478
- const BIOME = "2.4.8";
479
-
480
- // ---------- shared tooling files ----------
481
-
482
- function tsconfig(): string {
483
- return JSON.stringify(
484
- {
485
- compilerOptions: {
486
- target: "ES2022",
487
- module: "ESNext",
488
- moduleResolution: "Bundler",
489
- lib: ["ES2022", "DOM", "DOM.Iterable"],
490
- jsx: "react-jsx",
491
- strict: true,
492
- noUncheckedIndexedAccess: true,
493
- noUnusedLocals: true,
494
- noUnusedParameters: true,
495
- verbatimModuleSyntax: true,
496
- skipLibCheck: true,
497
- noEmit: true,
498
- baseUrl: ".",
499
- paths: { "@/*": ["./src/*"] },
500
- types: ["vite/client"],
501
- },
502
- include: ["src", "*.config.ts"],
503
- },
504
- null,
505
- 2,
506
- );
507
- }
508
-
509
- function biomeJson(): string {
510
- return JSON.stringify(
511
- {
512
- $schema: `https://biomejs.dev/schemas/${BIOME}/schema.json`,
513
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
514
- files: { ignoreUnknown: true },
515
- formatter: { enabled: true, indentStyle: "tab", lineWidth: 80 },
516
- linter: { enabled: true, rules: { recommended: true } },
517
- javascript: { formatter: { quoteStyle: "double" } },
518
- assist: { actions: { source: { organizeImports: "on" } } },
519
- },
520
- null,
521
- 2,
522
- );
523
- }
524
-
525
- function componentsJson(): string {
526
- // shadcn/ui config -- "new-york" style on Tailwind v3 with our token names.
527
- return JSON.stringify(
528
- {
529
- $schema: "https://ui.shadcn.com/schema.json",
530
- style: "new-york",
531
- rsc: false,
532
- tsx: true,
533
- tailwind: {
534
- config: "tailwind.config.js",
535
- css: "src/styles/app.css",
536
- baseColor: "slate",
537
- cssVariables: true,
538
- prefix: "",
539
- },
540
- aliases: {
541
- components: "@/components",
542
- utils: "@/lib/utils",
543
- ui: "@/components/ui",
544
- lib: "@/lib",
545
- hooks: "@/hooks",
546
- },
547
- iconLibrary: "lucide",
548
- },
549
- null,
550
- 2,
551
- );
552
- }
553
-
554
- // ---------- shadcn primitives (token-driven) ----------
555
-
556
- function uiButton(): string {
557
- return `import * as React from "react"
558
- import { Slot } from "@radix-ui/react-slot"
559
- import { cva, type VariantProps } from "class-variance-authority"
560
- import { cn } from "@/lib/utils"
561
-
562
- const buttonVariants = cva(
563
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
564
- {
565
- variants: {
566
- variant: {
567
- default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
568
- destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
569
- outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
570
- secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
571
- ghost: "hover:bg-accent hover:text-accent-foreground",
572
- link: "text-primary underline-offset-4 hover:underline",
573
- },
574
- size: {
575
- default: "h-9 px-4 py-2",
576
- sm: "h-8 rounded-md px-3 text-xs",
577
- lg: "h-10 rounded-md px-8",
578
- icon: "h-9 w-9",
579
- },
580
- },
581
- defaultVariants: { variant: "default", size: "default" },
582
- },
583
- )
584
-
585
- export interface ButtonProps
586
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
587
- VariantProps<typeof buttonVariants> {
588
- asChild?: boolean
589
- }
590
-
591
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
592
- ({ className, variant, size, asChild = false, ...props }, ref) => {
593
- const Comp = asChild ? Slot : "button"
594
- return (
595
- <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
596
- )
597
- },
598
- )
599
- Button.displayName = "Button"
600
-
601
- export { Button, buttonVariants }
602
- `;
603
- }
604
-
605
- function uiCard(): string {
606
- return `import * as React from "react"
607
- import { cn } from "@/lib/utils"
608
-
609
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
610
- ({ className, ...props }, ref) => (
611
- <div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow", className)} {...props} />
612
- ),
613
- )
614
- Card.displayName = "Card"
615
-
616
- const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
617
- ({ className, ...props }, ref) => (
618
- <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
619
- ),
620
- )
621
- CardHeader.displayName = "CardHeader"
622
-
623
- const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
624
- ({ className, ...props }, ref) => (
625
- <div ref={ref} className={cn("font-heading font-semibold leading-none tracking-tight", className)} {...props} />
626
- ),
627
- )
628
- CardTitle.displayName = "CardTitle"
629
-
630
- const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
631
- ({ className, ...props }, ref) => (
632
- <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
633
- ),
634
- )
635
- CardDescription.displayName = "CardDescription"
636
-
637
- const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
638
- ({ className, ...props }, ref) => (
639
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
640
- ),
641
- )
642
- CardContent.displayName = "CardContent"
643
-
644
- export { Card, CardHeader, CardTitle, CardDescription, CardContent }
645
- `;
646
- }
647
-
648
- function uiInput(): string {
649
- return `import * as React from "react"
650
- import { cn } from "@/lib/utils"
651
-
652
- const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
653
- ({ className, type, ...props }, ref) => (
654
- <input
655
- type={type}
656
- ref={ref}
657
- className={cn(
658
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
659
- className,
660
- )}
661
- {...props}
662
- />
663
- ),
664
- )
665
- Input.displayName = "Input"
666
-
667
- export { Input }
668
- `;
669
- }
670
-
671
- function uiTextarea(): string {
672
- return `import * as React from "react"
673
- import { cn } from "@/lib/utils"
674
-
675
- const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
676
- ({ className, ...props }, ref) => (
677
- <textarea
678
- ref={ref}
679
- className={cn(
680
- "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
681
- className,
682
- )}
683
- {...props}
684
- />
685
- ),
686
- )
687
- Textarea.displayName = "Textarea"
688
-
689
- export { Textarea }
690
- `;
691
- }
692
-
693
- function uiLabel(): string {
694
- return `import * as React from "react"
695
- import { cn } from "@/lib/utils"
696
-
697
- const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
698
- ({ className, ...props }, ref) => (
699
- <label
700
- ref={ref}
701
- className={cn(
702
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
703
- className,
704
- )}
705
- {...props}
706
- />
707
- ),
708
- )
709
- Label.displayName = "Label"
710
-
711
- export { Label }
712
- `;
713
- }
714
43
 
715
- function uiBadge(): string {
716
- return `import * as React from "react"
717
- import { cva, type VariantProps } from "class-variance-authority"
718
- import { cn } from "@/lib/utils"
719
-
720
- const badgeVariants = cva(
721
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
722
- {
723
- variants: {
724
- variant: {
725
- default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
726
- secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
727
- destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
728
- outline: "text-foreground",
729
- },
730
- },
731
- defaultVariants: { variant: "default" },
732
- },
733
- )
734
-
735
- export interface BadgeProps
736
- extends React.HTMLAttributes<HTMLDivElement>,
737
- VariantProps<typeof badgeVariants> {}
738
-
739
- function Badge({ className, variant, ...props }: BadgeProps) {
740
- return <div className={cn(badgeVariants({ variant }), className)} {...props} />
741
- }
742
-
743
- export { Badge, badgeVariants }
744
- `;
745
- }
746
-
747
- // ---------- mode: SPA (TanStack Router) ----------
748
-
749
- function spaPackageJson(name: string): string {
750
- return JSON.stringify(
751
- {
752
- name,
753
- private: true,
754
- version: "0.0.0",
755
- type: "module",
756
- scripts: {
757
- dev: "vite dev --host",
758
- build: "vite build",
759
- preview: "vite preview",
760
- lint: "biome check --write .",
761
- },
762
- dependencies: {
763
- react: REACT,
764
- "react-dom": REACT,
765
- "@tanstack/react-router": TANSTACK,
766
- "class-variance-authority": "^0.7.1",
767
- clsx: "^2.1.1",
768
- "tailwind-merge": "^2.6.0",
769
- "lucide-react": "^0.468.0",
770
- "@radix-ui/react-slot": "^1.1.1",
771
- },
772
- devDependencies: {
773
- "@tanstack/router-plugin": TANSTACK,
774
- "@types/react": "^19.0.0",
775
- "@types/react-dom": "^19.0.0",
776
- "@vitejs/plugin-react": "^4.3.4",
777
- "@biomejs/biome": BIOME,
778
- autoprefixer: "^10.4.20",
779
- postcss: "^8.4.49",
780
- tailwindcss: "^3.4.17",
781
- typescript: TS,
782
- vite: VITE,
783
- },
784
- },
785
- null,
786
- 2,
44
+ const created = await renderTemplate(
45
+ SCAFFOLD_VERSION,
46
+ { LEO_NAME: name, LEO_PROJECT_UUID: projectUuidFromDir(dir) },
47
+ dir,
787
48
  );
788
- }
789
-
790
- function spaViteConfig(): string {
791
- return `import { defineConfig } from "vite"
792
- import react from "@vitejs/plugin-react"
793
- import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
794
- import path from "node:path"
795
-
796
- export default defineConfig({
797
- // File-based routing: routes live in src/routes, routeTree.gen.ts is generated.
798
- plugins: [TanStackRouterVite({ target: "react", autoCodeSplitting: true }), react()],
799
- // allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
800
- server: { allowedHosts: true },
801
- resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
802
- })
803
- `;
804
- }
805
-
806
- function spaIndexHtml(fontFamily: string): string {
807
- const fontParam = fontFamily.replace(/\s+/g, "+");
808
- return `<!DOCTYPE html>
809
- <html lang="en">
810
- <head>
811
- <meta charset="UTF-8" />
812
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
813
- <link rel="preconnect" href="https://fonts.googleapis.com">
814
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
815
- <link href="https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" rel="stylesheet">
816
- <title>App</title>
817
- </head>
818
- <body>
819
- <div id="root"></div>
820
- <script type="module" src="/src/main.tsx"></script>
821
- </body>
822
- </html>
823
- `;
824
- }
825
-
826
- function spaMainTsx(): string {
827
- return `import React from "react"
828
- import ReactDOM from "react-dom/client"
829
- import { RouterProvider, createRouter } from "@tanstack/react-router"
830
- import { routeTree } from "./routeTree.gen"
831
- import "./styles/app.css"
832
-
833
- const router = createRouter({ routeTree, defaultPreload: "intent", scrollRestoration: true })
834
-
835
- declare module "@tanstack/react-router" {
836
- interface Register {
837
- router: typeof router
838
- }
839
- }
840
-
841
- ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
842
- <React.StrictMode>
843
- <RouterProvider router={router} />
844
- </React.StrictMode>,
845
- )
846
- `;
847
- }
848
-
849
- function spaRootRoute(): string {
850
- return `import { Outlet, createRootRoute } from "@tanstack/react-router"
851
-
852
- export const Route = createRootRoute({
853
- errorComponent: ({ error }) => (
854
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
855
- <h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
856
- <pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
857
- </main>
858
- ),
859
- notFoundComponent: () => (
860
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
861
- <h1 className="text-2xl font-heading font-semibold">404</h1>
862
- <p className="text-muted-foreground">This page could not be found.</p>
863
- </main>
864
- ),
865
- component: () => (
866
- <div className="min-h-screen bg-background text-foreground">
867
- <Outlet />
868
- </div>
869
- ),
870
- })
871
- `;
872
- }
873
-
874
- function spaIndexRoute(): string {
875
- return `import { createFileRoute } from "@tanstack/react-router"
876
- import { Button } from "@/components/ui/button"
877
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
878
-
879
- export const Route = createFileRoute("/")({ component: Home })
880
-
881
- function Home() {
882
- return (
883
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
884
- <Card className="w-full">
885
- <CardHeader>
886
- <CardTitle className="text-2xl">Welcome to your app</CardTitle>
887
- <CardDescription>TanStack Router SPA · Tailwind · shadcn/ui · Biome</CardDescription>
888
- </CardHeader>
889
- <CardContent className="flex gap-3">
890
- <Button>Get started</Button>
891
- <Button variant="outline">Learn more</Button>
892
- </CardContent>
893
- </Card>
894
- </main>
895
- )
896
- }
897
- `;
898
- }
899
-
900
- // ---------- mode: SSR (TanStack Start on Cloudflare workerd) ----------
901
-
902
- function ssrPackageJson(name: string): string {
903
- return JSON.stringify(
904
- {
905
- name,
906
- private: true,
907
- version: "0.0.0",
908
- type: "module",
909
- scripts: {
910
- // Node-target SSR (Nitro). vite dev for local; build emits
911
- // .output/server/index.mjs (PORT-aware) which the CF Sandbox runs.
912
- // `predev` runs automatically on `npm run dev` (the sandbox's launch
913
- // command) → creates the DB tables before the server starts, so a
914
- // DB query in a route loader never hits "no such table".
915
- predev: "drizzle-kit push --force",
916
- dev: "vite dev --host",
917
- build: "vite build",
918
- // `preview` is what HelloLeo's PM2 publish runs for vite projects to
919
- // serve the production build. For node-SSR that's the Nitro server
920
- // (honours PORT). `prepreview` runs first → ensures DB tables exist.
921
- prepreview: "drizzle-kit push --force",
922
- preview: "node .output/server/index.mjs",
923
- start: "node .output/server/index.mjs",
924
- "db:generate": "drizzle-kit generate",
925
- "db:push": "drizzle-kit push --force",
926
- deploy: "wrangler deploy",
927
- lint: "biome check --write .",
928
- },
929
- dependencies: {
930
- react: REACT,
931
- "react-dom": REACT,
932
- "@tanstack/react-router": TANSTACK,
933
- "@tanstack/react-start": TANSTACK,
934
- "drizzle-orm": "^0.36.4",
935
- "@libsql/client": "^0.14.0",
936
- "class-variance-authority": "^0.7.1",
937
- clsx: "^2.1.1",
938
- "tailwind-merge": "^2.6.0",
939
- "lucide-react": "^0.468.0",
940
- "@radix-ui/react-slot": "^1.1.1",
941
- },
942
- devDependencies: {
943
- wrangler: "^4.20.0",
944
- "@cloudflare/workers-types": "^4.20250601.0",
945
- "drizzle-kit": "^0.28.1",
946
- "@types/node": "^22.0.0",
947
- "@types/react": "^19.0.0",
948
- "@types/react-dom": "^19.0.0",
949
- "@vitejs/plugin-react": "^4.3.4",
950
- "@biomejs/biome": BIOME,
951
- autoprefixer: "^10.4.20",
952
- postcss: "^8.4.49",
953
- tailwindcss: "^3.4.17",
954
- typescript: TS,
955
- vite: VITE,
956
- },
957
- },
958
- null,
959
- 2,
960
- );
961
- }
962
-
963
- function ssrViteConfig(): string {
964
- return `import { defineConfig } from "vite"
965
- import react from "@vitejs/plugin-react"
966
- import { tanstackStart } from "@tanstack/react-start/plugin/vite"
967
- import path from "node:path"
968
-
969
- // TanStack Start default (Node server) target -> dist/server/server.js + dist/client.
970
- // The CF Sandbox container runs that server (PORT-aware), proven by the POC.
971
- export default defineConfig({
972
- plugins: [tanstackStart({ customViteReactPlugin: true }), react()],
973
- // allowedHosts:true is REQUIRED for the HelloLeo preview sandbox (proxied host).
974
- server: { allowedHosts: true },
975
- // Pre-bundle shared deps at boot so vite doesn't re-optimize + restart the dev
976
- // server mid-request (which 500s the first SSR load with "transport disconnected").
977
- optimizeDeps: {
978
- include: ["clsx", "tailwind-merge", "class-variance-authority", "lucide-react"],
979
- },
980
- resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
981
- // __WORKERD__ tells src/db which runtime it's building for: true only for the
982
- // prod cloudflare-module build (NITRO_PRESET set by the publish flow), false
983
- // for dev and the Node preview build. Lets getDb dead-code-eliminate the wrong
984
- // DB driver per target.
985
- define: {
986
- __WORKERD__: JSON.stringify(process.env.NITRO_PRESET === "cloudflare-module"),
987
- },
988
- // "cloudflare:workers" is a workerd-only module (src/db reads the D1 binding
989
- // from it in prod). vite's SSR pass can't resolve it — externalize so the
990
- // workerd build keeps it as an import; on other targets it's DCE'd away.
991
- build: { rollupOptions: { external: ["cloudflare:workers"] } },
992
- })
993
- `;
994
- }
995
-
996
- function ssrWranglerJsonc(name: string): string {
997
- return `{
998
- // PROD target (Workers for Platforms). Dev/preview runs the Node server in the
999
- // CF Sandbox container and does NOT use this file.
1000
- //
1001
- // NOTE: HelloLeo's publish flow REGENERATES this file at deploy time — it
1002
- // auto-creates the D1 database, fills in database_id, and pins its own
1003
- // compatibility_date. This file is the local-dev / manual \`wrangler deploy\`
1004
- // copy; for a manual deploy, run \`wrangler d1 create ${name}\` and paste the
1005
- // id below first. compatibility_date is kept at/under the runtime baseline so
1006
- // \`wrangler dev\` works on the pinned workerd binary.
1007
- "$schema": "node_modules/wrangler/config-schema.json",
1008
- "name": "${name}",
1009
- "compatibility_date": "2026-04-01",
1010
- "compatibility_flags": ["nodejs_compat"],
1011
- "observability": { "enabled": true },
1012
- "assets": { "directory": ".output/public", "binding": "ASSETS" },
1013
- "d1_databases": [
1014
- { "binding": "DB", "database_name": "${name}", "database_id": "" }
1015
- ]
1016
- }
1017
- `;
1018
- }
1019
-
1020
- function ssrRouter(): string {
1021
- return `import { createRouter as createTanStackRouter } from "@tanstack/react-router"
1022
- import { routeTree } from "./routeTree.gen"
1023
-
1024
- export function createRouter() {
1025
- return createTanStackRouter({
1026
- routeTree,
1027
- defaultPreload: "intent",
1028
- scrollRestoration: true,
1029
- })
1030
- }
1031
-
1032
- declare module "@tanstack/react-router" {
1033
- interface Register {
1034
- router: ReturnType<typeof createRouter>
1035
- }
1036
- }
1037
- `;
1038
- }
1039
-
1040
- function ssrRootRoute(fontFamily: string): string {
1041
- const fontParam = fontFamily.replace(/\s+/g, "+");
1042
- return `import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"
1043
- import appCss from "../styles/app.css?url"
1044
-
1045
- export const Route = createRootRoute({
1046
- head: () => ({
1047
- meta: [
1048
- { charSet: "utf-8" },
1049
- { name: "viewport", content: "width=device-width, initial-scale=1" },
1050
- { title: "App" },
1051
- ],
1052
- // The design-system font is loaded here (SSR has no index.html). Keep the
1053
- // family in sync with --font-sans / --font-heading in src/styles/app.css.
1054
- links: [
1055
- { rel: "preconnect", href: "https://fonts.googleapis.com" },
1056
- { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
1057
- { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap" },
1058
- { rel: "stylesheet", href: appCss },
1059
- ],
1060
- }),
1061
- errorComponent: ({ error }) => (
1062
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-3 p-8">
1063
- <h1 className="text-2xl font-heading font-semibold text-destructive">Something went wrong</h1>
1064
- <pre className="max-w-full overflow-auto rounded-md bg-muted p-4 text-left text-xs text-muted-foreground">{error instanceof Error ? error.message : String(error)}</pre>
1065
- </main>
1066
- ),
1067
- notFoundComponent: () => (
1068
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-2 p-8">
1069
- <h1 className="text-2xl font-heading font-semibold">404</h1>
1070
- <p className="text-muted-foreground">This page could not be found.</p>
1071
- </main>
1072
- ),
1073
- component: RootComponent,
1074
- })
1075
-
1076
- function RootComponent() {
1077
- return (
1078
- <html lang="en">
1079
- <head>
1080
- <HeadContent />
1081
- </head>
1082
- <body className="min-h-screen bg-background text-foreground font-sans">
1083
- <Outlet />
1084
- <Scripts />
1085
- </body>
1086
- </html>
1087
- )
1088
- }
1089
- `;
1090
- }
1091
-
1092
- function ssrIndexRoute(): string {
1093
- return `import { createFileRoute } from "@tanstack/react-router"
1094
- import { createServerFn } from "@tanstack/react-start"
1095
- import { Button } from "@/components/ui/button"
1096
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
1097
-
1098
- // Server functions (TanStack Start 1.131.x API — IMPORTANT, do not invent
1099
- // methods):
1100
- // - No input: createServerFn({ method: "GET" }).handler(async () => {...})
1101
- // - With input: createServerFn({ method: "POST" })
1102
- // .validator((d: { title: string }) => d) // NOT .inputType()
1103
- // .handler(async ({ data }) => { data.title }) // input is { data }
1104
- // Call it as: addTask({ data: { title } }) // NOT addTask({ title })
1105
- // - DB access: \`const db = await getDb()\` (async). Resolves D1 in prod,
1106
- // libsql in dev. Safe in server fns AND route loaders — just
1107
- // never at module top. Prefer server functions for mutations.
1108
- // Example DB-backed server fn (dev=libsql, prod=D1, same code):
1109
- // const getItems = createServerFn({ method: "GET" }).handler(async () => {
1110
- // const db = await getDb()
1111
- // return await db.select().from(items)
1112
- // })
1113
- const getServerInfo = createServerFn({ method: "GET" }).handler(async () => {
1114
- return { renderedAt: new Date().toISOString() }
1115
- })
1116
-
1117
- export const Route = createFileRoute("/")({
1118
- loader: () => getServerInfo(),
1119
- component: Home,
1120
- })
1121
-
1122
- function Home() {
1123
- const info = Route.useLoaderData()
1124
- return (
1125
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8">
1126
- <Card className="w-full">
1127
- <CardHeader>
1128
- <CardTitle className="text-2xl">Welcome to your app</CardTitle>
1129
- <CardDescription>TanStack Start SSR (workerd) · Tailwind · shadcn/ui · Biome · Drizzle/D1</CardDescription>
1130
- </CardHeader>
1131
- <CardContent className="flex flex-col gap-3">
1132
- <p className="text-sm text-muted-foreground">Server-rendered at {info.renderedAt}</p>
1133
- <div className="flex gap-3">
1134
- <Button>Get started</Button>
1135
- <Button variant="outline">Learn more</Button>
1136
- </div>
1137
- </CardContent>
1138
- </Card>
1139
- </main>
1140
- )
1141
- }
1142
- `;
1143
- }
1144
-
1145
- function ssrDbSchema(): string {
1146
- return `import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
1147
-
1148
- // Example table. dev uses local D1 (miniflare), prod uses the D1 binding "DB".
1149
- export const items = sqliteTable("items", {
1150
- id: integer("id").primaryKey({ autoIncrement: true }),
1151
- title: text("title").notNull(),
1152
- createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
1153
- })
1154
- `;
1155
- }
1156
-
1157
- function ssrDbIndex(): string {
1158
- return `/// <reference types="@cloudflare/workers-types" />
1159
- import { drizzle as drizzleD1 } from "drizzle-orm/d1"
1160
- import * as schema from "./schema"
1161
-
1162
- // Resolve the DB for the CURRENT request. ASYNC — always \`await getDb()\`.
1163
- // Works from ANYWHERE on the server (route loaders, server functions,
1164
- // beforeLoad) because it does NOT depend on AsyncLocalStorage / getEvent (which
1165
- // is only populated inside server functions under workerd — calling getDb in a
1166
- // loader the old way threw "No HTTPEvent found in AsyncLocalStorage").
1167
- // - PROD (workerd): the D1 binding read straight from the per-request env.
1168
- // - DEV (node SSR): a local libsql file, imported lazily and behind the DEV
1169
- // guard so neither @libsql/client nor node:stream is ever bundled into the
1170
- // workerd build (which would break it).
1171
- // Same sqlite schema both ways.
1172
- // __WORKERD__ is replaced at build time by vite (see vite.config.ts): true only
1173
- // for the prod cloudflare-module build, false for dev + the Node preview build.
1174
- // Using it (not import.meta.env.DEV) is what lets the Node preview — also a
1175
- // production build — still take the libsql path. It's a literal, so the unused
1176
- // branch (and its driver) is dead-code-eliminated from each bundle.
1177
- declare const __WORKERD__: boolean
1178
49
 
1179
- export async function getDb() {
1180
- if (__WORKERD__) {
1181
- const { env } = await import("cloudflare:workers")
1182
- return drizzleD1((env as unknown as { DB: D1Database }).DB, { schema })
1183
- }
1184
- const { drizzle } = await import("drizzle-orm/libsql")
1185
- const { createClient } = await import("@libsql/client")
1186
- return drizzle(
1187
- createClient({ url: process.env.DATABASE_URL ?? "file:./local.db" }),
1188
- { schema },
1189
- )
1190
- }
1191
- `;
1192
- }
1193
-
1194
- function ssrDrizzleConfig(): string {
1195
- return `import { defineConfig } from "drizzle-kit"
1196
-
1197
- // Dev/preview: push the schema straight into the local libsql file (no D1 creds
1198
- // needed) — this is what \`predev\` runs so tables exist before the dev server.
1199
- // Prod (D1) uses generated migrations applied via wrangler, not this push.
1200
- export default defineConfig({
1201
- dialect: "sqlite",
1202
- schema: "./src/db/schema.ts",
1203
- out: "./drizzle",
1204
- dbCredentials: { url: process.env.DATABASE_URL ?? "file:./local.db" },
1205
- })
1206
- `;
1207
- }
1208
-
1209
- function gitignore(mode: string): string {
1210
- const base = `node_modules
1211
- dist
1212
- .output
1213
- .nitro
1214
- .tanstack
1215
- .vite
1216
- local.db
1217
- *.local
1218
- .DS_Store
1219
-
1220
- # Generated by the TanStack Router/Start vite plugin
1221
- src/routeTree.gen.ts
1222
- `;
1223
- return mode === "ssr" ? `${base}.wrangler\n.dev.vars\n` : base;
1224
- }
1225
-
1226
- // ---------- writer ----------
1227
-
1228
- interface ScaffoldAppOptions {
1229
- name?: string;
1230
- mode?: "spa" | "ssr";
1231
- primaryColor?: string;
1232
- fontFamily?: string;
1233
- borderRadius?: string;
1234
- }
1235
-
1236
- async function writeAppScaffold(
1237
- opts: ScaffoldAppOptions,
1238
- dir: string,
1239
- ): Promise<string[]> {
1240
- const name = opts.name ?? "leo-app";
1241
- const mode = opts.mode ?? "spa";
1242
- const primary = normalizePrimaryColor(
1243
- opts.primaryColor ?? "221.2 83.2% 53.3%",
50
+ await mkdir(path.join(dir, ".leo"), { recursive: true });
51
+ const leoPath = path.join(".leo", "project.json");
52
+ await Bun.write(
53
+ path.join(dir, leoPath),
54
+ JSON.stringify({ $schema: 1, scaffold: SCAFFOLD_VERSION }, null, 2),
1244
55
  );
1245
- const font = opts.fontFamily ?? "Inter";
1246
- const radius = opts.borderRadius ?? "0.5rem";
1247
-
1248
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
1249
- await mkdir(path.join(dir, "src", "components", "ui"), { recursive: true });
1250
- await mkdir(path.join(dir, "src", "routes"), { recursive: true });
1251
- await mkdir(path.join(dir, "src", "styles"), { recursive: true });
1252
56
 
1253
- // Files common to both modes.
1254
- const files: [string, string][] = [
1255
- ["tsconfig.json", tsconfig()],
1256
- ["biome.json", biomeJson()],
1257
- ["components.json", componentsJson()],
1258
- ["tailwind.config.js", tailwindConfig()],
1259
- ["postcss.config.js", postcssConfig()],
1260
- [".gitignore", gitignore(mode)],
1261
- [path.join("src", "styles", "app.css"), indexCss(primary, font, radius)],
1262
- [path.join("src", "lib", "utils.ts"), utilsTs()],
1263
- [path.join("src", "lib", "proxy.ts"), proxyTs()],
1264
- [path.join("src", "components", "ui", "button.tsx"), uiButton()],
1265
- [path.join("src", "components", "ui", "card.tsx"), uiCard()],
1266
- [path.join("src", "components", "ui", "input.tsx"), uiInput()],
1267
- [path.join("src", "components", "ui", "textarea.tsx"), uiTextarea()],
1268
- [path.join("src", "components", "ui", "label.tsx"), uiLabel()],
1269
- [path.join("src", "components", "ui", "badge.tsx"), uiBadge()],
1270
- ];
1271
-
1272
- if (mode === "spa") {
1273
- files.push(
1274
- ["package.json", spaPackageJson(name)],
1275
- ["vite.config.ts", spaViteConfig()],
1276
- ["index.html", spaIndexHtml(font)],
1277
- [path.join("src", "main.tsx"), spaMainTsx()],
1278
- [path.join("src", "routes", "__root.tsx"), spaRootRoute()],
1279
- [path.join("src", "routes", "index.tsx"), spaIndexRoute()],
1280
- );
1281
- } else {
1282
- await mkdir(path.join(dir, "src", "db"), { recursive: true });
1283
- files.push(
1284
- ["package.json", ssrPackageJson(name)],
1285
- ["vite.config.ts", ssrViteConfig()],
1286
- ["wrangler.jsonc", ssrWranglerJsonc(name)],
1287
- ["drizzle.config.ts", ssrDrizzleConfig()],
1288
- [path.join("src", "router.tsx"), ssrRouter()],
1289
- [path.join("src", "routes", "__root.tsx"), ssrRootRoute(font)],
1290
- [path.join("src", "routes", "index.tsx"), ssrIndexRoute()],
1291
- [path.join("src", "db", "schema.ts"), ssrDbSchema()],
1292
- [path.join("src", "db", "index.ts"), ssrDbIndex()],
1293
- );
1294
- }
1295
-
1296
- const created: string[] = [];
1297
- for (const [filePath, content] of files) {
1298
- await Bun.write(path.join(dir, filePath), content);
1299
- created.push(filePath);
1300
- }
1301
- return created.sort();
57
+ return [...created, leoPath];
1302
58
  }
1303
59
 
1304
- export const scaffoldApp = tool({
60
+ export const scaffold = tool({
1305
61
  description:
1306
- "Scaffold a modern TanStack app with the HelloLeo design system, ShadcnUI, " +
1307
- "Biome and strict TypeScript. Two modes: mode='spa' (TanStack Router, " +
1308
- "client-only -- use for static frontends / dashboards) or mode='ssr' " +
1309
- "(TanStack Start on Cloudflare workerd -- use when the project needs a " +
1310
- "backend, SSR, server functions, or a database via D1/Drizzle). " +
1311
- "Creates package.json, vite.config.ts, tsconfig.json (strict), biome.json, " +
1312
- "components.json, tailwind.config.js, postcss.config.js, " +
1313
- "src/styles/app.css (design tokens, light+dark), src/lib/utils.ts + proxy.ts, " +
1314
- "shadcn primitives in src/components/ui (Button, Card, Input, Textarea, " +
1315
- "Label, Badge), and TanStack routes in src/routes. " +
1316
- "Call this FIRST for any new project before writing feature routes/components. " +
1317
- "For NetSuite projects use `setup_netsuite` instead.",
62
+ "Scaffold a new React 19 + Vite + Tailwind CSS v4 + TanStack Router project " +
63
+ "with shadcn/ui baked in. Renders the full template: TanStack file-based " +
64
+ "routing (src/routes/), the complete shadcn/ui component set under " +
65
+ "src/components/ui, src/lib/utils.ts (cn helper), src/lib/proxy.ts " +
66
+ "(HelloLeo integration proxy), src/styles.css (shadcn design tokens, " +
67
+ "light+dark), plus package.json, vite.config.ts, tsconfig.json, " +
68
+ "tsr.config.json, components.json, .gitignore and the generated " +
69
+ "src/routeTree.gen.ts. Call this FIRST for any new project before adding " +
70
+ "routes and pages. For NetSuite projects (NetSuite MCP in the live list), " +
71
+ "call `setup_netsuite` INSTEAD -- it bundles base scaffold + OAuth wiring " +
72
+ "in one call. Do not call both.",
1318
73
  args: {
1319
74
  name: tool.schema
1320
75
  .string()
1321
76
  .optional()
1322
- .describe("Project name for package.json (default: leo-app)"),
1323
- mode: tool.schema
1324
- .enum(["spa", "ssr"])
1325
- .optional()
1326
- .describe(
1327
- "'spa' = TanStack Router client-only (default). 'ssr' = TanStack Start " +
1328
- "on Cloudflare workerd with SSR + D1/Drizzle. Pick 'ssr' when the app " +
1329
- "needs a backend, server functions, auth, or a database.",
1330
- ),
1331
- primaryColor: tool.schema
1332
- .string()
1333
- .optional()
1334
- .describe(
1335
- 'Primary brand color as raw HSL triplet, e.g. "221.2 83.2% 53.3%". ' +
1336
- 'Hex like "#3b82f6" is auto-converted. Default: blue.',
1337
- ),
1338
- fontFamily: tool.schema
1339
- .string()
1340
- .optional()
1341
- .describe('Google Font family, e.g. "Inter", "Poppins" (default: Inter)'),
1342
- borderRadius: tool.schema
1343
- .string()
1344
- .optional()
1345
- .describe('Base border radius, e.g. "0.5rem" (default: 0.5rem)'),
77
+ .describe("Project name for package.json (default: leo-project)"),
1346
78
  },
1347
79
  async execute(args, context) {
1348
- const mode = args.mode ?? "spa";
1349
- const created = await writeAppScaffold(args, context.directory);
1350
- const next =
1351
- mode === "ssr"
1352
- ? [
1353
- "Mode: SSR (TanStack Start, Node server — runs in the CF Sandbox container).",
1354
- "Dev server: `npm run dev`.",
1355
- "Add routes in src/routes/*.tsx; server logic via createServerFn.",
1356
- "Database: edit src/db/schema.ts, `npm run db:push`, query via `db` from src/db.",
1357
- " (dev = local libsql file; prod = D1, see wrangler.jsonc).",
1358
- ]
1359
- : [
1360
- "Mode: SPA (TanStack Router, client-only).",
1361
- "Dev server: `npm run dev`.",
1362
- "Add routes in src/routes/*.tsx (file-based; routeTree.gen.ts is generated).",
1363
- ];
80
+ const created = await writeBaseScaffold(args, context.directory);
1364
81
  return [
1365
- `Scaffolded ${created.length} files (mode=${mode}):`,
82
+ `Scaffolded ${created.length} files:`,
1366
83
  ...created.map((f) => ` - ${f}`),
1367
84
  "",
1368
- ...next,
1369
- "",
1370
- "Design system: src/styles/app.css tokens (light+dark) drive Tailwind +",
1371
- " shadcn. Never hardcode colors -- use bg-primary, text-foreground, etc.",
1372
- "UI primitives live in src/components/ui (Button, Card scaffolded).",
1373
- " Add more shadcn components consuming the same tokens.",
1374
- "Integrations: callIntegration() from src/lib/proxy.ts.",
1375
- "Lint/format: `npm run lint` (Biome).",
85
+ "Next: add routes as files under src/routes/ (TanStack file-based",
86
+ " routing). Edit src/routes/index.tsx for the home page.",
87
+ "UI: shadcn/ui components are already in src/components/ui -- import and",
88
+ " use them; do not attempt to re-add via the shadcn CLI.",
89
+ "Design system: edit tokens in src/styles.css (light + dark blocks).",
90
+ "Data fetching: React Query is wired (QueryClient on the router context +",
91
+ " QueryClientProvider). ALWAYS fetch with @tanstack/react-query --",
92
+ " useQuery / useMutation in components, or a route loader calling",
93
+ " context.queryClient.ensureQueryData(). Do NOT hand-roll fetches with",
94
+ " useEffect + useState. Wrap the fetch call (usually callIntegration())",
95
+ " in the queryFn.",
96
+ "Integration calls: use callIntegration() from src/lib/proxy.ts -- as the",
97
+ " queryFn body, never on its own in useEffect. Its header comment",
98
+ " documents slugs, response unwrapping, pagination, and frontend-only vs",
99
+ " HelloLeo Cloud routing.",
1376
100
  ].join("\n");
1377
101
  },
1378
102
  });
1379
103
 
1380
- // Registered as `scaffold` — this is THE scaffold tool (replaced the legacy
1381
- // React+Vite `scaffold`). The coder UI keys its scaffold events off this name.
1382
104
  export const ScaffoldPlugin: Plugin = async () => {
1383
105
  return {
1384
- tool: { scaffold: scaffoldApp },
106
+ tool: { scaffold: scaffold },
1385
107
  };
1386
108
  };