create-surf-app 1.0.0-alpha.3 → 1.0.0-alpha.30

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,21 +1,16 @@
1
1
  // src/index.ts
2
2
  import fs from "fs";
3
- import os from "os";
4
3
  import path from "path";
5
4
  import { fileURLToPath } from "url";
6
- var DEFAULT_BACKEND_PORT = "3001";
7
5
  var VALID_TEMPLATES = ["vite", "nextjs"];
8
6
  async function createSurfApp({
9
7
  projectName = ".",
10
- backendPort = process.env.BACKEND_PORT || DEFAULT_BACKEND_PORT,
11
- previewBase = process.env.BASE_PATH,
12
8
  template: templateArg,
13
9
  logger = console.log
14
10
  } = {}) {
15
11
  const root = path.resolve(projectName);
16
12
  const name = path.basename(root);
17
13
  const template = validateTemplate(templateArg);
18
- const validatedBackendPort = validatePort("backend", backendPort);
19
14
  const templateDir = resolveTemplateDir(template);
20
15
  logger(`
21
16
  Creating Surf app (${template}) in ${root}
@@ -23,7 +18,6 @@ async function createSurfApp({
23
18
  fs.mkdirSync(root, { recursive: true });
24
19
  copyDir(templateDir, root, root, logger);
25
20
  if (template === "nextjs") {
26
- writeNextjsEnvFile(root, validatedBackendPort, previewBase);
27
21
  finalizePackageName(root, name);
28
22
  logger(`
29
23
  Done! Next steps:
@@ -35,19 +29,12 @@ Done! Next steps:
35
29
  Open http://localhost:3000
36
30
  `);
37
31
  } else {
38
- writeEnvFiles(root, validatedBackendPort, previewBase);
39
32
  logger(`
40
33
  Done! Next steps:
41
34
 
42
35
  cd ${name}
43
- cd backend && npm install && cd ..
44
- cd frontend && npm install && cd ..
45
-
46
- # Start backend
47
- cd backend && npm run dev &
48
-
49
- # Start frontend
50
- cd frontend && npm run dev
36
+ npm install
37
+ npm run dev
51
38
 
52
39
  Open the local URL printed by Vite
53
40
  `);
@@ -87,43 +74,12 @@ function copyDir(src, dest, root, logger) {
87
74
  logger(` ${path.relative(root, destPath)}`);
88
75
  }
89
76
  }
90
- function validatePort(label, value) {
91
- const port = Number.parseInt(value, 10);
92
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
93
- throw new Error(`Invalid ${label} port: ${value}`);
94
- }
95
- return String(port);
96
- }
97
77
  function finalizePackageName(root, projectName) {
98
78
  const pkgPath = path.join(root, "package.json");
99
79
  if (!fs.existsSync(pkgPath)) return;
100
80
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
101
81
  pkg.name = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "surf-app";
102
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${os.EOL}`);
103
- }
104
- function writeNextjsEnvFile(root, port, basePath) {
105
- const envPath = path.join(root, ".env");
106
- const envContent = [
107
- `FRONTEND_PORT=${port}`,
108
- `BASE_PATH=${basePath || ""}`,
109
- "SURF_API_KEY="
110
- ].join(os.EOL);
111
- fs.writeFileSync(envPath, `${envContent}${os.EOL}`);
112
- }
113
- function writeEnvFiles(root, backendPort, previewBase) {
114
- const backendEnvPath = path.join(root, "backend", ".env");
115
- const frontendEnvPath = path.join(root, "frontend", ".env");
116
- const backendEnv = [
117
- `BACKEND_PORT=${backendPort}`,
118
- "SURF_API_KEY="
119
- ].join(os.EOL);
120
- fs.writeFileSync(backendEnvPath, `${backendEnv}${os.EOL}`);
121
- const frontendEnv = [
122
- "FRONTEND_PORT=5173",
123
- `BACKEND_PORT=${backendPort}`,
124
- `BASE_PATH=${previewBase || ""}`
125
- ].join(os.EOL);
126
- fs.writeFileSync(frontendEnvPath, `${frontendEnv}${os.EOL}`);
82
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
127
83
  }
128
84
 
