create-surf-app 0.1.26-alpha.0 → 1.0.0-alpha.10

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 (89) hide show
  1. package/dist/chunk-FDATV75D.js +87 -0
  2. package/dist/cli.js +1 -7
  3. package/dist/index.js +1 -1
  4. package/dist/templates/default/CLAUDE.md +20 -0
  5. package/dist/templates/default/backend/.env.example +2 -0
  6. package/dist/templates/default/backend/package.json +2 -3
  7. package/dist/templates/default/backend/scripts/check-env.js +40 -0
  8. package/dist/templates/default/backend/server.js +1 -1
  9. package/dist/templates/default/frontend/.env.example +3 -0
  10. package/dist/templates/default/frontend/package.json +2 -2
  11. package/dist/templates/default/frontend/scripts/check-env.cjs +51 -0
  12. package/dist/templates/default/frontend/src/vite-env.d.ts +0 -9
  13. package/dist/templates/default/frontend/vite.config.ts +6 -21
  14. package/dist/templates/default/package.json +11 -0
  15. package/dist/templates/nextjs/.env.example +3 -0
  16. package/dist/templates/nextjs/CLAUDE.md +93 -1
  17. package/dist/templates/nextjs/app/api/__sync-schema/route.ts +18 -0
  18. package/dist/templates/nextjs/app/api/cron/route.ts +31 -0
  19. package/dist/templates/nextjs/app/api/health/route.ts +1 -1
  20. package/dist/templates/nextjs/app/api/market/price/route.ts +5 -21
  21. package/dist/templates/nextjs/app/globals.css +1 -25
  22. package/dist/templates/nextjs/app/layout.tsx +16 -25
  23. package/dist/templates/nextjs/app/page.tsx +20 -60
  24. package/dist/templates/nextjs/app/providers.tsx +37 -0
  25. package/dist/templates/nextjs/components/ui/accordion.tsx +55 -0
  26. package/dist/templates/nextjs/components/ui/alert.tsx +59 -0
  27. package/dist/templates/nextjs/components/ui/aspect-ratio.tsx +5 -0
  28. package/dist/templates/nextjs/components/ui/avatar.tsx +48 -0
  29. package/dist/templates/nextjs/components/ui/badge.tsx +36 -0
  30. package/dist/templates/nextjs/components/ui/breadcrumb.tsx +115 -0
  31. package/dist/templates/nextjs/components/ui/button.tsx +57 -0
  32. package/dist/templates/nextjs/components/ui/calendar.tsx +211 -0
  33. package/dist/templates/nextjs/components/ui/card.tsx +76 -0
  34. package/dist/templates/nextjs/components/ui/carousel.tsx +262 -0
  35. package/dist/templates/nextjs/components/ui/checkbox.tsx +30 -0
  36. package/dist/templates/nextjs/components/ui/collapsible.tsx +9 -0
  37. package/dist/templates/nextjs/components/ui/command.tsx +153 -0
  38. package/dist/templates/nextjs/components/ui/context-menu.tsx +200 -0
  39. package/dist/templates/nextjs/components/ui/dialog.tsx +120 -0
  40. package/dist/templates/nextjs/components/ui/drawer.tsx +118 -0
  41. package/dist/templates/nextjs/components/ui/dropdown-menu.tsx +201 -0
  42. package/dist/templates/nextjs/components/ui/form.tsx +176 -0
  43. package/dist/templates/nextjs/components/ui/hover-card.tsx +29 -0
  44. package/dist/templates/nextjs/components/ui/input.tsx +22 -0
  45. package/dist/templates/nextjs/components/ui/label.tsx +26 -0
  46. package/dist/templates/nextjs/components/ui/menubar.tsx +256 -0
  47. package/dist/templates/nextjs/components/ui/navigation-menu.tsx +128 -0
  48. package/dist/templates/nextjs/components/ui/popover.tsx +33 -0
  49. package/dist/templates/nextjs/components/ui/progress.tsx +26 -0
  50. package/dist/templates/nextjs/components/ui/radio-group.tsx +42 -0
  51. package/dist/templates/nextjs/components/ui/resizable.tsx +43 -0
  52. package/dist/templates/nextjs/components/ui/scroll-area.tsx +46 -0
  53. package/dist/templates/nextjs/components/ui/select.tsx +157 -0
  54. package/dist/templates/nextjs/components/ui/separator.tsx +31 -0
  55. package/dist/templates/nextjs/components/ui/sheet.tsx +140 -0
  56. package/dist/templates/nextjs/components/ui/skeleton.tsx +15 -0
  57. package/dist/templates/nextjs/components/ui/slider.tsx +26 -0
  58. package/dist/templates/nextjs/components/ui/sonner.tsx +29 -0
  59. package/dist/templates/nextjs/components/ui/switch.tsx +29 -0
  60. package/dist/templates/nextjs/components/ui/table.tsx +120 -0
  61. package/dist/templates/nextjs/components/ui/tabs.tsx +53 -0
  62. package/dist/templates/nextjs/components/ui/textarea.tsx +22 -0
  63. package/dist/templates/nextjs/components/ui/toast.tsx +129 -0
  64. package/dist/templates/nextjs/components/ui/toaster.tsx +35 -0
  65. package/dist/templates/nextjs/components/ui/toggle-group.tsx +59 -0
  66. package/dist/templates/nextjs/components/ui/toggle.tsx +43 -0
  67. package/dist/templates/nextjs/components/ui/tooltip.tsx +30 -0
  68. package/dist/templates/nextjs/db/index.ts +8 -0
  69. package/dist/templates/nextjs/db/schema.ts +8 -0
  70. package/dist/templates/nextjs/eslint.config.mjs +14 -16
  71. package/dist/templates/nextjs/hooks/use-toast.ts +95 -0
  72. package/dist/templates/nextjs/instrumentation.ts +14 -0
  73. package/dist/templates/nextjs/lib/boot.ts +56 -0
  74. package/dist/templates/nextjs/lib/utils.ts +6 -0
  75. package/dist/templates/nextjs/next.config.ts +7 -3
  76. package/dist/templates/nextjs/package.json +68 -17
  77. package/dist/templates/nextjs/postcss.config.mjs +2 -2
  78. package/dist/templates/nextjs/scripts/check-env.js +52 -0
  79. package/dist/templates/nextjs/tsconfig.json +5 -10
  80. package/package.json +2 -2
  81. package/dist/chunk-E32T2IIS.js +0 -148
  82. package/dist/templates/nextjs/AGENTS.md +0 -10
  83. package/dist/templates/nextjs/README.md +0 -54
  84. package/dist/templates/nextjs/app/favicon.ico +0 -0
  85. package/dist/templates/nextjs/public/file.svg +0 -1
  86. package/dist/templates/nextjs/public/globe.svg +0 -1
  87. package/dist/templates/nextjs/public/next.svg +0 -1
  88. package/dist/templates/nextjs/public/vercel.svg +0 -1
  89. package/dist/templates/nextjs/public/window.svg +0 -1
