create-surf-app 0.1.4 → 0.1.6
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.
- package/dist/chunk-AMAMASIV.js +1016 -0
- package/dist/cli.js +51 -0
- package/dist/index.js +5 -379
- package/dist/templates/default/CLAUDE.md +46 -0
- package/dist/templates/default/backend/db/index.js +23 -0
- package/dist/templates/default/backend/db/schema.js +20 -0
- package/dist/templates/{backend → default/backend}/eslint.config.mjs +1 -1
- package/dist/templates/default/backend/lib/db.js +67 -0
- package/dist/templates/default/backend/package.json +25 -0
- package/dist/templates/default/backend/routes/proxy.js +66 -0
- package/dist/templates/default/backend/server.js +444 -0
- package/dist/templates/default/frontend/components.json +21 -0
- package/{templates → dist/templates/default}/frontend/eslint.config.js +1 -1
- package/dist/templates/default/frontend/package.json +82 -0
- package/dist/templates/default/frontend/src/App.tsx +23 -0
- package/dist/templates/default/frontend/src/ErrorBoundary.tsx +106 -0
- package/dist/templates/default/frontend/src/components/ui/accordion.tsx +55 -0
- package/dist/templates/default/frontend/src/components/ui/alert.tsx +59 -0
- package/dist/templates/default/frontend/src/components/ui/aspect-ratio.tsx +5 -0
- package/dist/templates/default/frontend/src/components/ui/avatar.tsx +48 -0
- package/dist/templates/default/frontend/src/components/ui/badge.tsx +36 -0
- package/dist/templates/default/frontend/src/components/ui/breadcrumb.tsx +115 -0
- package/dist/templates/default/frontend/src/components/ui/button.tsx +57 -0
- package/dist/templates/default/frontend/src/components/ui/calendar.tsx +211 -0
- package/dist/templates/default/frontend/src/components/ui/card.tsx +76 -0
- package/dist/templates/default/frontend/src/components/ui/carousel.tsx +262 -0
- package/dist/templates/default/frontend/src/components/ui/checkbox.tsx +30 -0
- package/dist/templates/default/frontend/src/components/ui/collapsible.tsx +9 -0
- package/dist/templates/default/frontend/src/components/ui/command.tsx +153 -0
- package/dist/templates/default/frontend/src/components/ui/context-menu.tsx +200 -0
- package/dist/templates/default/frontend/src/components/ui/dialog.tsx +120 -0
- package/dist/templates/default/frontend/src/components/ui/drawer.tsx +118 -0
- package/dist/templates/default/frontend/src/components/ui/dropdown-menu.tsx +201 -0
- package/dist/templates/default/frontend/src/components/ui/form.tsx +176 -0
- package/dist/templates/default/frontend/src/components/ui/hover-card.tsx +29 -0
- package/dist/templates/default/frontend/src/components/ui/input.tsx +22 -0
- package/dist/templates/default/frontend/src/components/ui/label.tsx +26 -0
- package/dist/templates/default/frontend/src/components/ui/menubar.tsx +256 -0
- package/dist/templates/default/frontend/src/components/ui/navigation-menu.tsx +128 -0
- package/dist/templates/default/frontend/src/components/ui/popover.tsx +33 -0
- package/dist/templates/default/frontend/src/components/ui/progress.tsx +26 -0
- package/dist/templates/default/frontend/src/components/ui/radio-group.tsx +42 -0
- package/dist/templates/default/frontend/src/components/ui/resizable.tsx +43 -0
- package/dist/templates/default/frontend/src/components/ui/scroll-area.tsx +46 -0
- package/dist/templates/default/frontend/src/components/ui/select.tsx +157 -0
- package/dist/templates/default/frontend/src/components/ui/separator.tsx +31 -0
- package/dist/templates/default/frontend/src/components/ui/sheet.tsx +140 -0
- package/dist/templates/default/frontend/src/components/ui/skeleton.tsx +15 -0
- package/dist/templates/default/frontend/src/components/ui/slider.tsx +26 -0
- package/dist/templates/default/frontend/src/components/ui/sonner.tsx +29 -0
- package/dist/templates/default/frontend/src/components/ui/switch.tsx +29 -0
- package/dist/templates/default/frontend/src/components/ui/table.tsx +120 -0
- package/dist/templates/default/frontend/src/components/ui/tabs.tsx +53 -0
- package/dist/templates/default/frontend/src/components/ui/textarea.tsx +22 -0
- package/dist/templates/default/frontend/src/components/ui/toast.tsx +129 -0
- package/dist/templates/default/frontend/src/components/ui/toaster.tsx +35 -0
- package/dist/templates/default/frontend/src/components/ui/toggle-group.tsx +59 -0
- package/dist/templates/default/frontend/src/components/ui/toggle.tsx +43 -0
- package/dist/templates/default/frontend/src/components/ui/tooltip.tsx +30 -0
- package/dist/templates/default/frontend/src/db/schema.ts +16 -0
- package/{templates → dist/templates/default}/frontend/src/entry-client.tsx +11 -8
- package/dist/templates/default/frontend/src/hooks/use-toast.ts +95 -0
- package/dist/templates/default/frontend/src/index.css +314 -0
- package/dist/templates/default/frontend/src/lib/api.ts +31 -0
- package/dist/templates/default/frontend/src/lib/fetch.ts +38 -0
- package/dist/templates/default/frontend/src/lib/utils.ts +6 -0
- package/dist/templates/default/frontend/src/vite-env.d.ts +11 -0
- package/dist/templates/default/frontend/tsconfig.json +22 -0
- package/dist/templates/default/frontend/vite.config.ts +162 -0
- package/package.json +7 -7
- package/dist/templates/frontend/eslint.config.js +0 -42
- package/dist/templates/frontend/src/entry-client.tsx +0 -109
- package/templates/backend/eslint.config.mjs +0 -21
- package/templates/frontend/index.html +0 -43
- package/templates/frontend/src/entry-server.tsx +0 -13
- /package/dist/templates/{frontend → default/frontend}/index.html +0 -0
- /package/dist/templates/{frontend → default/frontend}/src/entry-server.tsx +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createSurfApp
|
|
4
|
+
} from "./chunk-AMAMASIV.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
8
|
+
"--frontend-port",
|
|
9
|
+
"--backend-port",
|
|
10
|
+
"--preview-base",
|
|
11
|
+
"--swagger-url"
|
|
12
|
+
]);
|
|
13
|
+
function getFlag(args, name) {
|
|
14
|
+
const idx = args.indexOf(name);
|
|
15
|
+
return idx >= 0 && args[idx + 1] ? args[idx + 1] : void 0;
|
|
16
|
+
}
|
|
17
|
+
function parseCliArgs(args) {
|
|
18
|
+
const positionalArgs = [];
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (!arg.startsWith("--")) {
|
|
22
|
+
positionalArgs.push(arg);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!VALUE_FLAGS.has(arg)) {
|
|
26
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
27
|
+
}
|
|
28
|
+
if (!args[index + 1] || args[index + 1].startsWith("--")) {
|
|
29
|
+
throw new Error(`Missing value for flag: ${arg}`);
|
|
30
|
+
}
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
if (positionalArgs.length > 1) {
|
|
34
|
+
throw new Error(`Expected at most one project directory, got: ${positionalArgs.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
projectName: positionalArgs[0] || ".",
|
|
38
|
+
frontendPort: getFlag(args, "--frontend-port"),
|
|
39
|
+
backendPort: getFlag(args, "--backend-port"),
|
|
40
|
+
previewBase: getFlag(args, "--preview-base"),
|
|
41
|
+
swaggerUrl: getFlag(args, "--swagger-url")
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function runCli() {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
await createSurfApp(parseCliArgs(args));
|
|
47
|
+
}
|
|
48
|
+
runCli().catch((error) => {
|
|
49
|
+
console.error(error);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,380 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var args = process.argv.slice(2);
|
|
7
|
-
var projectName = args.find((a) => !a.startsWith("--")) || ".";
|
|
8
|
-
var frontendPort = getFlag("--port") || process.env.VITE_PORT || "5173";
|
|
9
|
-
var backendPort = getFlag("--backend-port") || process.env.VITE_BACKEND_PORT || "3001";
|
|
10
|
-
function getFlag(name2) {
|
|
11
|
-
const idx = args.indexOf(name2);
|
|
12
|
-
return idx >= 0 && args[idx + 1] ? args[idx + 1] : void 0;
|
|
13
|
-
}
|
|
14
|
-
var frontendPkg = {
|
|
15
|
-
name: "frontend",
|
|
16
|
-
private: true,
|
|
17
|
-
type: "module",
|
|
18
|
-
scripts: {
|
|
19
|
-
dev: "vite",
|
|
20
|
-
build: "vite build",
|
|
21
|
-
preview: "vite preview",
|
|
22
|
-
lint: "eslint ."
|
|
23
|
-
},
|
|
24
|
-
dependencies: {
|
|
25
|
-
"@surf-ai/sdk": "latest",
|
|
26
|
-
"@surf-ai/theme": "latest",
|
|
27
|
-
"@radix-ui/react-accordion": "1.2.12",
|
|
28
|
-
"@radix-ui/react-aspect-ratio": "1.1.8",
|
|
29
|
-
"@radix-ui/react-avatar": "1.1.11",
|
|
30
|
-
"@radix-ui/react-checkbox": "1.3.3",
|
|
31
|
-
"@radix-ui/react-collapsible": "1.1.12",
|
|
32
|
-
"@radix-ui/react-context-menu": "2.2.16",
|
|
33
|
-
"@radix-ui/react-dialog": "1.1.15",
|
|
34
|
-
"@radix-ui/react-dropdown-menu": "2.1.16",
|
|
35
|
-
"@radix-ui/react-hover-card": "1.1.15",
|
|
36
|
-
"@radix-ui/react-label": "2.1.8",
|
|
37
|
-
"@radix-ui/react-menubar": "1.1.16",
|
|
38
|
-
"@radix-ui/react-navigation-menu": "1.2.14",
|
|
39
|
-
"@radix-ui/react-popover": "1.1.15",
|
|
40
|
-
"@radix-ui/react-progress": "1.1.8",
|
|
41
|
-
"@radix-ui/react-radio-group": "1.3.8",
|
|
42
|
-
"@radix-ui/react-scroll-area": "1.2.10",
|
|
43
|
-
"@radix-ui/react-select": "2.2.6",
|
|
44
|
-
"@radix-ui/react-separator": "1.1.8",
|
|
45
|
-
"@radix-ui/react-slider": "1.3.6",
|
|
46
|
-
"@radix-ui/react-slot": "1.2.4",
|
|
47
|
-
"@radix-ui/react-switch": "1.2.6",
|
|
48
|
-
"@radix-ui/react-tabs": "1.1.13",
|
|
49
|
-
"@radix-ui/react-toast": "1.2.15",
|
|
50
|
-
"@radix-ui/react-toggle": "1.1.10",
|
|
51
|
-
"@radix-ui/react-toggle-group": "1.1.11",
|
|
52
|
-
"@radix-ui/react-tooltip": "1.2.8",
|
|
53
|
-
"@tanstack/react-query": "5.94.5",
|
|
54
|
-
"class-variance-authority": "0.7.1",
|
|
55
|
-
"clsx": "2.1.1",
|
|
56
|
-
"cmdk": "1.1.1",
|
|
57
|
-
"echarts": "5.6.0",
|
|
58
|
-
"echarts-for-react": "3.0.6",
|
|
59
|
-
"embla-carousel-react": "8.6.0",
|
|
60
|
-
"lucide-react": "0.454.0",
|
|
61
|
-
"next-themes": "0.4.6",
|
|
62
|
-
"react": "19.2.4",
|
|
63
|
-
"react-dom": "19.2.4",
|
|
64
|
-
"react-router-dom": "7.6.1",
|
|
65
|
-
"sonner": "1.7.4",
|
|
66
|
-
"tailwind-merge": "2.6.1",
|
|
67
|
-
"vaul": "1.1.2",
|
|
68
|
-
"zod": "3.25.76"
|
|
69
|
-
},
|
|
70
|
-
devDependencies: {
|
|
71
|
-
"@eslint/js": "9.39.4",
|
|
72
|
-
"@tailwindcss/vite": "4.2.2",
|
|
73
|
-
"@types/node": "22.19.15",
|
|
74
|
-
"@types/react": "19.2.14",
|
|
75
|
-
"@types/react-dom": "19.2.3",
|
|
76
|
-
"@vitejs/plugin-react": "4.7.0",
|
|
77
|
-
"eslint": "9.39.4",
|
|
78
|
-
"eslint-plugin-react-hooks": "5.2.0",
|
|
79
|
-
"eslint-plugin-react-refresh": "0.4.26",
|
|
80
|
-
"globals": "16.5.0",
|
|
81
|
-
"tailwindcss": "4.2.2",
|
|
82
|
-
"tw-animate-css": "1.4.0",
|
|
83
|
-
"typescript": "5.9.3",
|
|
84
|
-
"typescript-eslint": "8.57.1",
|
|
85
|
-
"vite": "6.4.1"
|
|
86
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createSurfApp
|
|
3
|
+
} from "./chunk-AMAMASIV.js";
|
|
4
|
+
export {
|
|
5
|
+
createSurfApp
|
|
87
6
|
};
|
|
88
|
-
var templates = {
|
|
89
|
-
// ── Backend ──────────────────────────────────────────────────────────────
|
|
90
|
-
"backend/package.json": JSON.stringify({
|
|
91
|
-
name: "backend",
|
|
92
|
-
private: true,
|
|
93
|
-
scripts: {
|
|
94
|
-
start: "node server.js",
|
|
95
|
-
dev: "node --env-file=.env --watch server.js"
|
|
96
|
-
},
|
|
97
|
-
dependencies: {
|
|
98
|
-
"@surf-ai/sdk": "latest",
|
|
99
|
-
"express": "^4.22.0"
|
|
100
|
-
}
|
|
101
|
-
}, null, 2),
|
|
102
|
-
"backend/server.js": `const { createServer } = require('@surf-ai/sdk/server')
|
|
103
|
-
createServer().start()
|
|
104
|
-
`,
|
|
105
|
-
"backend/routes/.gitkeep": "",
|
|
106
|
-
"backend/db/schema.js": `// Define your Drizzle ORM tables here.
|
|
107
|
-
// Example:
|
|
108
|
-
// const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
|
|
109
|
-
// exports.users = pgTable('users', {
|
|
110
|
-
// id: serial('id').primaryKey(),
|
|
111
|
-
// name: text('name').notNull(),
|
|
112
|
-
// created_at: timestamp('created_at').defaultNow(),
|
|
113
|
-
// })
|
|
114
|
-
`,
|
|
115
|
-
"backend/.env": `PORT=${backendPort}
|
|
116
|
-
`,
|
|
117
|
-
// ── Frontend ─────────────────────────────────────────────────────────────
|
|
118
|
-
"frontend/package.json": JSON.stringify(frontendPkg, null, 2),
|
|
119
|
-
"frontend/.env": `VITE_PORT=${frontendPort}
|
|
120
|
-
VITE_BACKEND_PORT=${backendPort}
|
|
121
|
-
VITE_BASE=${process.env.VITE_BASE || "/"}
|
|
122
|
-
`,
|
|
123
|
-
"frontend/vite.config.ts": `import { defineConfig, loadEnv } from 'vite'
|
|
124
|
-
import react from '@vitejs/plugin-react'
|
|
125
|
-
import tailwindcss from '@tailwindcss/vite'
|
|
126
|
-
import path from 'path'
|
|
127
|
-
|
|
128
|
-
const env = loadEnv('', process.cwd(), '')
|
|
129
|
-
|
|
130
|
-
function requiredPort(name: string) {
|
|
131
|
-
const value = env[name]
|
|
132
|
-
if (!value) {
|
|
133
|
-
throw new Error(\`Missing \${name}. Set it in frontend/.env or your shell environment.\`)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const port = Number.parseInt(value, 10)
|
|
137
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
138
|
-
throw new Error(\`Invalid \${name}: \${value}\`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return port
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const FRONTEND_PORT = requiredPort('VITE_PORT')
|
|
145
|
-
const BACKEND_PORT = requiredPort('VITE_BACKEND_PORT')
|
|
146
|
-
const BASE = env.VITE_BASE || '/'
|
|
147
|
-
|
|
148
|
-
export default defineConfig({
|
|
149
|
-
base: BASE,
|
|
150
|
-
plugins: [react(), tailwindcss()],
|
|
151
|
-
server: {
|
|
152
|
-
port: FRONTEND_PORT,
|
|
153
|
-
host: '0.0.0.0',
|
|
154
|
-
proxy: {
|
|
155
|
-
'/proxy': { target: \`http://127.0.0.1:\${BACKEND_PORT}\`, changeOrigin: true },
|
|
156
|
-
'/api': { target: \`http://127.0.0.1:\${BACKEND_PORT}\`, changeOrigin: true },
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
resolve: {
|
|
160
|
-
alias: {
|
|
161
|
-
'@': path.resolve(__dirname, './src'),
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
})
|
|
165
|
-
`,
|
|
166
|
-
// index.html comes from templates/ directory (has cold-start guard script)
|
|
167
|
-
"frontend/tsconfig.json": JSON.stringify({
|
|
168
|
-
compilerOptions: {
|
|
169
|
-
target: "ES2020",
|
|
170
|
-
module: "ESNext",
|
|
171
|
-
moduleResolution: "bundler",
|
|
172
|
-
jsx: "react-jsx",
|
|
173
|
-
strict: true,
|
|
174
|
-
esModuleInterop: true,
|
|
175
|
-
skipLibCheck: true,
|
|
176
|
-
paths: { "@/*": ["./src/*"] }
|
|
177
|
-
},
|
|
178
|
-
include: ["src"]
|
|
179
|
-
}, null, 2),
|
|
180
|
-
"frontend/components.json": JSON.stringify({
|
|
181
|
-
"$schema": "https://ui.shadcn.com/schema.json",
|
|
182
|
-
"style": "default",
|
|
183
|
-
"rsc": false,
|
|
184
|
-
"tsx": true,
|
|
185
|
-
"tailwind": {
|
|
186
|
-
"config": "",
|
|
187
|
-
"css": "src/index.css",
|
|
188
|
-
"baseColor": "slate",
|
|
189
|
-
"cssVariables": true
|
|
190
|
-
},
|
|
191
|
-
"aliases": {
|
|
192
|
-
"components": "@/components",
|
|
193
|
-
"utils": "@surf-ai/sdk/react",
|
|
194
|
-
"hooks": "@/hooks"
|
|
195
|
-
}
|
|
196
|
-
}, null, 2),
|
|
197
|
-
// entry-client.tsx replaces main.tsx — comes from templates/ directory
|
|
198
|
-
"frontend/src/App.tsx": `import { useMarketPrice } from '@surf-ai/sdk/react'
|
|
199
|
-
|
|
200
|
-
export default function App() {
|
|
201
|
-
const { data, isLoading, error } = useMarketPrice({ symbol: 'BTC', time_range: '1d' })
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<div className="min-h-screen bg-background text-foreground p-10">
|
|
205
|
-
<h1 className="text-3xl font-bold mb-4">Surf App</h1>
|
|
206
|
-
{isLoading && <p className="text-muted-foreground">Loading BTC price...</p>}
|
|
207
|
-
{error && <p className="text-destructive">Error: {(error as Error).message}</p>}
|
|
208
|
-
{data?.data?.[0] && (
|
|
209
|
-
<p className="text-4xl font-bold">
|
|
210
|
-
BTC: <span className="text-primary">\${data.data[0].value?.toLocaleString()}</span>
|
|
211
|
-
</p>
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
`,
|
|
217
|
-
"frontend/src/index.css": `@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;600;700;900&family=Roboto+Mono:wght@400;500&display=swap');
|
|
218
|
-
@import "tailwindcss";
|
|
219
|
-
@import "tw-animate-css";
|
|
220
|
-
@import "@surf-ai/theme";
|
|
221
|
-
`,
|
|
222
|
-
// cn() and useToast() are now in @surf-ai/sdk/react
|
|
223
|
-
"frontend/src/db/schema.ts": `// Database schema definition \u2014 keep in sync with backend/db/schema.js.
|
|
224
|
-
// This file mirrors the backend schema for TypeScript type safety in the frontend.
|
|
225
|
-
//
|
|
226
|
-
// Example:
|
|
227
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
|
|
228
|
-
// export const users = pgTable('users', {
|
|
229
|
-
// id: serial('id').primaryKey(),
|
|
230
|
-
// name: text('name').notNull(),
|
|
231
|
-
// created_at: timestamp('created_at').defaultNow(),
|
|
232
|
-
// })
|
|
233
|
-
`,
|
|
234
|
-
"frontend/src/vite-env.d.ts": `/// <reference types="vite/client" />
|
|
235
|
-
`,
|
|
236
|
-
// ── Root ──────────────────────────────────────────────────────────────────
|
|
237
|
-
"CLAUDE.md": `# Project
|
|
238
|
-
|
|
239
|
-
Built with [Surf SDK](https://github.com/cyberconnecthq/urania/tree/main/packages/sdk).
|
|
240
|
-
|
|
241
|
-
## Imports from @surf-ai/sdk
|
|
242
|
-
|
|
243
|
-
Everything comes from \`@surf-ai/sdk\`. Do NOT create local utility files for these.
|
|
244
|
-
|
|
245
|
-
**Frontend (\`@surf-ai/sdk/react\`):**
|
|
246
|
-
\`\`\`tsx
|
|
247
|
-
import { useMarketPrice, useTokenHolders } from '@surf-ai/sdk/react' // data hooks
|
|
248
|
-
import { cn } from '@surf-ai/sdk/react' // Tailwind class merge
|
|
249
|
-
import { useToast, toast } from '@surf-ai/sdk/react' // toast notifications
|
|
250
|
-
\`\`\`
|
|
251
|
-
|
|
252
|
-
**Backend (\`@surf-ai/sdk/server\`):**
|
|
253
|
-
\`\`\`js
|
|
254
|
-
const { dataApi } = require('@surf-ai/sdk/server')
|
|
255
|
-
const data = await dataApi.market.price({ symbol: 'BTC' })
|
|
256
|
-
const holders = await dataApi.token.holders({ address: '0x...', chain: 'ethereum' })
|
|
257
|
-
// Escape hatch for new endpoints:
|
|
258
|
-
const raw = await dataApi.get('newcategory/endpoint', { foo: 'bar' })
|
|
259
|
-
\`\`\`
|
|
260
|
-
|
|
261
|
-
## Structure
|
|
262
|
-
|
|
263
|
-
\`\`\`
|
|
264
|
-
frontend/src/App.tsx - build your UI here
|
|
265
|
-
frontend/src/components/ - add components
|
|
266
|
-
frontend/src/db/schema.ts - frontend DB schema mirror
|
|
267
|
-
backend/routes/*.js - add API routes (auto-mounted at /api/{name})
|
|
268
|
-
backend/db/schema.js - define database tables
|
|
269
|
-
\`\`\`
|
|
270
|
-
|
|
271
|
-
## Built-in Endpoints (from @surf-ai/sdk/server)
|
|
272
|
-
|
|
273
|
-
\`createServer()\` provides these automatically \u2014 do NOT create routes for them:
|
|
274
|
-
|
|
275
|
-
| Endpoint | Method | Purpose |
|
|
276
|
-
|----------|--------|---------|
|
|
277
|
-
| \`/api/health\` | GET | Health check \u2014 \`{ status: 'ok' }\` |
|
|
278
|
-
| \`/api/__sync-schema\` | POST | Sync \`backend/db/schema.js\` tables to database |
|
|
279
|
-
| \`/api/cron\` | GET | List cron jobs with status and next run time |
|
|
280
|
-
| \`/api/cron\` | POST | Create a new cron task |
|
|
281
|
-
| \`/api/cron/:id\` | PATCH | Update a cron task (schedule, enabled, etc.) |
|
|
282
|
-
| \`/api/cron/:id\` | DELETE | Delete a cron task |
|
|
283
|
-
| \`/api/cron/:id/run\` | POST | Manually trigger a cron task |
|
|
284
|
-
| \`/proxy/*\` | ANY | Data API passthrough \u2014 \`/proxy/market/price\` \u2192 hermod |
|
|
285
|
-
|
|
286
|
-
Auto-registered from \`backend/routes/*.js\`:
|
|
287
|
-
| File | Endpoint |
|
|
288
|
-
|------|----------|
|
|
289
|
-
| \`routes/btc.js\` | \`/api/btc\` |
|
|
290
|
-
| \`routes/portfolio.js\` | \`/api/portfolio\` |
|
|
291
|
-
|
|
292
|
-
## Database
|
|
293
|
-
|
|
294
|
-
Define tables in \`backend/db/schema.js\` using Drizzle ORM:
|
|
295
|
-
\`\`\`js
|
|
296
|
-
const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
|
|
297
|
-
exports.users = pgTable('users', {
|
|
298
|
-
id: serial('id').primaryKey(),
|
|
299
|
-
name: text('name').notNull(),
|
|
300
|
-
created_at: timestamp('created_at').defaultNow(),
|
|
301
|
-
})
|
|
302
|
-
\`\`\`
|
|
303
|
-
|
|
304
|
-
Tables are auto-created on startup and when \`schema.js\` changes (file watcher).
|
|
305
|
-
The agent can also call \`POST /api/__sync-schema\` explicitly after editing.
|
|
306
|
-
|
|
307
|
-
## Dev Servers
|
|
308
|
-
|
|
309
|
-
- Frontend: \`cd frontend && npm run dev\` (port from \`.env\`, do NOT pass \`--port\`)
|
|
310
|
-
- Backend: \`cd backend && npm run dev\` (port from \`.env\`)
|
|
311
|
-
- After \`npm install\` new packages, do NOT restart servers \u2014 Vite auto-discovers deps, backend uses \`node --watch\`
|
|
312
|
-
- If a server crashes, restart with \`npm run dev\` in that directory
|
|
313
|
-
- NEVER use \`npx vite\` \u2014 always use \`npm run dev\`
|
|
314
|
-
|
|
315
|
-
## Do NOT modify
|
|
316
|
-
|
|
317
|
-
- \`vite.config.ts\` \u2014 proxy and build config
|
|
318
|
-
- \`frontend/.env\` \u2014 port configuration (auto-generated)
|
|
319
|
-
- \`backend/server.js\` \u2014 uses @surf-ai/sdk/server
|
|
320
|
-
- \`entry-client.tsx\` \u2014 app bootstrap with SSR hydration
|
|
321
|
-
- \`entry-server.tsx\` \u2014 SSR render for deploy
|
|
322
|
-
- \`index.html\` \u2014 cold-start guard and Surf badge
|
|
323
|
-
- \`eslint.config.*\` \u2014 lint rules
|
|
324
|
-
- \`index.css\` \u2014 only imports, do not add styles here (use Tailwind classes)
|
|
325
|
-
|
|
326
|
-
## Rules
|
|
327
|
-
|
|
328
|
-
- Use \`@surf-ai/sdk/react\` hooks in frontend, \`@surf-ai/sdk/server\` dataApi in backend
|
|
329
|
-
- Use Tailwind CSS classes for styling (Surf Design System theme via \`@surf-ai/theme\`)
|
|
330
|
-
- Use shadcn/ui components \u2014 install with \`npx shadcn@latest add button\`
|
|
331
|
-
- Use \`cn()\` from \`@surf-ai/sdk/react\` to merge Tailwind classes
|
|
332
|
-
- Frontend packages are pre-installed \u2014 check \`package.json\` before installing
|
|
333
|
-
- Dark theme is the default (configured in entry-client.tsx)
|
|
334
|
-
`
|
|
335
|
-
};
|
|
336
|
-
var root = path.resolve(projectName);
|
|
337
|
-
var name = path.basename(root);
|
|
338
|
-
console.log(`
|
|
339
|
-
Creating Surf app in ./${projectName === "." ? "" : name}
|
|
340
|
-
`);
|
|
341
|
-
fs.mkdirSync(root, { recursive: true });
|
|
342
|
-
for (const [relPath, content] of Object.entries(templates)) {
|
|
343
|
-
const fullPath = path.join(root, relPath);
|
|
344
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
345
|
-
fs.writeFileSync(fullPath, content);
|
|
346
|
-
console.log(` ${relPath}`);
|
|
347
|
-
}
|
|
348
|
-
var templatesDir = path.join(new URL(".", import.meta.url).pathname, "templates");
|
|
349
|
-
if (fs.existsSync(templatesDir)) {
|
|
350
|
-
copyDir(templatesDir, root);
|
|
351
|
-
}
|
|
352
|
-
function copyDir(src, dest) {
|
|
353
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
354
|
-
const srcPath = path.join(src, entry.name);
|
|
355
|
-
const destPath = path.join(dest, entry.name);
|
|
356
|
-
if (entry.isDirectory()) {
|
|
357
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
358
|
-
copyDir(srcPath, destPath);
|
|
359
|
-
} else {
|
|
360
|
-
fs.writeFileSync(destPath, fs.readFileSync(srcPath));
|
|
361
|
-
console.log(` ${path.relative(root, destPath)}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
var cdStep = projectName === "." ? "" : ` cd ${name}
|
|
366
|
-
`;
|
|
367
|
-
console.log(`
|
|
368
|
-
Done! Next steps:
|
|
369
|
-
|
|
370
|
-
${cdStep} cd backend && npm install && cd ..
|
|
371
|
-
cd frontend && npm install && cd ..
|
|
372
|
-
|
|
373
|
-
# Start backend
|
|
374
|
-
cd backend && npm run dev &
|
|
375
|
-
|
|
376
|
-
# Start frontend
|
|
377
|
-
cd frontend && npm run dev
|
|
378
|
-
|
|
379
|
-
Open http://localhost:${frontendPort}
|
|
380
|
-
`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Project Rules
|
|
2
|
+
|
|
3
|
+
## Do NOT Touch
|
|
4
|
+
|
|
5
|
+
- `vite.config.ts` — pre-configured proxy chain; changing it breaks preview.
|
|
6
|
+
- `.env` files — managed by the system; editing causes Vite restart & preview downtime.
|
|
7
|
+
- `entry-client.tsx` `notifyParentReady` / `data-surf-placeholder` — hosting app depends on these.
|
|
8
|
+
- Dev servers — NEVER start/stop/kill/restart them manually (`pkill`, `kill`, `node`, `npm run dev`). Causes EADDRINUSE.
|
|
9
|
+
|
|
10
|
+
## npm install Rules
|
|
11
|
+
|
|
12
|
+
- **ALL packages in `frontend/package.json` are already installed.** Just import them directly — do NOT run `npm install` for them.
|
|
13
|
+
- Running `npm install` on pre-installed packages corrupts the node_modules tmpfs mount, kills the Vite dev server, and causes 5+ minute preview downtime (white screen).
|
|
14
|
+
- Only `npm install` packages that are NOT in package.json. When in doubt, read `frontend/package.json` first.
|
|
15
|
+
|
|
16
|
+
## Editing Rules
|
|
17
|
+
|
|
18
|
+
- **Read before edit** — always Read a file before using Edit or Write on it.
|
|
19
|
+
- **Edit, don't rewrite** — use the Edit tool on scaffold files (marked SCAFFOLD). Never use Write to replace them entirely; it breaks infrastructure (proxy, HMR, cold-start guards).
|
|
20
|
+
- **One file per tool call** — enables streaming progress and faster error recovery.
|
|
21
|
+
- **200-line limit** — split components exceeding 200 lines into sub-components.
|
|
22
|
+
|
|
23
|
+
## Storage Rules
|
|
24
|
+
|
|
25
|
+
- **Never use localStorage or in-memory arrays for persistent user data** (CRUD, todos, plans, bookmarks, notes, watchlists, etc.). Read `.claude/skills/database/SKILL.md` and use PostgreSQL + Drizzle ORM.
|
|
26
|
+
- localStorage is ONLY acceptable for: theme preference, UI collapsed/expanded state, session tokens.
|
|
27
|
+
|
|
28
|
+
## Data Table Rules
|
|
29
|
+
|
|
30
|
+
- **List tables need sorting + pagination + search.** Tables displaying browsable collections (token lists, tx history, leaderboards — unbounded row count) MUST use the SortableTable pattern from `component-reference/references/data-display.md`. Plain `<Table>` is fine for summary/comparison tables or small bounded-size API results (top-5, recent-3).
|
|
31
|
+
|
|
32
|
+
## First-Edit Checklist (do on every new project)
|
|
33
|
+
|
|
34
|
+
- **Page title** — ALWAYS update `<title>App</title>` in `frontend/index.html` to a meaningful title matching the website purpose. Never ship with `<title>App</title>`.
|
|
35
|
+
- **Favicon** — `frontend/public/favicon.ico` is already provided; do NOT delete or overwrite it.
|
|
36
|
+
|
|
37
|
+
## Common Mistakes to Avoid
|
|
38
|
+
|
|
39
|
+
- **API URLs** — NEVER use bare `/proxy/...` or `/api/...`. Always use `${API_BASE}/proxy/...`, `${API_BASE}/api/...`, or the generated hooks in `api.ts`.
|
|
40
|
+
- **No React.lazy / dynamic import()** — HMR is unavailable in preview; causes "Invalid hook call" crashes.
|
|
41
|
+
- **Hooks at top level** — ALL React hooks MUST be called before any conditional `return`. Use `useQuery({ enabled: !!condition })` for conditional fetching.
|
|
42
|
+
- **SSR safety** — NEVER access `document`, `window`, `localStorage` at module top level or during render. Use `typeof window !== 'undefined'` or `useEffect`.
|
|
43
|
+
- **API response safety** — NEVER render API fields without type-checking. Objects as React children cause white-screen crashes: `typeof x === 'string' ? x : x?.name ?? ''`.
|
|
44
|
+
- **ThemeProvider** — MUST set `storageKey="surf-studio-theme"` to avoid conflicts with the parent Surf app iframe.
|
|
45
|
+
- **No mock data** — never hardcode data. Use real API calls; show proper error/empty states.
|
|
46
|
+
- **ASCII only** — no curly quotes, en-dash, em-dash in code. Non-ASCII punctuation causes build failures.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM client (pg-proxy driver).
|
|
3
|
+
*
|
|
4
|
+
* Routes all SQL through hermod's DB proxy — works identically in
|
|
5
|
+
* sandbox (OutboundProxy) and deployed mode (hermod gateway).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { db } = require('./db');
|
|
9
|
+
* const { todos } = require('./db/schema');
|
|
10
|
+
* const rows = await db.select().from(todos);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { drizzle } = require('drizzle-orm/pg-proxy');
|
|
14
|
+
const { dbQuery } = require('../lib/db');
|
|
15
|
+
|
|
16
|
+
const db = drizzle(async (sql, params, method) => {
|
|
17
|
+
const result = await dbQuery(sql, params, { arrayMode: true });
|
|
18
|
+
// pg-proxy expects rows as positional arrays — arrayMode makes hermod
|
|
19
|
+
// return [[val1, val2, ...], ...] instead of [{col: val}, ...].
|
|
20
|
+
return { rows: result.rows || [] };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
module.exports = { db };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM schema definitions.
|
|
3
|
+
*
|
|
4
|
+
* Define your tables here using Drizzle's pgTable builder.
|
|
5
|
+
* This file is the single source of truth for your database schema.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* const { pgTable, serial, text, boolean, timestamp } = require('drizzle-orm/pg-core');
|
|
9
|
+
*
|
|
10
|
+
* const todos = pgTable('todos', {
|
|
11
|
+
* id: serial('id').primaryKey(),
|
|
12
|
+
* title: text('title').notNull(),
|
|
13
|
+
* completed: boolean('completed').default(false),
|
|
14
|
+
* createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* module.exports = { todos };
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Add your table definitions here.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side database helpers.
|
|
3
|
+
*
|
|
4
|
+
* All SQL execution happens here in the backend — NEVER in frontend code.
|
|
5
|
+
* Calls /proxy/db/* via loopback through the Express proxy middleware,
|
|
6
|
+
* which handles both sandbox mode (OutboundProxy) and deployed mode (hermod).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PORT = Number.parseInt(process.env.PORT || '', 10);
|
|
10
|
+
if (!Number.isInteger(PORT)) {
|
|
11
|
+
throw new Error('PORT env var is required');
|
|
12
|
+
}
|
|
13
|
+
const BASE = `http://127.0.0.1:${PORT}/proxy/db`;
|
|
14
|
+
|
|
15
|
+
async function request(path, options = {}) {
|
|
16
|
+
const res = await fetch(`${BASE}${path}`, options);
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const body = await res.json().catch(() => ({ message: res.statusText }));
|
|
19
|
+
const err = new Error(body.detail || body.error || body.message || `DB request failed: ${res.status}`);
|
|
20
|
+
err.status = res.status;
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Provision a database for the current user (idempotent). */
|
|
27
|
+
async function dbProvision(reason) {
|
|
28
|
+
return request('/provision', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ reason: reason || '' }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execute a parameterized SQL query.
|
|
37
|
+
* ALWAYS use $1, $2, ... placeholders — NEVER concatenate user input.
|
|
38
|
+
* @param {string} sql
|
|
39
|
+
* @param {any[]} params
|
|
40
|
+
* @param {{ arrayMode?: boolean }} [options] - Pass { arrayMode: true } for Drizzle pg-proxy
|
|
41
|
+
*/
|
|
42
|
+
async function dbQuery(sql, params = [], options = {}) {
|
|
43
|
+
return request('/query', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ sql, params, ...options }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** List all tables with approximate row counts. */
|
|
51
|
+
async function dbTables() {
|
|
52
|
+
const data = await request('/tables');
|
|
53
|
+
return data.tables;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Get column definitions for a table. */
|
|
57
|
+
async function dbTableSchema(name) {
|
|
58
|
+
const data = await request(`/tables/${encodeURIComponent(name)}/schema`);
|
|
59
|
+
return data.columns;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Check database connection status and usage. */
|
|
63
|
+
async function dbStatus() {
|
|
64
|
+
return request('/status');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { dbProvision, dbQuery, dbTables, dbTableSchema, dbStatus };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"check": "node --check server.js && find routes lib db -type f -name '*.js' -print0 | xargs -0 -n1 node --check",
|
|
7
|
+
"lint": "eslint .",
|
|
8
|
+
"start": "node server.js",
|
|
9
|
+
"dev": "node --watch --watch-path=./routes --watch-path=./lib --watch-path=./server.js server.js",
|
|
10
|
+
"verify": "npm run lint && npm run check"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "4.22.1",
|
|
14
|
+
"cors": "2.8.6",
|
|
15
|
+
"http-proxy-middleware": "3.0.5",
|
|
16
|
+
"drizzle-orm": "0.44.7",
|
|
17
|
+
"drizzle-kit": "0.30.6",
|
|
18
|
+
"croner": "9.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/js": "9.39.4",
|
|
22
|
+
"eslint": "9.39.4",
|
|
23
|
+
"globals": "16.5.0"
|
|
24
|
+
}
|
|
25
|
+
}
|