@helloleo/plugins 0.1.13 → 0.1.14

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.
@@ -1,1343 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import path from "node:path";
3
- import { type BaseScaffoldOptions, normalizePrimaryColor } from "./scaffold.ts";
4
-
5
- /**
6
- * TanStack Start project template (SSR React deployed as a Cloudflare Worker).
7
- *
8
- * Replaces the legacy Vite-SPA scaffold (`writeBaseScaffold`) for all new
9
- * non-NetSuite projects. Backend is real server-function code hitting a D1
10
- * binding directly -- HelloLeo Cloud IS the Start server. Dev runs in a
11
- * CodeSandbox microVM (`vite dev`, Miniflare-local D1); prod ships to Workers
12
- * for Platforms with a real D1 binding.
13
- *
14
- * NetSuite projects still use the legacy Vite-SPA scaffold (see
15
- * `setup-netsuite.ts`) until that OAuth flow is migrated separately.
16
- */
17
-
18
- function packageJson(name: string): string {
19
- return JSON.stringify(
20
- {
21
- name,
22
- private: true,
23
- type: "module",
24
- imports: { "#/*": "./src/*" },
25
- scripts: {
26
- dev: "vite dev",
27
- build: "vite build",
28
- preview: "vite preview",
29
- deploy: "vite build && wrangler deploy",
30
- check: "biome check",
31
- },
32
- dependencies: {
33
- "@cloudflare/vite-plugin": "^1.26.0",
34
- "@tailwindcss/vite": "^4.1.18",
35
- "@tanstack/react-query": "latest",
36
- "@tanstack/react-router": "latest",
37
- "@tanstack/react-start": "latest",
38
- "@tanstack/router-plugin": "^1.132.0",
39
- "class-variance-authority": "^0.7.1",
40
- clsx: "^2.1.1",
41
- jose: "^6.1.0",
42
- "lucide-react": "^0.577.0",
43
- react: "^19.2.0",
44
- "react-dom": "^19.2.0",
45
- "tailwind-merge": "^3.0.2",
46
- tailwindcss: "^4.1.18",
47
- zod: "^4.3.6",
48
- },
49
- devDependencies: {
50
- "@biomejs/biome": "2.4.5",
51
- "@cloudflare/workers-types": "^4.20250906.0",
52
- "@types/better-sqlite3": "^7.6.11",
53
- "@types/node": "^22.10.2",
54
- "better-sqlite3": "^11.8.0",
55
- "@types/react": "^19.2.0",
56
- "@types/react-dom": "^19.2.0",
57
- "@vitejs/plugin-react": "^6.0.1",
58
- typescript: "^5.9.0",
59
- vite: "^8.0.0",
60
- wrangler: "^4.70.0",
61
- },
62
- },
63
- null,
64
- 2,
65
- );
66
- }
67
-
68
- function viteConfig(): string {
69
- return `import { fileURLToPath } from "node:url";
70
- import { defineConfig } from "vite";
71
- import { tanstackStart } from "@tanstack/react-start/plugin/vite";
72
- import viteReact from "@vitejs/plugin-react";
73
- import tailwindcss from "@tailwindcss/vite";
74
- import { cloudflare } from "@cloudflare/vite-plugin";
75
-
76
- const src = fileURLToPath(new URL("./src", import.meta.url));
77
- const cfDevShim = fileURLToPath(
78
- new URL("./src/integrations/hl-cloud/cf-dev-shim.ts", import.meta.url),
79
- );
80
-
81
- export default defineConfig(({ command }) => {
82
- const isDev = command === "serve";
83
- return {
84
- // allowedHosts lets the CodeSandbox preview proxy (*.csb.app) reach the
85
- // dev server. Required for the HelloLeo dev preview to render.
86
- server: { allowedHosts: true },
87
- resolve: {
88
- alias: {
89
- // Match the tsconfig path aliases (#/* and @/*).
90
- "@": src,
91
- "#": src,
92
- // Dev runs under plain node -- workerd can't run in the preview VM --
93
- // so shim the worker-only "cloudflare:workers" module with a local
94
- // sqlite-backed D1 + dev env. Prod build uses the real binding.
95
- ...(isDev ? { "cloudflare:workers": cfDevShim } : {}),
96
- },
97
- },
98
- plugins: [
99
- // Prod: real workerd runtime + D1 binding. Dev: skipped (workerd can't
100
- // run nested in the preview VM); plain vite SSR + the shim above.
101
- ...(isDev ? [] : [cloudflare({ viteEnvironment: { name: "ssr" } })]),
102
- tailwindcss(),
103
- tanstackStart(),
104
- viteReact(),
105
- ],
106
- };
107
- });
108
- `;
109
- }
110
-
111
- function tsconfigJson(): string {
112
- return JSON.stringify(
113
- {
114
- include: ["**/*.ts", "**/*.tsx", "worker-configuration.d.ts"],
115
- compilerOptions: {
116
- target: "ES2022",
117
- jsx: "react-jsx",
118
- module: "ESNext",
119
- paths: { "#/*": ["./src/*"], "@/*": ["./src/*"] },
120
- lib: ["ES2022", "DOM", "DOM.Iterable"],
121
- types: ["vite/client"],
122
- moduleResolution: "bundler",
123
- allowImportingTsExtensions: true,
124
- verbatimModuleSyntax: true,
125
- noEmit: true,
126
- skipLibCheck: true,
127
- strict: true,
128
- noUnusedLocals: true,
129
- noUnusedParameters: true,
130
- noFallthroughCasesInSwitch: true,
131
- noUncheckedSideEffectImports: true,
132
- },
133
- },
134
- null,
135
- 2,
136
- );
137
- }
138
-
139
- function biomeJson(): string {
140
- return JSON.stringify(
141
- {
142
- $schema: "https://biomejs.dev/schemas/2.4.5/schema.json",
143
- vcs: { enabled: false, clientKind: "git", useIgnoreFile: false },
144
- files: {
145
- ignoreUnknown: false,
146
- includes: [
147
- "**/src/**/*",
148
- "**/vite.config.ts",
149
- "!**/src/routeTree.gen.ts",
150
- "!**/src/styles.css",
151
- ],
152
- },
153
- formatter: { enabled: true, indentStyle: "tab" },
154
- assist: { actions: { source: { organizeImports: "on" } } },
155
- linter: { enabled: true, rules: { recommended: true } },
156
- javascript: { formatter: { quoteStyle: "double" } },
157
- },
158
- null,
159
- 2,
160
- );
161
- }
162
-
163
- function componentsJson(): string {
164
- return JSON.stringify(
165
- {
166
- $schema: "https://ui.shadcn.com/schema.json",
167
- style: "new-york",
168
- rsc: false,
169
- tsx: true,
170
- tailwind: {
171
- config: "",
172
- css: "src/styles.css",
173
- baseColor: "zinc",
174
- cssVariables: true,
175
- prefix: "",
176
- },
177
- aliases: {
178
- components: "#/components",
179
- utils: "#/lib/utils",
180
- ui: "#/components/ui",
181
- lib: "#/lib",
182
- hooks: "#/hooks",
183
- },
184
- iconLibrary: "lucide",
185
- },
186
- null,
187
- 2,
188
- );
189
- }
190
-
191
- function wranglerJsonc(name: string): string {
192
- return `{
193
- "$schema": "node_modules/wrangler/config-schema.json",
194
- "name": "${name}",
195
- "compatibility_date": "2025-09-02",
196
- "compatibility_flags": ["nodejs_compat"],
197
- // src/server.ts wraps the TanStack Start server-entry to render graceful
198
- // 500 pages instead of leaking raw SSR stack traces.
199
- "main": "src/server.ts",
200
- // HelloLeo Cloud database. In dev, @cloudflare/vite-plugin emulates this
201
- // with a local Miniflare sqlite file. The real prod database_id is injected
202
- // by the HelloLeo deploy pipeline -- do not hardcode a prod id here.
203
- "d1_databases": [
204
- { "binding": "DB", "database_id": "local-dev-placeholder" }
205
- ]
206
- }
207
- `;
208
- }
209
-
210
- function workerConfigurationDts(): string {
211
- return `/// <reference types="@cloudflare/workers-types" />
212
-
213
- // Cloudflare bindings for this project's Worker. \`wrangler types\` can
214
- // regenerate this. The HelloLeo deploy pipeline injects the real D1
215
- // database_id at publish time.
216
- interface Env {
217
- // HelloLeo Cloud database (D1). Access request-scoped via getDb() in
218
- // src/integrations/hl-cloud/db.server.ts -- never read at module scope.
219
- DB: D1Database;
220
- // HMAC secret used to sign/verify HelloLeo Cloud auth JWTs. Set as a Worker
221
- // secret. The issuer/verify mechanism is the auth integration point -- see
222
- // src/integrations/hl-cloud/auth.functions.ts.
223
- HL_CLOUD_JWT_SECRET: string;
224
- // This project's HelloLeo UUID, used by the server-side integration proxy.
225
- PROJECT_UUID?: string;
226
- }
227
-
228
- declare module "cloudflare:workers" {
229
- export const env: Env;
230
- }
231
- `;
232
- }
233
-
234
- function gitignore(): string {
235
- return `node_modules
236
- dist
237
- .output
238
- .vinxi
239
- .wrangler
240
- .dev.db
241
- .dev.db-*
242
- .env
243
- .env.*
244
- !.env.example
245
- *.local
246
- .DS_Store
247
- `;
248
- }
249
-
250
- // DEV-ONLY shim for "cloudflare:workers" (aliased in vite.config when
251
- // command==="serve"). workerd can't run in the preview VM, so back the DB
252
- // binding with a local sqlite file; SSR + server functions behave like prod.
253
- function cfDevShimTs(): string {
254
- return `import Database from "better-sqlite3";
255
- import { existsSync, readFileSync } from "node:fs";
256
-
257
- const sqlite = new Database(".dev.db");
258
- sqlite.pragma("journal_mode = WAL");
259
- if (existsSync("schema.sql")) {
260
- try {
261
- sqlite.exec(readFileSync("schema.sql", "utf-8"));
262
- } catch (e) {
263
- console.error("[cf-dev-shim] failed to apply schema.sql:", e);
264
- }
265
- }
266
-
267
- // Minimal D1Database-compatible adapter over better-sqlite3 (sync), presented
268
- // with D1's async prepare().bind().all()/first()/run() surface.
269
- function makeDb() {
270
- return {
271
- prepare(sql: string) {
272
- const stmt = sqlite.prepare(sql);
273
- const bound = (params: unknown[]) => ({
274
- async all<T = unknown>() {
275
- return { results: stmt.all(...params) as T[], success: true, meta: {} };
276
- },
277
- async first<T = unknown>() {
278
- return (stmt.get(...params) as T) ?? null;
279
- },
280
- async run() {
281
- const r = stmt.run(...params);
282
- return {
283
- success: true,
284
- meta: { changes: r.changes, last_row_id: Number(r.lastInsertRowid) },
285
- };
286
- },
287
- });
288
- return { bind: (...params: unknown[]) => bound(params), ...bound([]) };
289
- },
290
- };
291
- }
292
-
293
- export const env = {
294
- DB: makeDb(),
295
- HL_CLOUD_JWT_SECRET:
296
- process.env.HL_CLOUD_JWT_SECRET ?? "dev-insecure-secret-change-me",
297
- PROJECT_UUID: process.env.PROJECT_UUID ?? "dev",
298
- } as unknown as Env;
299
- `;
300
- }
301
-
302
- function schemaSql(): string {
303
- return `-- HelloLeo Cloud schema (D1 / sqlite).
304
- -- Dev: apply to the local Miniflare DB. Prod: applied by the deploy pipeline.
305
- -- wrangler d1 execute DB --local --file=./schema.sql
306
- -- wrangler d1 execute DB --remote --file=./schema.sql
307
-
308
- CREATE TABLE IF NOT EXISTS users (
309
- id TEXT PRIMARY KEY,
310
- email TEXT NOT NULL UNIQUE,
311
- password_hash TEXT NOT NULL,
312
- created_at INTEGER NOT NULL
313
- );
314
-
315
- -- Example domain table. Every row is owned by a user; server functions always
316
- -- filter by context.userId so users never see each other's data.
317
- CREATE TABLE IF NOT EXISTS items (
318
- id TEXT PRIMARY KEY,
319
- user_id TEXT NOT NULL,
320
- title TEXT NOT NULL DEFAULT '',
321
- created_at INTEGER NOT NULL
322
- );
323
- CREATE INDEX IF NOT EXISTS items_user_id_idx ON items (user_id);
324
- `;
325
- }
326
-
327
- function stylesCss(primary: string, font: string, radius: string): string {
328
- const fontValue = `'${font}', ui-sans-serif, system-ui, sans-serif`;
329
- return `@import 'tailwindcss';
330
-
331
- @custom-variant dark (&:is(.dark *));
332
-
333
- /*
334
- * HelloLeo Design System -- READ BEFORE STYLING.
335
- *
336
- * The CSS variables below are the single source of truth for the project's
337
- * design tokens. They power the no-code Design panel in the HelloLeo webapp:
338
- * the user edits colors / fonts / radius / shadows visually, and the panel
339
- * writes back to THIS file (src/styles.css). Keeping the conventions intact
340
- * is what unlocks that round-trip.
341
- *
342
- * Conventions (do not break):
343
- * - Colors are stored as raw HSL triplets (no hsl() wrapper, no #hex).
344
- * The @theme block below wraps them via hsl(var(--token)) so Tailwind v4
345
- * utilities (bg-primary, text-foreground, border-border, ...) resolve.
346
- * Never inline #hex in components -- always use the token utilities.
347
- * - --primary / --secondary / --accent / --destructive / --success /
348
- * --warning each have a paired -foreground for text on that surface.
349
- * - --radius drives radius-lg/md/sm via calc() in @theme. Edit here.
350
- * - --font-sans for body, --font-heading for headings. Both load from the
351
- * Google Fonts <link> in src/routes/__root.tsx.
352
- * - .dark mirrors :root one-for-one. When adding a token, add it to BOTH
353
- * blocks or dark mode breaks.
354
- *
355
- * UI primitives (Button, Card, Input, ...) MUST live in src/components/ui/
356
- * and consume these tokens via Tailwind classes -- never hardcoded colors.
357
- */
358
-
359
- :root {
360
- --background: 0 0% 100%;
361
- --foreground: 222.2 84% 4.9%;
362
- --card: 0 0% 100%;
363
- --card-foreground: 222.2 84% 4.9%;
364
- --primary: ${primary};
365
- --primary-foreground: 210 40% 98%;
366
- --secondary: 210 40% 96.1%;
367
- --secondary-foreground: 222.2 47.4% 11.2%;
368
- --muted: 210 40% 96.1%;
369
- --muted-foreground: 215.4 16.3% 46.9%;
370
- --accent: 210 40% 96.1%;
371
- --accent-foreground: 222.2 47.4% 11.2%;
372
- --destructive: 0 84.2% 60.2%;
373
- --destructive-foreground: 210 40% 98%;
374
- --success: 142 76% 36%;
375
- --success-foreground: 210 40% 98%;
376
- --warning: 38 92% 50%;
377
- --warning-foreground: 222.2 47.4% 11.2%;
378
- --border: 214.3 31.8% 91.4%;
379
- --input: 214.3 31.8% 91.4%;
380
- --ring: 221.2 83.2% 53.3%;
381
- --radius: ${radius};
382
- --font-sans: ${fontValue};
383
- --font-heading: ${fontValue};
384
- }
385
-
386
- .dark {
387
- --background: 222.2 84% 4.9%;
388
- --foreground: 210 40% 98%;
389
- --card: 222.2 84% 4.9%;
390
- --card-foreground: 210 40% 98%;
391
- --primary: 217.2 91.2% 59.8%;
392
- --primary-foreground: 222.2 47.4% 11.2%;
393
- --secondary: 217.2 32.6% 17.5%;
394
- --secondary-foreground: 210 40% 98%;
395
- --muted: 217.2 32.6% 17.5%;
396
- --muted-foreground: 215 20.2% 65.1%;
397
- --accent: 217.2 32.6% 17.5%;
398
- --accent-foreground: 210 40% 98%;
399
- --destructive: 0 62.8% 30.6%;
400
- --destructive-foreground: 210 40% 98%;
401
- --success: 142 76% 36%;
402
- --success-foreground: 210 40% 98%;
403
- --warning: 38 92% 50%;
404
- --warning-foreground: 222.2 47.4% 11.2%;
405
- --border: 217.2 32.6% 17.5%;
406
- --input: 217.2 32.6% 17.5%;
407
- --ring: 224.3 76.3% 48%;
408
- --font-sans: ${fontValue};
409
- --font-heading: ${fontValue};
410
- }
411
-
412
- @theme inline {
413
- --color-background: hsl(var(--background));
414
- --color-foreground: hsl(var(--foreground));
415
- --color-card: hsl(var(--card));
416
- --color-card-foreground: hsl(var(--card-foreground));
417
- --color-primary: hsl(var(--primary));
418
- --color-primary-foreground: hsl(var(--primary-foreground));
419
- --color-secondary: hsl(var(--secondary));
420
- --color-secondary-foreground: hsl(var(--secondary-foreground));
421
- --color-muted: hsl(var(--muted));
422
- --color-muted-foreground: hsl(var(--muted-foreground));
423
- --color-accent: hsl(var(--accent));
424
- --color-accent-foreground: hsl(var(--accent-foreground));
425
- --color-destructive: hsl(var(--destructive));
426
- --color-destructive-foreground: hsl(var(--destructive-foreground));
427
- --color-success: hsl(var(--success));
428
- --color-success-foreground: hsl(var(--success-foreground));
429
- --color-warning: hsl(var(--warning));
430
- --color-warning-foreground: hsl(var(--warning-foreground));
431
- --color-border: hsl(var(--border));
432
- --color-input: hsl(var(--input));
433
- --color-ring: hsl(var(--ring));
434
- --radius-sm: calc(var(--radius) - 4px);
435
- --radius-md: calc(var(--radius) - 2px);
436
- --radius-lg: var(--radius);
437
- --radius-xl: calc(var(--radius) + 4px);
438
- --font-sans: var(--font-sans);
439
- --font-heading: var(--font-heading);
440
- }
441
-
442
- @layer base {
443
- * {
444
- @apply border-border;
445
- }
446
- body {
447
- @apply bg-background text-foreground font-sans antialiased;
448
- }
449
- }
450
- `;
451
- }
452
-
453
- function utilsTs(): string {
454
- return `import { clsx, type ClassValue } from "clsx";
455
- import { twMerge } from "tailwind-merge";
456
-
457
- export function cn(...inputs: ClassValue[]) {
458
- return twMerge(clsx(inputs));
459
- }
460
- `;
461
- }
462
-
463
- function routerTsx(): string {
464
- return `import { QueryClient } from "@tanstack/react-query";
465
- import { createRouter as createTanStackRouter } from "@tanstack/react-router";
466
- import { routeTree } from "./routeTree.gen";
467
-
468
- export function getRouter() {
469
- const queryClient = new QueryClient();
470
-
471
- const router = createTanStackRouter({
472
- routeTree,
473
- context: { queryClient },
474
- scrollRestoration: true,
475
- defaultPreload: "intent",
476
- defaultPreloadStaleTime: 0,
477
- });
478
-
479
- return router;
480
- }
481
-
482
- declare module "@tanstack/react-router" {
483
- interface Register {
484
- router: ReturnType<typeof getRouter>;
485
- }
486
- }
487
- `;
488
- }
489
-
490
- function serverTs(): string {
491
- return `// Cloudflare Worker entry. Wraps the TanStack Start server-entry so SSR
492
- // failures render a clean 500 page instead of leaking a raw stack trace.
493
- type ServerEntry = {
494
- fetch: (
495
- request: Request,
496
- env: unknown,
497
- ctx: unknown,
498
- ) => Promise<Response> | Response;
499
- };
500
-
501
- let serverEntryPromise: Promise<ServerEntry> | undefined;
502
-
503
- async function getServerEntry(): Promise<ServerEntry> {
504
- if (!serverEntryPromise) {
505
- serverEntryPromise = import("@tanstack/react-start/server-entry").then(
506
- (m) => ((m as { default?: ServerEntry }).default ?? m) as ServerEntry,
507
- );
508
- }
509
- return serverEntryPromise;
510
- }
511
-
512
- function errorPage(): Response {
513
- const html =
514
- "<!doctype html><html><head><meta charset='utf-8'>" +
515
- "<meta name='viewport' content='width=device-width, initial-scale=1'>" +
516
- "<title>Something went wrong</title></head>" +
517
- "<body style='font-family:system-ui;display:grid;place-items:center;" +
518
- "min-height:100vh;margin:0;color:#0f172a'>" +
519
- "<div style='text-align:center'><h1>Something went wrong</h1>" +
520
- "<p>The server hit an unexpected error. Try again.</p></div></body></html>";
521
- return new Response(html, {
522
- status: 500,
523
- headers: { "content-type": "text/html; charset=utf-8" },
524
- });
525
- }
526
-
527
- export default {
528
- async fetch(request: Request, env: unknown, ctx: unknown) {
529
- try {
530
- const handler = await getServerEntry();
531
- return await handler.fetch(request, env, ctx);
532
- } catch (error) {
533
- console.error(error);
534
- return errorPage();
535
- }
536
- },
537
- };
538
- `;
539
- }
540
-
541
- function startTs(): string {
542
- return `import { createStart, createMiddleware } from "@tanstack/react-start";
543
- import { attachHlCloudAuth } from "./integrations/hl-cloud/auth-attacher";
544
-
545
- // Re-throw framework redirects/notFound; turn anything else into a 500 so a
546
- // thrown error in a server function never crashes the whole request.
547
- const errorMiddleware = createMiddleware().server(async ({ next }) => {
548
- return await next();
549
- });
550
-
551
- export const startInstance = createStart(() => ({
552
- requestMiddleware: [errorMiddleware],
553
- // Global client middleware: attaches the HelloLeo Cloud bearer token to
554
- // every server-function RPC. Without this, authenticated calls are rejected.
555
- functionMiddleware: [attachHlCloudAuth],
556
- }));
557
- `;
558
- }
559
-
560
- function rootTsx(font: string): string {
561
- const fontParam = font.replace(/\s+/g, "+");
562
- return `import {
563
- HeadContent,
564
- Link,
565
- Outlet,
566
- Scripts,
567
- createRootRouteWithContext,
568
- } from "@tanstack/react-router";
569
- import {
570
- QueryClient,
571
- QueryClientProvider,
572
- } from "@tanstack/react-query";
573
- import type { ReactNode } from "react";
574
-
575
- import appCss from "../styles.css?url";
576
-
577
- const THEME_INIT = "(function(){try{var m=localStorage.getItem('theme');var d=window.matchMedia('(prefers-color-scheme: dark)').matches;var r=(m==='dark'||(m!=='light'&&d))?'dark':'light';document.documentElement.classList.add(r);document.documentElement.style.colorScheme=r;}catch(e){}})();";
578
-
579
- export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
580
- head: () => ({
581
- meta: [
582
- { charSet: "utf-8" },
583
- { name: "viewport", content: "width=device-width, initial-scale=1" },
584
- { title: "App" },
585
- ],
586
- links: [
587
- { rel: "stylesheet", href: appCss },
588
- { rel: "preconnect", href: "https://fonts.googleapis.com" },
589
- { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" },
590
- {
591
- rel: "stylesheet",
592
- href: "https://fonts.googleapis.com/css2?family=${fontParam}:wght@400;500;600;700&display=swap",
593
- },
594
- ],
595
- }),
596
- shellComponent: RootShell,
597
- component: RootComponent,
598
- notFoundComponent: NotFound,
599
- errorComponent: ErrorPage,
600
- });
601
-
602
- function RootShell({ children }: { children: ReactNode }) {
603
- return (
604
- <html lang="en" suppressHydrationWarning>
605
- <head>
606
- <script dangerouslySetInnerHTML={{ __html: THEME_INIT }} />
607
- <HeadContent />
608
- </head>
609
- <body>
610
- {children}
611
- <Scripts />
612
- </body>
613
- </html>
614
- );
615
- }
616
-
617
- function RootComponent() {
618
- const { queryClient } = Route.useRouteContext();
619
- return (
620
- <QueryClientProvider client={queryClient}>
621
- <Outlet />
622
- </QueryClientProvider>
623
- );
624
- }
625
-
626
- function NotFound() {
627
- return (
628
- <div className="grid min-h-screen place-items-center px-4 text-center">
629
- <div>
630
- <h1 className="text-6xl font-bold">404</h1>
631
- <p className="mt-2 text-muted-foreground">This page does not exist.</p>
632
- <Link to="/" className="mt-4 inline-block text-primary underline">
633
- Go home
634
- </Link>
635
- </div>
636
- </div>
637
- );
638
- }
639
-
640
- function ErrorPage({ error }: { error: Error }) {
641
- return (
642
- <div className="grid min-h-screen place-items-center px-4 text-center">
643
- <div>
644
- <h1 className="text-xl font-semibold">Something went wrong</h1>
645
- <p className="mt-2 text-sm text-muted-foreground">{error.message}</p>
646
- </div>
647
- </div>
648
- );
649
- }
650
- `;
651
- }
652
-
653
- function indexRouteTsx(name: string): string {
654
- return `import { createFileRoute, Link } from "@tanstack/react-router";
655
-
656
- export const Route = createFileRoute("/")({ component: Home });
657
-
658
- function Home() {
659
- return (
660
- <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 px-6 text-center">
661
- <h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
662
- ${name}
663
- </h1>
664
- <p className="max-w-md text-muted-foreground">
665
- A TanStack Start app on HelloLeo Cloud. Data lives in D1 and is read
666
- through type-safe server functions. Edit src/routes to build your app.
667
- </p>
668
- <div className="flex gap-3">
669
- <Link
670
- to="/dashboard"
671
- className="rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground hover:opacity-90"
672
- >
673
- Open dashboard
674
- </Link>
675
- <Link
676
- to="/login"
677
- className="rounded-md border border-input px-5 py-2.5 text-sm font-semibold hover:bg-accent"
678
- >
679
- Sign in
680
- </Link>
681
- </div>
682
- </main>
683
- );
684
- }
685
- `;
686
- }
687
-
688
- function authenticatedLayoutTsx(): string {
689
- return `import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
690
- import { getToken } from "../integrations/hl-cloud/client";
691
-
692
- // Route guard for everything under /_authenticated. Runs on the client before
693
- // load: no token -> redirect to /login. Server functions independently
694
- // re-verify the JWT (requireHlCloudAuth), so this is UX, not the security
695
- // boundary -- never rely on a client guard alone to protect data.
696
- export const Route = createFileRoute("/_authenticated")({
697
- beforeLoad: ({ location }) => {
698
- if (typeof window === "undefined") return;
699
- if (!getToken()) {
700
- throw redirect({ to: "/login", search: { redirect: location.href } });
701
- }
702
- },
703
- component: () => <Outlet />,
704
- });
705
- `;
706
- }
707
-
708
- function dashboardRouteTsx(): string {
709
- return `import { createFileRoute } from "@tanstack/react-router";
710
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
711
- import { useState } from "react";
712
- import { createItem, listItems } from "../../lib/example.functions";
713
- import { signOut } from "../../integrations/hl-cloud/client";
714
-
715
- export const Route = createFileRoute("/_authenticated/dashboard")({
716
- component: Dashboard,
717
- });
718
-
719
- function Dashboard() {
720
- const qc = useQueryClient();
721
- const [title, setTitle] = useState("");
722
- const items = useQuery({
723
- queryKey: ["items"],
724
- queryFn: () => listItems(),
725
- });
726
- const add = useMutation({
727
- mutationFn: (t: string) => createItem({ data: { title: t } }),
728
- onSuccess: () => {
729
- setTitle("");
730
- qc.invalidateQueries({ queryKey: ["items"] });
731
- },
732
- });
733
-
734
- return (
735
- <main className="mx-auto max-w-xl px-6 py-12">
736
- <div className="mb-6 flex items-center justify-between">
737
- <h1 className="text-2xl font-bold">Your items</h1>
738
- <button
739
- type="button"
740
- onClick={() => signOut()}
741
- className="text-sm text-muted-foreground underline"
742
- >
743
- Sign out
744
- </button>
745
- </div>
746
-
747
- <form
748
- onSubmit={(e) => {
749
- e.preventDefault();
750
- if (title.trim()) add.mutate(title.trim());
751
- }}
752
- className="mb-6 flex gap-2"
753
- >
754
- <input
755
- value={title}
756
- onChange={(e) => setTitle(e.target.value)}
757
- placeholder="New item title"
758
- className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
759
- />
760
- <button
761
- type="submit"
762
- disabled={add.isPending}
763
- className="rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-50"
764
- >
765
- Add
766
- </button>
767
- </form>
768
-
769
- {items.isLoading ? (
770
- <p className="text-sm text-muted-foreground">Loading...</p>
771
- ) : (
772
- <ul className="space-y-2">
773
- {(items.data ?? []).map((item) => (
774
- <li
775
- key={item.id}
776
- className="rounded-md border border-border px-3 py-2 text-sm"
777
- >
778
- {item.title}
779
- </li>
780
- ))}
781
- </ul>
782
- )}
783
- </main>
784
- );
785
- }
786
- `;
787
- }
788
-
789
- function loginRouteTsx(): string {
790
- return `import { createFileRoute, useNavigate } from "@tanstack/react-router";
791
- import { type FormEvent, useState } from "react";
792
- import { signIn, signUp } from "../integrations/hl-cloud/client";
793
-
794
- export const Route = createFileRoute("/login")({
795
- validateSearch: (search: Record<string, unknown>) => ({
796
- redirect: typeof search.redirect === "string" ? search.redirect : "/dashboard",
797
- }),
798
- component: LoginPage,
799
- });
800
-
801
- function LoginPage() {
802
- const navigate = useNavigate();
803
- const { redirect } = Route.useSearch();
804
- const [mode, setMode] = useState<"signin" | "signup">("signin");
805
- const [email, setEmail] = useState("");
806
- const [password, setPassword] = useState("");
807
- const [error, setError] = useState<string | null>(null);
808
- const [loading, setLoading] = useState(false);
809
-
810
- async function submit(e: FormEvent) {
811
- e.preventDefault();
812
- setLoading(true);
813
- setError(null);
814
- try {
815
- if (mode === "signin") {
816
- await signIn(email, password);
817
- } else {
818
- await signUp(email, password);
819
- }
820
- navigate({ to: redirect as "/dashboard", replace: true });
821
- } catch (err) {
822
- setError(err instanceof Error ? err.message : "Authentication failed");
823
- } finally {
824
- setLoading(false);
825
- }
826
- }
827
-
828
- return (
829
- <div className="flex min-h-screen items-center justify-center px-4">
830
- <div className="w-full max-w-sm">
831
- <div className="mb-6 text-center">
832
- <h1 className="text-2xl font-bold">Welcome</h1>
833
- <p className="mt-1 text-sm text-muted-foreground">
834
- {mode === "signin" ? "Sign in to continue" : "Create your account"}
835
- </p>
836
- </div>
837
- <form onSubmit={submit} className="space-y-4">
838
- <input
839
- type="email"
840
- required
841
- placeholder="Email"
842
- value={email}
843
- onChange={(e) => setEmail(e.target.value)}
844
- autoComplete="email"
845
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
846
- />
847
- <input
848
- type="password"
849
- required
850
- minLength={8}
851
- placeholder="Password"
852
- value={password}
853
- onChange={(e) => setPassword(e.target.value)}
854
- autoComplete={mode === "signin" ? "current-password" : "new-password"}
855
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
856
- />
857
- {error ? <p className="text-sm text-destructive">{error}</p> : null}
858
- <button
859
- type="submit"
860
- disabled={loading}
861
- className="w-full rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-50"
862
- >
863
- {loading ? "Please wait..." : mode === "signin" ? "Sign in" : "Sign up"}
864
- </button>
865
- </form>
866
- <button
867
- type="button"
868
- onClick={() => setMode(mode === "signin" ? "signup" : "signin")}
869
- className="mt-4 w-full text-center text-sm text-muted-foreground underline"
870
- >
871
- {mode === "signin"
872
- ? "Need an account? Sign up"
873
- : "Have an account? Sign in"}
874
- </button>
875
- </div>
876
- </div>
877
- );
878
- }
879
- `;
880
- }
881
-
882
- function dbServerTs(): string {
883
- return `// Request-scoped accessors for Cloudflare bindings. On Workers, env binds at
884
- // REQUEST time -- reading bindings at module scope yields undefined. Always
885
- // call these INSIDE a server function / handler, never at the top level.
886
- import { env } from "cloudflare:workers";
887
-
888
- export function getEnv(): Env {
889
- return env;
890
- }
891
-
892
- export function getDb(): D1Database {
893
- const db = env.DB;
894
- if (!db) {
895
- throw new Error(
896
- "D1 binding 'DB' is not configured. Check wrangler.jsonc d1_databases.",
897
- );
898
- }
899
- return db;
900
- }
901
- `;
902
- }
903
-
904
- function authMiddlewareTs(): string {
905
- return `import { createMiddleware } from "@tanstack/react-start";
906
- import { getRequest } from "@tanstack/react-start/server";
907
- import { jwtVerify } from "jose";
908
- import { getDb, getEnv } from "./db.server";
909
-
910
- // Server-side gate for any server function that touches user data. Validates
911
- // the HelloLeo Cloud JWT from the Authorization header, resolves the user id,
912
- // and exposes { db, userId, claims } on context. This is the real security
913
- // boundary -- the client route guard is only UX.
914
- //
915
- // AUTH INTEGRATION POINT: this verifies an HMAC (HS256) JWT signed with
916
- // env.HL_CLOUD_JWT_SECRET (see auth.functions.ts). To back identity with an
917
- // external provider (Odoo / NetSuite / Salesforce) instead, swap the verify
918
- // step here and resolve claims from that provider.
919
- export const requireHlCloudAuth = createMiddleware({ type: "function" }).server(
920
- async ({ next }) => {
921
- const { HL_CLOUD_JWT_SECRET } = getEnv();
922
- if (!HL_CLOUD_JWT_SECRET) {
923
- throw new Error("Server auth is not configured: missing HL_CLOUD_JWT_SECRET");
924
- }
925
-
926
- const request = getRequest();
927
- const authHeader = request?.headers.get("authorization");
928
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
929
- throw new Error("Unauthorized: missing bearer token");
930
- }
931
- const token = authHeader.slice("Bearer ".length).trim();
932
- if (!token) {
933
- throw new Error("Unauthorized: empty bearer token");
934
- }
935
-
936
- let userId: string;
937
- let claims: Record<string, unknown>;
938
- try {
939
- const { payload } = await jwtVerify(
940
- token,
941
- new TextEncoder().encode(HL_CLOUD_JWT_SECRET),
942
- );
943
- if (!payload.sub) throw new Error("no sub");
944
- userId = payload.sub;
945
- claims = payload as Record<string, unknown>;
946
- } catch {
947
- throw new Error("Unauthorized: invalid or expired token");
948
- }
949
-
950
- return next({ context: { db: getDb(), userId, claims } });
951
- },
952
- );
953
- `;
954
- }
955
-
956
- function authAttacherTs(): string {
957
- return `import { createMiddleware } from "@tanstack/react-start";
958
- import { getToken } from "./client";
959
-
960
- // Global client middleware (registered in src/start.ts). Attaches the stored
961
- // HelloLeo Cloud bearer token to every server-function RPC so requireHlCloudAuth
962
- // can authenticate it. Without this, authenticated server functions reject.
963
- export const attachHlCloudAuth = createMiddleware({ type: "function" }).client(
964
- async ({ next }) => {
965
- const token = getToken();
966
- return next({
967
- headers: token ? { Authorization: "Bearer " + token } : {},
968
- });
969
- },
970
- );
971
- `;
972
- }
973
-
974
- function clientTs(): string {
975
- return `// Client-side HelloLeo Cloud auth. Holds the bearer token in localStorage and
976
- // wraps the auth server functions. The token is attached to every server-fn
977
- // RPC by attachHlCloudAuth (src/integrations/hl-cloud/auth-attacher.ts).
978
- //
979
- // NOTE: localStorage is readable by any script on the page (XSS exposure). It
980
- // is used here for simplicity and to survive the cross-site preview iframe.
981
- // Harden per your threat model before shipping sensitive data.
982
- import { signInFn, signUpFn } from "./auth.functions";
983
-
984
- const TOKEN_KEY = "hl_token";
985
- const listeners = new Set<() => void>();
986
-
987
- export function getToken(): string | null {
988
- if (typeof window === "undefined") return null;
989
- try {
990
- return window.localStorage.getItem(TOKEN_KEY);
991
- } catch {
992
- return null;
993
- }
994
- }
995
-
996
- function setToken(token: string | null) {
997
- try {
998
- if (token) window.localStorage.setItem(TOKEN_KEY, token);
999
- else window.localStorage.removeItem(TOKEN_KEY);
1000
- } catch {
1001
- /* ignore storage failures */
1002
- }
1003
- for (const fn of listeners) fn();
1004
- }
1005
-
1006
- export function onAuthChange(fn: () => void): () => void {
1007
- listeners.add(fn);
1008
- return () => listeners.delete(fn);
1009
- }
1010
-
1011
- export async function signIn(email: string, password: string) {
1012
- const { token } = await signInFn({ data: { email, password } });
1013
- setToken(token);
1014
- return token;
1015
- }
1016
-
1017
- export async function signUp(email: string, password: string) {
1018
- const { token } = await signUpFn({ data: { email, password } });
1019
- setToken(token);
1020
- return token;
1021
- }
1022
-
1023
- export function signOut() {
1024
- setToken(null);
1025
- if (typeof window !== "undefined") window.location.assign("/login");
1026
- }
1027
- `;
1028
- }
1029
-
1030
- function authFunctionsTs(): string {
1031
- return `import { createServerFn } from "@tanstack/react-start";
1032
- import { SignJWT } from "jose";
1033
- import { z } from "zod";
1034
- import { getDb, getEnv } from "./db.server";
1035
-
1036
- // HelloLeo Cloud auth issuer: D1-backed identity + HMAC (HS256) JWTs.
1037
- //
1038
- // AUTH INTEGRATION POINT: this is the default, self-contained identity store.
1039
- // To back identity with an external provider (Odoo / NetSuite / Salesforce),
1040
- // replace signInFn/signUpFn with that provider's login and keep issueToken so
1041
- // the rest of the app (requireHlCloudAuth) stays unchanged.
1042
-
1043
- const credentials = z.object({
1044
- email: z.string().email().max(320),
1045
- password: z.string().min(8).max(200),
1046
- });
1047
-
1048
- type UserRow = { id: string; email: string; password_hash: string };
1049
-
1050
- function toHex(bytes: Uint8Array): string {
1051
- let out = "";
1052
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
1053
- return out;
1054
- }
1055
-
1056
- function fromHex(hex: string): Uint8Array {
1057
- const out = new Uint8Array(hex.length / 2);
1058
- for (let i = 0; i < out.length; i++) {
1059
- out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1060
- }
1061
- return out;
1062
- }
1063
-
1064
- // PBKDF2-SHA256 via Web Crypto (Workers-native, no node deps). Stored as
1065
- // "<saltHex>:<hashHex>".
1066
- async function hashPassword(password: string, saltHex?: string): Promise<string> {
1067
- const enc = new TextEncoder();
1068
- const salt = saltHex ? fromHex(saltHex) : crypto.getRandomValues(new Uint8Array(16));
1069
- const key = await crypto.subtle.importKey(
1070
- "raw",
1071
- enc.encode(password),
1072
- "PBKDF2",
1073
- false,
1074
- ["deriveBits"],
1075
- );
1076
- const bits = await crypto.subtle.deriveBits(
1077
- { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
1078
- key,
1079
- 256,
1080
- );
1081
- return toHex(salt) + ":" + toHex(new Uint8Array(bits));
1082
- }
1083
-
1084
- function timingSafeEqual(a: string, b: string): boolean {
1085
- if (a.length !== b.length) return false;
1086
- let diff = 0;
1087
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1088
- return diff === 0;
1089
- }
1090
-
1091
- async function verifyPassword(password: string, stored: string): Promise<boolean> {
1092
- const saltHex = stored.split(":")[0];
1093
- if (!saltHex) return false;
1094
- return timingSafeEqual(await hashPassword(password, saltHex), stored);
1095
- }
1096
-
1097
- async function issueToken(userId: string, email: string): Promise<string> {
1098
- const { HL_CLOUD_JWT_SECRET } = getEnv();
1099
- if (!HL_CLOUD_JWT_SECRET) {
1100
- throw new Error("Server auth is not configured: missing HL_CLOUD_JWT_SECRET");
1101
- }
1102
- return await new SignJWT({ email })
1103
- .setProtectedHeader({ alg: "HS256" })
1104
- .setSubject(userId)
1105
- .setIssuedAt()
1106
- .setExpirationTime("7d")
1107
- .sign(new TextEncoder().encode(HL_CLOUD_JWT_SECRET));
1108
- }
1109
-
1110
- export const signUpFn = createServerFn({ method: "POST" })
1111
- .inputValidator((input: { email: string; password: string }) =>
1112
- credentials.parse(input),
1113
- )
1114
- .handler(async ({ data }) => {
1115
- const db = getDb();
1116
- const email = data.email.toLowerCase();
1117
- const existing = await db
1118
- .prepare("SELECT id FROM users WHERE email = ?")
1119
- .bind(email)
1120
- .first<{ id: string }>();
1121
- if (existing) throw new Error("An account with that email already exists");
1122
-
1123
- const id = crypto.randomUUID();
1124
- const passwordHash = await hashPassword(data.password);
1125
- await db
1126
- .prepare(
1127
- "INSERT INTO users (id, email, password_hash, created_at) VALUES (?, ?, ?, ?)",
1128
- )
1129
- .bind(id, email, passwordHash, Date.now())
1130
- .run();
1131
-
1132
- return { token: await issueToken(id, email), user: { id, email } };
1133
- });
1134
-
1135
- export const signInFn = createServerFn({ method: "POST" })
1136
- .inputValidator((input: { email: string; password: string }) =>
1137
- credentials.parse(input),
1138
- )
1139
- .handler(async ({ data }) => {
1140
- const db = getDb();
1141
- const email = data.email.toLowerCase();
1142
- const user = await db
1143
- .prepare("SELECT id, email, password_hash FROM users WHERE email = ?")
1144
- .bind(email)
1145
- .first<UserRow>();
1146
- if (!user || !(await verifyPassword(data.password, user.password_hash))) {
1147
- throw new Error("Invalid email or password");
1148
- }
1149
- return {
1150
- token: await issueToken(user.id, user.email),
1151
- user: { id: user.id, email: user.email },
1152
- };
1153
- });
1154
- `;
1155
- }
1156
-
1157
- function exampleFunctionsTs(): string {
1158
- return `import { createServerFn } from "@tanstack/react-start";
1159
- import { z } from "zod";
1160
- import { requireHlCloudAuth } from "../integrations/hl-cloud/auth-middleware";
1161
-
1162
- // The HelloLeo Cloud data pattern: server functions guarded by
1163
- // requireHlCloudAuth, querying the D1 binding directly via context.db. Every
1164
- // query filters by context.userId so users only ever see their own rows.
1165
- // This replaces the old MCP execute() REST path -- data is real backend code.
1166
-
1167
- export type Item = { id: string; title: string; created_at: number };
1168
-
1169
- export const listItems = createServerFn({ method: "GET" })
1170
- .middleware([requireHlCloudAuth])
1171
- .handler(async ({ context }) => {
1172
- const { results } = await context.db
1173
- .prepare(
1174
- "SELECT id, title, created_at FROM items WHERE user_id = ? ORDER BY created_at DESC LIMIT 500",
1175
- )
1176
- .bind(context.userId)
1177
- .all<Item>();
1178
- return results;
1179
- });
1180
-
1181
- export const createItem = createServerFn({ method: "POST" })
1182
- .middleware([requireHlCloudAuth])
1183
- .inputValidator((input: { title: string }) =>
1184
- z.object({ title: z.string().min(1).max(500) }).parse(input),
1185
- )
1186
- .handler(async ({ data, context }) => {
1187
- const id = crypto.randomUUID();
1188
- await context.db
1189
- .prepare("INSERT INTO items (id, user_id, title, created_at) VALUES (?, ?, ?, ?)")
1190
- .bind(id, context.userId, data.title, Date.now())
1191
- .run();
1192
- return { id, title: data.title, created_at: Date.now() } satisfies Item;
1193
- });
1194
- `;
1195
- }
1196
-
1197
- function integrationServerTs(): string {
1198
- return `// HelloLeo Integration Proxy -- SERVER-SIDE helper for connected integrations.
1199
- //
1200
- // All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
1201
- // Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through this
1202
- // single proxy. The proxy handles auth and CORS using credentials the user
1203
- // configured in Project Settings -- you NEVER pass an API key from code, and
1204
- // you NEVER call third-party hosts directly.
1205
- //
1206
- // On TanStack Start, integration calls run SERVER-SIDE (inside server
1207
- // functions), so secrets stay on the Worker and never reach the browser.
1208
- // Call callIntegration() from a createServerFn().handler, not from a component.
1209
- //
1210
- // Integration slugs:
1211
- // Use the exact slug from the Enabled MCP list with the "pipedream-" prefix
1212
- // removed, e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth". Native
1213
- // integrations (odoo, airtable) keep their plain name.
1214
- //
1215
- // Response shape (always wrapped): { success, status, data }. Unwrap data by
1216
- // provider:
1217
- // odoo -> data.result ; airtable -> data.records ; notion -> data.results ;
1218
- // microsoft 365 -> data.value ; klaviyo -> data.data ;
1219
- // supabase / dimomaint -> data
1220
- //
1221
- // Pagination: put query params directly in the endpoint string, e.g.
1222
- // endpoint: "/api/tickets?cursor=xyz&per_page=100". Never fetch-everything
1223
- // then filter -- paginate at the source.
1224
- import { getEnv } from "../integrations/hl-cloud/db.server";
1225
-
1226
- export interface ProxyRequest {
1227
- integration: string;
1228
- endpoint: string;
1229
- method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
1230
- body?: unknown;
1231
- headers?: Record<string, string>;
1232
- }
1233
-
1234
- export interface ProxyResponse<T = unknown> {
1235
- success: boolean;
1236
- status: number;
1237
- data: T;
1238
- }
1239
-
1240
- export async function callIntegration<T = unknown>(
1241
- req: ProxyRequest,
1242
- ): Promise<ProxyResponse<T>> {
1243
- const uuid = getEnv().PROJECT_UUID;
1244
- if (!uuid) {
1245
- throw new Error(
1246
- "PROJECT_UUID is not set. HelloLeo Cloud must be provisioned for integrations.",
1247
- );
1248
- }
1249
- const res = await fetch(
1250
- "https://api.app.helloleo.dev/api/integration-proxy/" + uuid,
1251
- {
1252
- method: "POST",
1253
- headers: { "Content-Type": "application/json" },
1254
- body: JSON.stringify({ method: "POST", ...req }),
1255
- },
1256
- );
1257
- return (await res.json()) as ProxyResponse<T>;
1258
- }
1259
- `;
1260
- }
1261
-
1262
- /**
1263
- * Write the TanStack Start project template. Returns the list of relative
1264
- * file paths written. Used by the `scaffold` tool for all new non-NetSuite
1265
- * projects.
1266
- */
1267
- export async function writeStartScaffold(
1268
- opts: BaseScaffoldOptions,
1269
- dir: string,
1270
- ): Promise<string[]> {
1271
- const name = opts.name ?? "leo-project";
1272
- const primary = normalizePrimaryColor(
1273
- opts.primaryColor ?? "221.2 83.2% 53.3%",
1274
- );
1275
- const font = opts.fontFamily ?? "Inter";
1276
- const radius = opts.borderRadius ?? "0.5rem";
1277
-
1278
- await mkdir(path.join(dir, "src", "routes", "_authenticated"), {
1279
- recursive: true,
1280
- });
1281
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
1282
- await mkdir(path.join(dir, "src", "integrations", "hl-cloud"), {
1283
- recursive: true,
1284
- });
1285
-
1286
- const files: [string, string][] = [
1287
- ["package.json", packageJson(name)],
1288
- ["vite.config.ts", viteConfig()],
1289
- ["tsconfig.json", tsconfigJson()],
1290
- ["biome.json", biomeJson()],
1291
- ["components.json", componentsJson()],
1292
- ["wrangler.jsonc", wranglerJsonc(name)],
1293
- ["worker-configuration.d.ts", workerConfigurationDts()],
1294
- [".gitignore", gitignore()],
1295
- ["schema.sql", schemaSql()],
1296
- [path.join("src", "styles.css"), stylesCss(primary, font, radius)],
1297
- [path.join("src", "router.tsx"), routerTsx()],
1298
- [path.join("src", "server.ts"), serverTs()],
1299
- [path.join("src", "start.ts"), startTs()],
1300
- [path.join("src", "routes", "__root.tsx"), rootTsx(font)],
1301
- [path.join("src", "routes", "index.tsx"), indexRouteTsx(name)],
1302
- [path.join("src", "routes", "login.tsx"), loginRouteTsx()],
1303
- [
1304
- path.join("src", "routes", "_authenticated.tsx"),
1305
- authenticatedLayoutTsx(),
1306
- ],
1307
- [
1308
- path.join("src", "routes", "_authenticated", "dashboard.tsx"),
1309
- dashboardRouteTsx(),
1310
- ],
1311
- [path.join("src", "lib", "utils.ts"), utilsTs()],
1312
- [path.join("src", "lib", "example.functions.ts"), exampleFunctionsTs()],
1313
- [path.join("src", "lib", "integration.server.ts"), integrationServerTs()],
1314
- [
1315
- path.join("src", "integrations", "hl-cloud", "db.server.ts"),
1316
- dbServerTs(),
1317
- ],
1318
- [
1319
- path.join("src", "integrations", "hl-cloud", "auth-middleware.ts"),
1320
- authMiddlewareTs(),
1321
- ],
1322
- [
1323
- path.join("src", "integrations", "hl-cloud", "auth-attacher.ts"),
1324
- authAttacherTs(),
1325
- ],
1326
- [path.join("src", "integrations", "hl-cloud", "client.ts"), clientTs()],
1327
- [
1328
- path.join("src", "integrations", "hl-cloud", "auth.functions.ts"),
1329
- authFunctionsTs(),
1330
- ],
1331
- [
1332
- path.join("src", "integrations", "hl-cloud", "cf-dev-shim.ts"),
1333
- cfDevShimTs(),
1334
- ],
1335
- ];
1336
-
1337
- const created: string[] = [];
1338
- for (const [filePath, content] of files) {
1339
- await Bun.write(path.join(dir, filePath), content);
1340
- created.push(filePath);
1341
- }
1342
- return created;
1343
- }