@@ -0,0 +1,87 @@
1
+ // src/index.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ var VALID_TEMPLATES = ["vite", "nextjs"];
6
+ async function createSurfApp({
7
+ projectName = ".",
8
+ template: templateArg,
9
+ logger = console.log
10
+ } = {}) {
11
+ const root = path.resolve(projectName);
12
+ const name = path.basename(root);
13
+ const template = validateTemplate(templateArg);
14
+ const templateDir = resolveTemplateDir(template);
15
+ logger(`
16
+ Creating Surf app (${template}) in ${root}
17
+ `);
18
+ fs.mkdirSync(root, { recursive: true });
19
+ copyDir(templateDir, root, root, logger);
20
+ if (template === "nextjs") {
21
+ finalizePackageName(root, name);
22
+ logger(`
23
+ Done! Next steps:
24
+
25
+ cd ${name}
26
+ npm install
27
+ npm run dev
28
+
29
+ Open http://localhost:3000
30
+ `);
31
+ } else {
32
+ logger(`
33
+ Done! Next steps:
34
+
35
+ cd ${name}
36
+ npm install
37
+ npm run dev
38
+
39
+ Open the local URL printed by Vite
40
+ `);
41
+ }
42
+ return root;
43
+ }
44
+ function validateTemplate(template) {
45
+ if (!template) return "vite";
46
+ if (!VALID_TEMPLATES.includes(template)) {
47
+ throw new Error(`Unknown template: ${template}. Valid templates: ${VALID_TEMPLATES.join(", ")}`);
48
+ }
49
+ return template;
50
+ }
51
+ function resolveTemplateDir(template = "vite") {
52
+ const dirName = template === "vite" ? "default" : template;
53
+ const here = path.dirname(fileURLToPath(import.meta.url));
54
+ const candidates = [
55
+ path.join(here, "templates", dirName),
56
+ path.join(here, "..", "templates", dirName)
57
+ ];
58
+ for (const candidate of candidates) {
59
+ if (fs.existsSync(candidate)) return candidate;
60
+ }
61
+ throw new Error(`Could not find ${template} template near ${here}`);
62
+ }
63
+ function copyDir(src, dest, root, logger) {
64
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
65
+ const srcPath = path.join(src, entry.name);
66
+ const destPath = path.join(dest, entry.name);
67
+ if (entry.isDirectory()) {
68
+ fs.mkdirSync(destPath, { recursive: true });
69
+ copyDir(srcPath, destPath, root, logger);
70
+ continue;
71
+ }
72
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
73
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath));
74
+ logger(` ${path.relative(root, destPath)}`);
75
+ }
76
+ }
77
+ function finalizePackageName(root, projectName) {
78
+ const pkgPath = path.join(root, "package.json");
79
+ if (!fs.existsSync(pkgPath)) return;
80
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
81
+ pkg.name = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "surf-app";
82
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
83
+ }
84
+
85
+ export {
86
+ createSurfApp
87
+ };
package/dist/cli.js CHANGED
@@ -1,13 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSurfApp
4
- } from "./chunk-E32T2IIS.js";
4
+ } from "./chunk-FDATV75D.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
8
- "--frontend-port",
9
- "--backend-port",
10
- "--preview-base",
11
8
  "--template"
