create-surf-app 0.1.25 → 0.1.26-alpha.0

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.
@@ -0,0 +1,148 @@
1
+ // src/index.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ var DEFAULT_FRONTEND_PORT = "5173";
7
+ var DEFAULT_BACKEND_PORT = "3001";
8
+ var TEMPLATES = {
9
+ default: {
10
+ label: "Vite + React + Express"
11
+ },
12
+ nextjs: {
13
+ label: "Next.js App Router"
14
+ }
15
+ };
16
+ async function createSurfApp({
17
+ projectName = ".",
18
+ frontendPort = process.env.SURF_APP_FRONTEND_PORT || process.env.VITE_PORT || DEFAULT_FRONTEND_PORT,
19
+ backendPort = process.env.SURF_APP_BACKEND_PORT || process.env.VITE_BACKEND_PORT || DEFAULT_BACKEND_PORT,
20
+ previewBase = process.env.VITE_BASE,
21
+ template = "default",
22
+ logger = console.log
23
+ } = {}) {
24
+ const root = path.resolve(projectName);
25
+ const name = path.basename(root);
26
+ const resolvedTemplate = resolveTemplate(template);
27
+ const validatedFrontendPort = validatePort("frontend", frontendPort);
28
+ const validatedBackendPort = resolvedTemplate === "default" ? validatePort("backend", backendPort) : void 0;
29
+ const templateDir = resolveTemplateDir(resolvedTemplate);
30
+ logger(`
31
+ Creating Surf app (${TEMPLATES[resolvedTemplate].label}) in ${root}
32
+ `);
33
+ fs.mkdirSync(root, { recursive: true });
34
+ copyDir(templateDir, root, root, logger);
35
+ writeEnvFiles(root, resolvedTemplate, validatedFrontendPort, validatedBackendPort, previewBase);
36
+ finalizeTemplate(root, resolvedTemplate, name);
37
+ logger(renderNextSteps({
38
+ name,
39
+ template: resolvedTemplate,
40
+ frontendPort: validatedFrontendPort,
41
+ backendPort: validatedBackendPort
42
+ }));
43
+ return root;
44
+ }
45
+ function resolveTemplate(template) {
46
+ if (template in TEMPLATES) {
47
+ return template;
48
+ }
49
+ throw new Error(`Unknown template: ${template}. Expected one of: ${Object.keys(TEMPLATES).join(", ")}`);
50
+ }
51
+ function renderNextSteps({
52
+ name,
53
+ template,
54
+ frontendPort,
55
+ backendPort
56
+ }) {
57
+ if (template === "nextjs") {
58
+ return `
59
+ Done! Next steps:
60
+
61
+ cd ${name}
62
+ npm install
63
+ npm run dev -- --port ${frontendPort}
64
+
65
+ Open http://localhost:${frontendPort}
66
+ `;
67
+ }
68
+ return `
69
+ Done! Next steps:
70
+
71
+ cd ${name}
72
+ cd backend && npm install && cd ..
73
+ cd frontend && npm install && cd ..
74
+
75
+ # Start backend
76
+ cd backend && PORT=${backendPort} npm run dev &
77
+
78
+ # Start frontend
79
+ cd frontend && npm run dev
80
+
81
+ Open http://localhost:${frontendPort}
82
+ `;
83
+ }
84
+ function resolveTemplateDir(template) {
85
+ const here = path.dirname(fileURLToPath(import.meta.url));
86
+ const candidates = [
87
+ path.join(here, "templates", template),
88
+ path.join(here, "..", "templates", template)
89
+ ];
90
+ for (const candidate of candidates) {
91
+ if (fs.existsSync(candidate)) return candidate;
92
+ }
93
+ throw new Error(`Could not find ${template} template near ${here}`);
94
+ }
95
+ function copyDir(src, dest, root, logger) {
96
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
97
+ const srcPath = path.join(src, entry.name);
98
+ const destPath = path.join(dest, entry.name);
99
+ if (entry.isDirectory()) {
100
+ fs.mkdirSync(destPath, { recursive: true });
101
+ copyDir(srcPath, destPath, root, logger);
102
+ continue;
103
+ }
104
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
105
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath));
106
+ logger(` ${path.relative(root, destPath)}`);
107
+ }
108
+ }
109
+ function validatePort(label, value) {
110
+ const port = Number.parseInt(value, 10);
111
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
112
+ throw new Error(`Invalid ${label} port: ${value}`);
113
+ }
114
+ return String(port);
115
+ }
116
+ function finalizeTemplate(root, template, projectName) {
117
+ if (template !== "nextjs") {
118
+ return;
119
+ }
120
+ const packageJsonPath = path.join(root, "package.json");
121
+ if (!fs.existsSync(packageJsonPath)) {
122
+ return;
123
+ }
124
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
125
+ packageJson.name = toPackageName(projectName);
126
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}${os.EOL}`);
127
+ }
128
+ function toPackageName(projectName) {
129
+ const normalized = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
130
+ return normalized || "next-app";
131
+ }
132
+ function writeEnvFiles(root, template, frontendPort, backendPort, previewBase) {
133
+ if (template === "nextjs") {
134
+ return;
135
+ }
136
+ const backendEnvPath = path.join(root, "backend", ".env");
137
+ const frontendEnvPath = path.join(root, "frontend", ".env");
138
+ fs.writeFileSync(backendEnvPath, `PORT=${backendPort}${os.EOL}`);
139
+ let frontendEnv = `VITE_PORT=${frontendPort}${os.EOL}VITE_BACKEND_PORT=${backendPort}${os.EOL}`;
140
+ if (previewBase) {
141
+ frontendEnv += `VITE_BASE=${previewBase}${os.EOL}`;
142
+ }
143
+ fs.writeFileSync(frontendEnvPath, frontendEnv);
144
+ }
145
+
146
+ export {
147
+ createSurfApp
148
+ };
package/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSurfApp
4
- } from "./chunk-GRAOPFXI.js";
4
+ } from "./chunk-E32T2IIS.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
8
8
  "--frontend-port",
9
9
  "--backend-port",
10
- "--preview-base"
10
+ "--preview-base",
11
+ "--template"
11
12
  ]);
12
13
  function getFlag(args, name) {
13
14
  const idx = args.indexOf(name);
@@ -36,7 +37,8 @@ function parseCliArgs(args) {
36
37
  projectName: positionalArgs[0] || ".",
37
38
  frontendPort: getFlag(args, "--frontend-port"),
38
39
  backendPort: getFlag(args, "--backend-port"),
39
- previewBase: getFlag(args, "--preview-base")
40
+ previewBase: getFlag(args, "--preview-base"),
41
+ template: getFlag(args, "--template")
40
42
  };
41
43
  }
42
44
  async function runCli() {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSurfApp
3
- } from "./chunk-GRAOPFXI.js";
3
+ } from "./chunk-E32T2IIS.js";
4
4
  export {
5
5
  createSurfApp
6
6
  };
@@ -1,6 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  interface ImportMetaEnv {
4
+ readonly VITE_PORT: string
4
5
  readonly VITE_BACKEND_PORT: string
5
6
  }
6
7
 
@@ -5,7 +5,7 @@ import tailwindcss from '@tailwindcss/vite'
5
5
 
6
6
  function readRequiredPort(
7
7
  env: Record<string, string>,
8
- name: 'VITE_BACKEND_PORT',
8
+ name: 'VITE_BACKEND_PORT' | 'VITE_PORT',
9
9
  ) {
10
10
  const value = env[name]
11
11
  const port = Number.parseInt(value || '', 10)
@@ -33,6 +33,7 @@ export default defineConfig(({ mode }) => {
33
33
  return {
34
34
  plugins: [react(), tailwindcss()],
35
35
  server: {
36
+ port: readRequiredPort(env, 'VITE_PORT'),
36
37
  host: '0.0.0.0',
37
38
  proxy: {
38
39
  [`${apiBasePrefix}/api`]: backendProxy,
@@ -0,0 +1,10 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+
6
+ # Surf SDK
7
+
8
+ - This is the official Next.js app template.
9
+ - Fetch Surf data in route handlers with `dataApi` from `@surf-ai/sdk/server`.
10
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,54 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Surf SDK
4
+
5
+ This template keeps the official Next.js app structure. Server-side Surf data is fetched in route handlers with `@surf-ai/sdk`.
6
+
7
+ Example:
8
+
9
+ ```ts
10
+ import { dataApi } from "@surf-ai/sdk/server";
11
+ import { NextResponse } from "next/server";
12
+
13
+ export async function GET() {
14
+ const data = await dataApi.market.price({ symbol: "BTC", time_range: "1d" });
15
+ return NextResponse.json(data);
16
+ }
17
+ ```
18
+
19
+ Provide `SURF_API_KEY` in the runtime environment before calling Surf-backed routes.
20
+
21
+ ## Getting Started
22
+
23
+ First, run the development server:
24
+
25
+ ```bash
26
+ npm run dev
27
+ # or
28
+ yarn dev
29
+ # or
30
+ pnpm dev
31
+ # or
32
+ bun dev
33
+ ```
34
+
35
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
36
+
37
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
38
+
39
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
40
+
41
+ ## Learn More
42
+
43
+ To learn more about Next.js, take a look at the following resources:
44
+
45
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
46
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
47
+
48
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
49
+
50
+ ## Deploy on Vercel
51
+
52
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
53
+
54
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,3 @@
1
+ export async function GET() {
2
+ return Response.json({ status: "ok" });
3
+ }
@@ -0,0 +1,25 @@
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;
5
+
6
+ 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
+ });
19
+
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
+ }
25
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,33 @@
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
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Create Next App",
17
+ description: "Generated by create next app",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ 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>
31
+ </html>
32
+ );
33
+ }
@@ -0,0 +1,65 @@
1
+ import Image from "next/image";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
6
+ <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
7
+ <Image
8
+ className="dark:invert"
9
+ src="/next.svg"
10
+ alt="Next.js logo"
11
+ width={100}
12
+ height={20}
13
+ priority
14
+ />
15
+ <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
16
+ <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
17
+ To get started, edit the page.tsx file.
18
+ </h1>
19
+ <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
20
+ Looking for a starting point or more instructions? Head over to{" "}
21
+ <a
22
+ href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
23
+ className="font-medium text-zinc-950 dark:text-zinc-50"
24
+ >
25
+ Templates
26
+ </a>{" "}
27
+ or the{" "}
28
+ <a
29
+ href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30
+ className="font-medium text-zinc-950 dark:text-zinc-50"
31
+ >
32
+ Learning
33
+ </a>{" "}
34
+ center.
35
+ </p>
36
+ </div>
37
+ <div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
38
+ <a
39
+ className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
40
+ href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ >
44
+ <Image
45
+ className="dark:invert"
46
+ src="/vercel.svg"
47
+ alt="Vercel logomark"
48
+ width={16}
49
+ height={16}
50
+ />
51
+ Deploy Now
52
+ </a>
53
+ <a
54
+ className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
55
+ href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56
+ target="_blank"
57
+ rel="noopener noreferrer"
58
+ >
59
+ Documentation
60
+ </a>
61
+ </div>
62
+ </main>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cna-baseline",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@surf-ai/sdk": "1.0.0-alpha.0",
13
+ "next": "16.2.2",
14
+ "react": "19.2.4",
15
+ "react-dom": "19.2.4"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.2.2",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-surf-app",
3
- "version": "0.1.25",
4
- "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk",
3
+ "version": "0.1.26-alpha.0",
4
+ "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk or Next.js",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-surf-app": "./dist/cli.js"
@@ -1,85 +0,0 @@
1
- // src/index.ts
2
- import fs from "fs";
3
- import os from "os";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- var DEFAULT_BACKEND_PORT = "3001";
7
- async function createSurfApp({
8
- projectName = ".",
9
- backendPort = process.env.VITE_BACKEND_PORT || DEFAULT_BACKEND_PORT,
10
- previewBase = process.env.VITE_BASE,
11
- logger = console.log
12
- } = {}) {
13
- const root = path.resolve(projectName);
14
- const name = path.basename(root);
15
- const validatedBackendPort = validatePort("backend", backendPort);
16
- const templateDir = resolveTemplateDir();
17
- logger(`
18
- Creating Surf app in ${root}
19
- `);
20
- fs.mkdirSync(root, { recursive: true });
21
- copyDir(templateDir, root, root, logger);
22
- writeEnvFiles(root, validatedBackendPort, previewBase);
23
- logger(`
24
- Done! Next steps:
25
-
26
- cd ${name}
27
- cd backend && npm install && cd ..
28
- cd frontend && npm install && cd ..
29
-
30
- # Start backend
31
- cd backend && PORT=${validatedBackendPort} npm run dev &
32
-
33
- # Start frontend
34
- cd frontend && npm run dev
35
-
36
- Open the local URL printed by Vite
37
- `);
38
- return root;
39
- }
40
- function resolveTemplateDir() {
41
- const here = path.dirname(fileURLToPath(import.meta.url));
42
- const candidates = [
43
- path.join(here, "templates", "default"),
44
- path.join(here, "..", "templates", "default")
45
- ];
46
- for (const candidate of candidates) {
47
- if (fs.existsSync(candidate)) return candidate;
48
- }
49
- throw new Error(`Could not find default template near ${here}`);
50
- }
51
- function copyDir(src, dest, root, logger) {
52
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
53
- const srcPath = path.join(src, entry.name);
54
- const destPath = path.join(dest, entry.name);
55
- if (entry.isDirectory()) {
56
- fs.mkdirSync(destPath, { recursive: true });
57
- copyDir(srcPath, destPath, root, logger);
58
- continue;
59
- }
60
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
61
- fs.writeFileSync(destPath, fs.readFileSync(srcPath));
62
- logger(` ${path.relative(root, destPath)}`);
63
- }
64
- }
65
- function validatePort(label, value) {
66
- const port = Number.parseInt(value, 10);
67
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
68
- throw new Error(`Invalid ${label} port: ${value}`);
69
- }
70
- return String(port);
71
- }
72
- function writeEnvFiles(root, backendPort, previewBase) {
73
- const backendEnvPath = path.join(root, "backend", ".env");
74
- const frontendEnvPath = path.join(root, "frontend", ".env");
75
- fs.writeFileSync(backendEnvPath, `PORT=${backendPort}${os.EOL}`);
76
- let frontendEnv = `VITE_BACKEND_PORT=${backendPort}${os.EOL}`;
77
- if (previewBase) {
78
- frontendEnv += `VITE_BASE=${previewBase}${os.EOL}`;
79
- }
80
- fs.writeFileSync(frontendEnvPath, frontendEnv);
81
- }
82
-
83
- export {
84
- createSurfApp
85
- };