129
85
  export {
package/dist/cli.js CHANGED
@@ -1,13 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSurfApp
4
- } from "./chunk-NZOL5GFZ.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-NZOL5GFZ.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 --env-file=.env server.js",
6
- "dev": "node --env-file=.env --watch server.js"
5
+ "dev": "node scripts/check-env.js node --watch server.js"
7
6
  },
8
7
  "dependencies": {
9
- "@surf-ai/sdk": "1.0.0-alpha.3",
8
+ "@surf-ai/sdk": "1.0.0-alpha.30",
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
+ }
@@ -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 .",
@@ -78,6 +78,6 @@
78
78
  "tw-animate-css": "1.4.0",
79
79
  "typescript-eslint": "8.57.1",
80
80
  "typescript": "5.9.3",
81
- "vite": "6.4.1"
81
+ "vite": "6.4.2"
82
82
  }
83
83
  }
@@ -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,25 +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: 'BACKEND_PORT' | 'FRONTEND_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 frontendPort = readRequiredPort(env, 'FRONTEND_PORT')
21
- const backendPort = readRequiredPort(env, 'BACKEND_PORT')
22
- const base = env.BASE_PATH || './'
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 || './'
23
10
  const hasAbsBase = base.startsWith('/')
24
11
  const apiBasePrefix = hasAbsBase ? base.replace(/\/$/, '') : ''
25
12
 