12
9
  ]);
13
10
  function getFlag(args, name) {
@@ -35,9 +32,6 @@ function parseCliArgs(args) {
35
32
  }
36
33
  return {
37
34
  projectName: positionalArgs[0] || ".",
38
- frontendPort: getFlag(args, "--frontend-port"),
39
- backendPort: getFlag(args, "--backend-port"),
40
- previewBase: getFlag(args, "--preview-base"),
41
35
  template: getFlag(args, "--template")
42
36
  };
43
37
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSurfApp
3
- } from "./chunk-E32T2IIS.js";
3
+ } from "./chunk-FDATV75D.js";
4
4
  export {
5
5
  createSurfApp
6
6
  };
@@ -88,3 +88,23 @@ The agent can also call `POST /api/__sync-schema` explicitly after editing.
88
88
  - Do not bypass your backend routes from the frontend
89
89
  - Frontend packages are pre-installed - check `package.json` before installing
90
90
  - Default to a dark theme unless the user explicitly asks for a different visual direction.
91
+
92
+ ## Design
93
+
94
+ ### Avoid AI-default patterns
95
+ - No colored icon boxes next to metrics (the blue-bg-with-icon KPI pattern)
96
+ - No gradient avatar circles with initials
97
+ - No "Built with React · Tailwind" footers
98
+ - No AI copywriting: "Elevate", "Seamless", "Unleash", "Delve", "Next-Gen"
99
+ - No "Oops!" error messages — be direct ("Failed to load. Try again.")
100
+ - No round placeholder numbers ($100.00) — use realistic data ($847.29)
101
+ - Sentence case on headings, not Title Case On Every Word
102
+ - Icons should aid scanning, not decorate — omit when the label is clear
103
+
104
+ ### ECharts
105
+ - Flat style: show primary axis line, dashed split lines, transparent chart bg
106
+ - Custom tooltip formatter with dash indicators (12×2.5px bars, not default circle dots)
107
+ - Legend: type "plain", icon "roundRect", itemWidth 12, itemHeight 3
108
+ - Prefer timeframe tabs (7D/30D/90D/1Y/All) over dataZoom for time series
109
+ - Default to theme visualizer palette; override when semantics demand it (red/green for gain/loss, sequential scales for heatmaps)
110
+ - Dark mode: parameterize tooltip bg, axis colors, split line colors via resolvedTheme
@@ -0,0 +1,2 @@
1
+ BACKEND_PORT=3001
2
+ SURF_API_KEY=
@@ -2,11 +2,10 @@
2
2
  "name": "backend",
3
3
  "private": true,
