create-surf-app 1.0.0-alpha.3 → 1.0.0-alpha.5
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-NZOL5GFZ.js → chunk-FDATV75D.js} +3 -47
- package/dist/cli.js +1 -7
- package/dist/index.js +1 -1
- package/dist/templates/default/CLAUDE.md +20 -0
- package/dist/templates/default/backend/.env.example +2 -0
- package/dist/templates/default/backend/package.json +3 -3
- package/dist/templates/default/backend/scripts/check-env.js +40 -0
- package/dist/templates/default/frontend/.env.example +3 -0
- package/dist/templates/default/frontend/package.json +2 -2
- package/dist/templates/default/frontend/scripts/check-env.cjs +51 -0
- package/dist/templates/default/frontend/vite.config.ts +6 -22
- package/dist/templates/default/package.json +2 -2
- package/dist/templates/nextjs/.env.example +3 -0
- package/dist/templates/nextjs/package.json +2 -2
- package/dist/templates/nextjs/scripts/check-env.js +12 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
44
|
-
|
|
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,
|
|
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-
|
|
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
|
@@ -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
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "backend",
|
|
3
3
|
"private": true,
|
|
4
4
|
"scripts": {
|
|
5
|
-
"start": "node
|
|
6
|
-
"dev": "node
|
|
5
|
+
"start": "node scripts/check-env.js node server.js",
|
|
6
|
+
"dev": "node scripts/check-env.js node --watch server.js"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@surf-ai/sdk": "1.0.0-alpha.
|
|
9
|
+
"@surf-ai/sdk": "1.0.0-alpha.5",
|
|
10
10
|
"express": "4.22.1"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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: FRONTEND_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
|
+
: ['FRONTEND_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
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
3
|
import react from '@vitejs/plugin-react'
|
|
4
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
env
|
|
8
|
-
|
|
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.FRONTEND_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',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-app",
|
|
3
3
|
"private": true,
|
|
4
|
+
"workspaces": ["backend", "frontend"],
|
|
4
5
|
"scripts": {
|
|
5
|
-
"dev": "concurrently \"npm run dev --
|
|
6
|
-
"install:all": "cd backend && npm install && cd ../frontend && npm install"
|
|
6
|
+
"dev": "concurrently \"npm run dev --workspace backend\" \"npm run dev --workspace frontend\""
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"concurrently": "^9.0.0"
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"type-check": "tsc --noEmit --incremental"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@surf-ai/sdk": "1.0.0-alpha.
|
|
12
|
+
"@surf-ai/sdk": "1.0.0-alpha.5",
|
|
13
13
|
"@surf-ai/theme": "latest",
|
|
14
|
-
"next": "15.
|
|
14
|
+
"next": "15.5.14",
|
|
15
15
|
"react": "19.2.4",
|
|
16
16
|
"react-dom": "19.2.4",
|
|
17
17
|
"drizzle-orm": "0.44.2",
|
|
@@ -26,14 +26,21 @@ if (fs.existsSync(envPath)) {
|
|
|
26
26
|
const args = process.argv.slice(2)
|
|
27
27
|
const isBuild = args.includes('build')
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// Vars that must be non-empty
|
|
30
|
+
const requiredNonEmpty = isBuild
|
|
31
|
+
? []
|
|
32
|
+
: ['FRONTEND_PORT', 'SURF_API_KEY']
|
|
32
33
|
|
|
33
|
-
|
|
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
|
|
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
|
|