@@ -35,11 +22,10 @@ export default defineConfig(({ mode }) => {
35
22
  plugins: [react(), tailwindcss()],
36
23
  server: {
37
24
  host: '0.0.0.0',
38
- port: frontendPort,
25
+ port: frontendPort || undefined,
39
26
  proxy: {
40
27
  [`${apiBasePrefix}/api`]: backendProxy,
41
28
  },
42
- // Keep the HMR socket under the preview base path.
43
29
  hmr: {
44
30
  path: 'ws/vite-hmr',
45
31
  },
@@ -51,8 +37,6 @@ export default defineConfig(({ mode }) => {
51
37
  dedupe: ['react', 'react-dom'],
52
38
  preserveSymlinks: true,
53
39
  },
54
- // Pre-bundle the deps touched during the initial boot path so cold starts
55
- // do not race Vite's lazy dependency optimizer.
56
40
  optimizeDeps: {
57
41
  include: [
58
42
  'react',
@@ -0,0 +1,3 @@
1
+ PORT=3000
2
+ BASE_PATH=
3
+ SURF_API_KEY=
@@ -79,7 +79,7 @@ Tables are auto-synced on server start and when `db/schema.ts` changes in dev mo
79
79
  - `db/index.ts` - database connection
80
80
  - `lib/boot.ts` - infrastructure (schema sync, cron init)
81
81
  - `app/layout.tsx` - root layout and providers
82
- - `app/providers.tsx` - client-side providers (QueryClient, theme)
82
+ - `app/providers.tsx` - client-side preview bridge hooks
83
83
  - `eslint.config.mjs` - lint rules
84
84
  - `globals.css` - only imports, do not add styles here (use Tailwind classes)
85
85
 
@@ -1,23 +1,35 @@
1
1
  "use client"
2
2
 
3
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4
- import { useState } from "react"
3
+ import { useEffect } from "react"
5
4
 
6
- export function Providers({ children }: { children: React.ReactNode }) {
7
- const [queryClient] = useState(
8
- () =>
9
- new QueryClient({
10
- defaultOptions: {
11
- queries: {
12
- refetchOnWindowFocus: false,
13
- retry: 3,
14
- staleTime: 30 * 1000,
15
- },
16
- },
17
- })
18
- )
5
+ // Notify parent frame that the app has rendered.
6
+ // DO NOT REMOVE — the hosting app uses this to dismiss the loading overlay.
7
+ function useSurfAppReady() {
8
+ useEffect(() => {
9
+ try {
10
+ window.parent.postMessage({ type: "surf-app-ready" }, "*")
11
+ } catch {
12
+ /* cross-origin — ignore */
13
+ }
14
+ }, [])
15
+ }
19
16
 
20
- return (
21
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
22
- )
17
+ // Patch fetch so `/api/*` calls automatically include basePath.
18
+ // Without this, `fetch('/api/...')` hits the parent app's routes instead of
19
+ // the preview dev server's API routes.
20
+ // DO NOT REMOVE — this is required for the preview proxy architecture.
21
+ const _basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
22
+ if (typeof window !== "undefined" && _basePath) {
23
+ const _origFetch = window.fetch
24
+ window.fetch = function patchedFetch(this: typeof globalThis, input: RequestInfo | URL, init?: RequestInit) {
25
+ if (typeof input === "string" && input.startsWith("/api/")) {
26
+ input = _basePath + input
27
+ }
28
+ return _origFetch.call(this, input, init)
29
+ } as typeof window.fetch
30
+ }
31
+
32
+ export function Providers({ children }: { children: React.ReactNode }) {
33
+ useSurfAppReady()
34
+ return children
23
35
  }
@@ -1,7 +1,11 @@
1
1
  import type { NextConfig } from 'next'
2
2
 
3
3
  const nextConfig: NextConfig = {
4
+ output: 'standalone',
4
5
  basePath: process.env.BASE_PATH!.replace(/\/+$/, ''),
6
+ env: {
7
+ NEXT_PUBLIC_BASE_PATH: process.env.BASE_PATH!.replace(/\/+$/, ''),
8
+ },
5
9
  serverExternalPackages: ['@surf-ai/sdk', 'drizzle-orm', 'drizzle-kit', 'croner'],
6
10
  }
7
11
 
@@ -4,17 +4,16 @@
4
4
  "scripts": {
5
5
  "dev": "node scripts/check-env.js next dev",
6
6
  "build": "node scripts/check-env.js next build",
7
- "start": "node scripts/check-env.js next start",
8
7
  "lint": "eslint .",
9
8
  "type-check": "tsc --noEmit --incremental"
10
9
  },
11
10
  "dependencies": {
12
- "@surf-ai/sdk": "1.0.0-alpha.3",
11
+ "@surf-ai/sdk": "1.0.0-alpha.30",
13
12
  "@surf-ai/theme": "latest",
14
- "next": "15.3.3",
13
+ "next": "15.5.14",
15
14
  "react": "19.2.4",
16
15
  "react-dom": "19.2.4",
17
- "drizzle-orm": "0.44.2",
16
+ "drizzle-orm": "0.45.2",
18
17
  "drizzle-kit": "0.31.1",
19
18
  "croner": "9.1.0",
20
19
  "class-variance-authority": "0.7.1",
@@ -60,8 +59,6 @@
60
59
  "@hookform/resolvers": "5.2.2",
61
60
  "date-fns": "4.1.0",
62
61
  "zod": "3.25.76",
63
- "@tanstack/react-query": "5.94.5",
64
- "@tanstack/query-core": "5.94.5",
65
62
  "scheduler": "0.27.0"
66
63
  },
67
64
  "devDependencies": {
@@ -26,22 +26,24 @@ if (fs.existsSync(envPath)) {
26
26
  const args = process.argv.slice(2)
27
27
  const isBuild = args.includes('build')
28
28
 
29
- const required = isBuild
30
- ? ['BASE_PATH', 'SURF_API_KEY']
31
- : ['FRONTEND_PORT', 'BASE_PATH', 'SURF_API_KEY']
29
+ // Vars that must be non-empty
30
+ const requiredNonEmpty = isBuild
31
+ ? []
32
+ : ['PORT', 'SURF_API_KEY']
32
33
 
33
- const missing = required.filter(k => !process.env[k])
34
+ // Vars that must be defined (empty is ok — e.g. 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]
34
40
 
35
41
  if (missing.length > 0) {
36
- console.error(`\n❌ Missing required env vars in .env: ${missing.join(', ')}\n`)
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`)
37
44
  process.exit(1)
38
45
  }
39
46
 
40
- // Pass FRONTEND_PORT as PORT so Next.js uses it
41
- if (process.env.FRONTEND_PORT) {
42
- process.env.PORT = process.env.FRONTEND_PORT
43
- }
44
-
45
47
  // Run the actual command
46
48
  try {
47
49
  execSync(args.join(' '), { stdio: 'inherit', env: process.env })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-surf-app",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.30",
4
4
  "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +0,0 @@
1
- {
2
- "name": "surf-app",
3
- "private": true,
4
- "scripts": {
5
- "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
6
- "install:all": "cd backend && npm install && cd ../frontend && npm install"
7
- },
8
- "devDependencies": {
9
- "concurrently": "^9.0.0"
10
- }
11
- }