4
4
  "scripts": {
5
- "start": "node server.js",
6
- "dev": "node --watch server.js"
5
+ "dev": "node scripts/check-env.js node --watch server.js"
7
6
  },
8
7
  "dependencies": {
9
- "@surf-ai/sdk": "0.1.4-beta",
8
+ "@surf-ai/sdk": "1.0.0-alpha.10",
10
9
  "express": "4.22.1"
11
10
  }
12
11
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Validates required env vars before running a command.
3
+ * Loads .env if it exists (optional convenience), then checks vars.
4
+ *
5
+ * Dev/start: BACKEND_PORT, SURF_API_KEY
6
+ */
7
+ const fs = require('node:fs')
8
+ const path = require('node:path')
9
+ const { execSync } = require('node:child_process')
10
+
11
+ // Load .env if it exists (convenience — env vars can come from anywhere)
12
+ const envPath = path.join(process.cwd(), '.env')
13
+ if (fs.existsSync(envPath)) {
14
+ for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
15
+ const trimmed = line.trim()
16
+ if (!trimmed || trimmed.startsWith('#')) continue
17
+ const eq = trimmed.indexOf('=')
18
+ if (eq < 0) continue
19
+ const key = trimmed.slice(0, eq)
20
+ const val = trimmed.slice(eq + 1)
21
+ if (!process.env[key]) process.env[key] = val
22
+ }
23
+ }
24
+
25
+ const args = process.argv.slice(2)
26
+
27
+ const required = ['BACKEND_PORT', 'SURF_API_KEY']
28
+ const missing = required.filter(k => !process.env[k])
29
+
30
+ if (missing.length > 0) {
31
+ console.error(`\n❌ Missing required env vars: ${missing.join(', ')}`)
32
+ console.error(` Set them in your environment or copy .env.example to .env\n`)
33
+ process.exit(1)
34
+ }
35
+
36
+ try {
37
+ execSync(args.join(' '), { stdio: 'inherit', env: process.env })
38
+ } catch (e) {
39
+ process.exit(e.status || 1)
40
+ }
@@ -1,2 +1,2 @@
1
1
  const { createServer } = require('@surf-ai/sdk/server')
2
- createServer({ proxy: false }).start()
2
+ createServer().start()
@@ -0,0 +1,3 @@
1
+ PORT=5173
2
+ BACKEND_PORT=3001
3
+ BASE_PATH=
@@ -4,8 +4,8 @@
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "vite",
8
- "build": "npm run build:client && npm run build:server",
7
+ "dev": "node scripts/check-env.cjs vite",
8
+ "build": "node scripts/check-env.cjs npm run build:client && npm run build:server",
9
9
  "build:client": "vite build --outDir dist/client",
10
10
  "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
11
11
  "lint": "eslint .",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Validates required env vars before running a command.
3
+ * Loads .env manually, checks vars, then execs the actual command.
4
+ *
5
+ * Build: BASE_PATH (defined, can be empty), BACKEND_PORT
6
+ * Dev: PORT, BACKEND_PORT, BASE_PATH
7
+ */
8
+ const fs = require('node:fs')
9
+ const path = require('node:path')
10
+ const { execSync } = require('node:child_process')
11
+
12
+ // Load .env manually (Vite doesn't load non-VITE_ vars into process.env)
13
+ const envPath = path.join(process.cwd(), '.env')
14
+ if (fs.existsSync(envPath)) {
15
+ for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
16
+ const trimmed = line.trim()
17
+ if (!trimmed || trimmed.startsWith('#')) continue
18
+ const eq = trimmed.indexOf('=')
19
+ if (eq < 0) continue
20
+ const key = trimmed.slice(0, eq)
21
+ const val = trimmed.slice(eq + 1)
22
+ if (!process.env[key]) process.env[key] = val
23
+ }
24
+ }
25
+
26
+ const args = process.argv.slice(2)
27
+ const isBuild = args.some(a => a.includes('build'))
28
+
29
+ // Vars that must be non-empty
30
+ const requiredNonEmpty = isBuild
31
+ ? ['BACKEND_PORT']
32
+ : ['PORT', 'BACKEND_PORT']
33
+
34
+ // Vars that must be defined (empty is ok — BASE_PATH="" means root)
35
+ const requiredDefined = ['BASE_PATH']
36
+
37
+ const missingNonEmpty = requiredNonEmpty.filter(k => !process.env[k])
38
+ const missingDefined = requiredDefined.filter(k => process.env[k] === undefined)
39
+ const missing = [...missingNonEmpty, ...missingDefined]
40
+
41
+ if (missing.length > 0) {
42
+ console.error(`\n❌ Missing required env vars: ${missing.join(', ')}`)
43
+ console.error(` Set them in your environment or copy .env.example to .env\n`)
44
+ process.exit(1)
45
+ }
46
+
47
+ try {
48
+ execSync(args.join(' '), { stdio: 'inherit', env: process.env })
49
+ } catch (e) {
50
+ process.exit(e.status || 1)
51
+ }
@@ -1,10 +1 @@
1
1
  /// <reference types="vite/client" />
2
-
3
- interface ImportMetaEnv {
4
- readonly VITE_PORT: string
5
- readonly VITE_BACKEND_PORT: string
6
- }
7
-
8
- interface ImportMeta {
9
- readonly env: ImportMetaEnv
10
- }
@@ -1,24 +1,12 @@
1
1
  import path from 'path'
2
- import { defineConfig, loadEnv } from 'vite'
2
+ import { defineConfig } from 'vite'
3
3
  import react from '@vitejs/plugin-react'
4
4
  import tailwindcss from '@tailwindcss/vite'
5
5
 
6
- function readRequiredPort(
7
- env: Record<string, string>,
8
- name: 'VITE_BACKEND_PORT' | 'VITE_PORT',
9
- ) {
10
- const value = env[name]
11
- const port = Number.parseInt(value || '', 10)
12
- if (!Number.isInteger(port)) {
13
- throw new Error(`Missing required ${name} in frontend/.env`)
14
- }
15
- return port
16
- }
17
-
18
- export default defineConfig(({ mode }) => {
19
- const env = loadEnv(mode, process.cwd())
20
- const backendPort = readRequiredPort(env, 'VITE_BACKEND_PORT')
21
- const base = env.VITE_BASE || './'
6
+ export default defineConfig(() => {
7
+ const frontendPort = Number.parseInt(process.env.PORT || '', 10)
8
+ const backendPort = Number.parseInt(process.env.BACKEND_PORT || '', 10)
9
+ const base = process.env.BASE_PATH || './'
22
10
  const hasAbsBase = base.startsWith('/')
23
11
  const apiBasePrefix = hasAbsBase ? base.replace(/\/$/, '') : ''
24
12
 
@@ -33,12 +21,11 @@ export default defineConfig(({ mode }) => {
33
21
  return {
34
22
  plugins: [react(), tailwindcss()],
35
23
  server: {
36
- port: readRequiredPort(env, 'VITE_PORT'),
37
24
  host: '0.0.0.0',
25
+ port: frontendPort || undefined,
38
26
  proxy: {
39
27
  [`${apiBasePrefix}/api`]: backendProxy,
40
28
  },
41
- // Keep the HMR socket under the preview base path.
42
29
  hmr: {
43
30
  path: 'ws/vite-hmr',
44
31
  },
@@ -50,8 +37,6 @@ export default defineConfig(({ mode }) => {
50
37
  dedupe: ['react', 'react-dom'],
51
38
  preserveSymlinks: true,
52
39
  },
53
- // Pre-bundle the deps touched during the initial boot path so cold starts
54
- // do not race Vite's lazy dependency optimizer.
55
40
  optimizeDeps: {
56
41
  include: [
57
42
  'react',
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "surf-app",
3
+ "private": true,
4
+ "workspaces": ["backend", "frontend"],
5
+ "scripts": {
6
+ "dev": "concurrently \"npm run dev --workspace backend\" \"npm run dev --workspace frontend\""
7
+ },
8
+ "devDependencies": {
9
+ "concurrently": "^9.0.0"
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ PORT=3000
2
+ BASE_PATH=
3
+ SURF_API_KEY=
@@ -1 +1,93 @@
1
- @AGENTS.md
1
+ # Project
2
+
3
+ Built with [Surf SDK](https://github.com/cyberconnecthq/urania/tree/main/packages/sdk) and Next.js App Router.
4
+
5
+ ## Imports from @surf-ai/sdk
6
+
7
+ Use `@surf-ai/sdk/server` in API route handlers to talk to Surf data APIs.
8
+ Do not import `@surf-ai/sdk/server` in client components.
9
+ Use `fetch('/api/...')` in client components to call your own API routes.
10
+
11
+ **API Route Handler (`@surf-ai/sdk/server`):**
12
+
13
+ ```ts
14
+ import { dataApi } from "@surf-ai/sdk/server";
15
+ const data = await dataApi.market.price({ symbol: "BTC" });
16
+ const holders = await dataApi.token.holders({
17
+ address: "0x...",
18
+ chain: "ethereum",
19
+ });
20
+ // Escape hatch for new endpoints:
21
+ const raw = await dataApi.get("newcategory/endpoint", { foo: "bar" });
22
+ ```
23
+
24
+ ## Structure
25
+
26
+ ```
27
+ app/page.tsx - build your UI here
28
+ app/api/*/route.ts - add API route handlers
29
+ components/ - add components (use "use client" directive)
30
+ db/schema.ts - define database tables
31
+ ```
32
+
33
+ ## Built-in Endpoints
34
+
35
+ These are provided automatically - do NOT create routes for them:
36
+
37
+ | Endpoint | Method | Purpose |
38
+ | -------------------- | ------ | ---------------------------------------------- |
39
+ | `/api/health` | GET | Health check - `{ status: 'ok' }` |
40
+ | `/api/__sync-schema` | POST | Sync `db/schema.ts` tables to database |
41
+ | `/api/cron` | GET | List cron jobs |
42
+ | `/api/cron` | POST | Create a new cron task |
43
+
44
+ ## Creating API Routes
45
+
46
+ Create Route Handler files in `app/api/`:
47
+
48
+ ```ts
49
+ // app/api/prices/route.ts
50
+ import { dataApi } from '@surf-ai/sdk/server'
51
+
52
+ export async function GET(request: Request) {
53
+ const { searchParams } = new URL(request.url)
54
+ const symbol = searchParams.get('symbol') || 'BTC'
55
+ const data = await dataApi.market.price({ symbol })
56
+ return Response.json(data)
57
+ }
58
+ ```
59
+
60
+ ## Database
61
+
62
+ Define tables in `db/schema.ts` using Drizzle ORM:
63
+
64
+ ```ts
65
+ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
66
+ export const users = pgTable("users", {
67
+ id: serial("id").primaryKey(),
68
+ name: text("name").notNull(),
69
+ created_at: timestamp("created_at").defaultNow(),
70
+ });
71
+ ```
72
+
73
+ Tables are auto-synced on server start and when `db/schema.ts` changes in dev mode.
74
+
75
+ ## Do NOT modify
76
+
77
+ - `instrumentation.ts` - server boot (schema sync, cron)
78
+ - `next.config.ts` - build/deploy config
79
+ - `db/index.ts` - database connection
80
+ - `lib/boot.ts` - infrastructure (schema sync, cron init)
81
+ - `app/layout.tsx` - root layout and providers
82
+ - `app/providers.tsx` - client-side providers (QueryClient, theme)
83
+ - `eslint.config.mjs` - lint rules
84
+ - `globals.css` - only imports, do not add styles here (use Tailwind classes)
85
+
86
+ ## Rules
87
+
88
+ - Add `"use client"` at the top of all components you create
89
+ - Use `fetch('/api/...')` in client components to call API routes
90
+ - Use `@surf-ai/sdk/server` `dataApi` in route handlers when you need Surf data
91
+ - Do not bypass your API routes from client components
92
+ - Packages are pre-installed - check `package.json` before installing
93
+ - Default to a dark theme unless the user explicitly asks for a different visual direction
@@ -0,0 +1,18 @@
1
+ import path from 'node:path'
2
+ import { syncSchema } from '@surf-ai/sdk/db'
3
+
4
+ export async function POST() {
5
+ try {
6
+ await syncSchema({
7
+ schemaPath: path.join(process.cwd(), 'db', 'schema.ts'),
8
+ retries: 2,
9
+ retryDelay: 1500,
10
+ })
11
+ return Response.json({ ok: true })
12
+ } catch (err) {
13
+ return Response.json(
14
+ { ok: false, error: String(err) },
15
+ { status: 500 }
16
+ )
17
+ }
18
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { randomUUID } from 'node:crypto'
4
+
5
+ const CRON_PATH = path.join(process.cwd(), 'cron.json')
6
+
7
+ function readCronConfig() {
8
+ if (!fs.existsSync(CRON_PATH)) return []
9
+ return JSON.parse(fs.readFileSync(CRON_PATH, 'utf-8'))
10
+ }
11
+
12
+ function writeCronConfig(config: unknown[]) {
13
+ fs.writeFileSync(CRON_PATH, JSON.stringify(config, null, 2))
14
+ }
15
+
16
+ export async function GET() {
17
+ return Response.json(readCronConfig())
18
+ }
19
+
20
+ export async function POST(request: Request) {
21
+ const body = await request.json()
22
+ const config = readCronConfig()
23
+ const task = {
24
+ id: randomUUID(),
25
+ ...body,
26
+ enabled: body.enabled ?? true,
27
+ }
28
+ config.push(task)
29
+ writeCronConfig(config)
30
+ return Response.json(task, { status: 201 })
31
+ }
@@ -1,3 +1,3 @@
1
1
  export async function GET() {
2
- return Response.json({ status: "ok" });
2
+ return Response.json({ status: 'ok' })
3
3
  }
@@ -1,25 +1,9 @@
1
- import { dataApi } from "@surf-ai/sdk/server";
2
- import { NextResponse } from "next/server";
3
-
4
- const TIME_RANGES = ["1d", "7d", "14d", "30d", "90d", "180d", "365d", "max"] as const;
1
+ import { dataApi } from '@surf-ai/sdk/server'
5
2
 
6
3
  export async function GET(request: Request) {
7
- const { searchParams } = new URL(request.url);
8
- const symbol = searchParams.get("symbol") || "BTC";
9
- const rawTimeRange = searchParams.get("time_range");
10
- const timeRange = TIME_RANGES.includes((rawTimeRange || "") as (typeof TIME_RANGES)[number])
11
- ? (rawTimeRange as (typeof TIME_RANGES)[number])
12
- : "1d";
13
-
14
- try {
15
- const data = await dataApi.market.price({
16
- symbol,
17
- time_range: timeRange,
18
- });
4
+ const { searchParams } = new URL(request.url)
5
+ const symbol = searchParams.get('symbol') || 'BTC'
19
6
 
20
- return NextResponse.json(data);
21
- } catch (error) {
22
- const message = error instanceof Error ? error.message : "Unknown error";
23
- return NextResponse.json({ error: message }, { status: 500 });
24
- }
7
+ const data = await dataApi.market.price({ symbol })
8
+ return Response.json(data)
25
9
  }
@@ -1,26 +1,2 @@
1
1
  @import "tailwindcss";
2
-
3
- :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
6
- }
7
-
8
- @theme inline {
9
- --color-background: var(--background);
10
- --color-foreground: var(--foreground);
11
- --font-sans: var(--font-geist-sans);
12
- --font-mono: var(--font-geist-mono);
13
- }
14
-
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
20
- }
21
-
22
- body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
26
- }
2
+ @import "tw-animate-css";
@@ -1,33 +1,24 @@
1
- import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
3
- import "./globals.css";
4
-
5
- const geistSans = Geist({
6
- variable: "--font-geist-sans",
7
- subsets: ["latin"],
8
- });
9
-
10
- const geistMono = Geist_Mono({
11
- variable: "--font-geist-mono",
12
- subsets: ["latin"],
13
- });
1
+ import type { Metadata } from "next"
2
+ import { ThemeProvider } from "next-themes"
3
+ import { Providers } from "./providers"
4
+ import "./globals.css"
14
5
 
15
6
  export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
18
- };
7
+ title: "Surf App",
8
+ }
19
9
 
20
10
  export default function RootLayout({
21
11
  children,
22
- }: Readonly<{
23
- children: React.ReactNode;
24
- }>) {
12
+ }: {
13
+ children: React.ReactNode
14
+ }) {
25
15
  return (
26
- <html
27
- lang="en"
28
- className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
29
- >
30
- <body className="min-h-full flex flex-col">{children}</body>
16
+ <html lang="en" suppressHydrationWarning>
17
+ <body>
18
+ <ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
19
+ <Providers>{children}</Providers>
20
+ </ThemeProvider>
21
+ </body>
31
22
  </html>
32
- );
23
+ )
33
24
  }