From 04d65934f66867079041c796d01264f816719852 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 19 Feb 2026 00:14:35 -0500 Subject: [PATCH 1/7] feat: empty project creation --- .gitignore | 58 +- index.js | 520 ----------------- package-lock.json | 602 +++++++++++++++++++- package.json | 21 +- src/commands/init.ts | 14 + src/core/exec.ts | 20 + src/core/inspect.ts | 16 + src/core/packageManager.ts | 10 + src/core/paths.ts | 9 + src/generators/frontend/react.ts | 93 +++ src/index.ts | 538 +++++++++++++++++ src/inspectors/detectBackend.ts | 0 src/inspectors/detectDatabase.ts | 0 src/inspectors/detectFrontend.ts | 0 src/prompts/emptyProject.ts | 23 + src/registry.ts | 0 templates/frontend/react/App.css.tpl | 157 +++++ templates/frontend/react/App.router.tsx.tpl | 77 +++ templates/frontend/react/App.tsx.tpl | 67 +++ templates/frontend/react/env.example.tpl | 2 + templates/frontend/react/main.tsx.tpl | 9 + tsconfig.json | 17 + 22 files changed, 1718 insertions(+), 535 deletions(-) delete mode 100755 index.js create mode 100644 src/commands/init.ts create mode 100644 src/core/exec.ts create mode 100644 src/core/inspect.ts create mode 100644 src/core/packageManager.ts create mode 100644 src/core/paths.ts create mode 100644 src/generators/frontend/react.ts create mode 100755 src/index.ts create mode 100644 src/inspectors/detectBackend.ts create mode 100644 src/inspectors/detectDatabase.ts create mode 100644 src/inspectors/detectFrontend.ts create mode 100644 src/prompts/emptyProject.ts create mode 100644 src/registry.ts create mode 100644 templates/frontend/react/App.css.tpl create mode 100644 templates/frontend/react/App.router.tsx.tpl create mode 100644 templates/frontend/react/App.tsx.tpl create mode 100644 templates/frontend/react/env.example.tpl create mode 100644 templates/frontend/react/main.tsx.tpl create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 877f6c1..d3cb9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,57 @@ -node_modules -.npmrc +# ---------------------------- +# Dependencies +# ---------------------------- +node_modules/ + +# ---------------------------- +# Build Output +# ---------------------------- +dist/ +*.tsbuildinfo + +# ---------------------------- +# Logs +# ---------------------------- +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +*.log + +# ---------------------------- +# Environment Files +# ---------------------------- +.env +.env.local +.env.*.local + +# ---------------------------- +# Coverage / Testing +# ---------------------------- +coverage/ +.nyc_output/ + +# ---------------------------- +# IDE / Editor +# ---------------------------- +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store + +# ---------------------------- +# OS +# ---------------------------- +Thumbs.db .DS_Store + +# ---------------------------- +# npm pack artifacts +# ---------------------------- +*.tgz + +# ---------------------------- +# Local linking artifacts +# ---------------------------- +.npmrc diff --git a/index.js b/index.js deleted file mode 100755 index cb304af..0000000 --- a/index.js +++ /dev/null @@ -1,520 +0,0 @@ -#!/usr/bin/env node -import fs from "fs"; -import path from "path"; -import { execSync } from "child_process"; -import { pipeline } from "stream"; -import { promisify } from "util"; -import { createWriteStream } from "fs"; -import AdmZip from "adm-zip"; -import { randomBytes } from "crypto"; -import net from "net"; - -const streamPipeline = promisify(pipeline); - -const MIN_NODE_MAJOR = 18; -const nodeMajor = Number(process.versions.node.split(".")[0]); - -function ensureExecutable(filePath) { - if (fs.existsSync(filePath)) { - fs.chmodSync(filePath, 0o755); - } -} - -function isPortAvailable(port) { - return new Promise((resolve) => { - const server = net - .createServer() - .once("error", () => resolve(false)) - .once("listening", () => { - server.close(); - resolve(true); - }) - .listen(port); - }); -} - -function printHelp() { - console.log(` -create-seamless - -Scaffold a local Seamless Auth development environment. - -Usage: - npx create-seamless [project-name] [options] - -Options: - --auth Include the Seamless Auth server - --api Include the Express API example - --web Include the React web application - --no-git Skip git initialization - - --auth-port Auth server port (default: 5312) - --api-port API server port (default: 3000) - --web-port Web server port (default: 5001) - - -h, --help Show this help message - -If no component flags are provided, all components are included. - -Docs: - https://docs.seamlessauth.com -`); -} - -if (nodeMajor < MIN_NODE_MAJOR) { - console.error(` -❌ Seamless requires Node ${MIN_NODE_MAJOR}+. -You are running Node ${process.versions.node} - -Upgrade at https://nodejs.org -`); - process.exit(1); -} - -const args = process.argv.slice(2); - -if (args.includes("-h") || args.includes("--help")) { - printHelp(); - process.exit(0); -} -const projectName = args.find((a) => !a.startsWith("--")) ?? "seamless-app"; - -const hasFlag = (flag) => args.includes(`--${flag}`); -const getFlag = (flag, fallback) => { - const i = args.indexOf(`--${flag}`); - return i !== -1 ? args[i + 1] : fallback; -}; - -const includeAuth = hasFlag("auth"); -const includeWeb = hasFlag("web"); -const includeApi = hasFlag("api"); -const skipGit = hasFlag("no-git"); - -const authPort = getFlag("auth-port", "5312"); -const apiPort = getFlag("api-port", "3000"); -const webPort = getFlag("web-port", "5001"); - -const wantsSomething = includeAuth || includeWeb || includeApi; -const AUTH = wantsSomething ? includeAuth : true; -const WEB = wantsSomething ? includeWeb : true; -const API = wantsSomething ? includeApi : true; - -const REPOS = { - auth: "fells-code/seamless-auth-api", - web: "fells-code/seamless-auth-starter-react", - api: "fells-code/seamless-auth-starter-express", -}; - -const GENERATED_README = (projectName) => `# ${projectName} - -This project was generated with \`create-seamless\` and provides a fully local, -open source authentication stack built on **Seamless Auth**. - -It is designed for development environments where you want: - -- Passwordless authentication -- No hosted dependencies -- No redirects or third-party auth services -- A production-shaped local setup - ---- - -## Project layout - -\`\`\`text -. -├─ auth/ # Seamless Auth open source server -├─ api/ # Backend API server (optional) -├─ web/ # Frontend web application (optional) -├─ Docker-compose.yml # Docker compose for one command spin up of dev environment -└─ README.md -\`\`\` - ---- - -## Running the stack - -### Running with Docker (optional) - -This project includes a Docker Compose configuration that allows you to run the -entire Seamless Auth stack locally with a single command. - -### Requirements - -* Docker -* Docker Compose - -### Start the stack - -From the project root, run: - -\`\`\`bash -docker compose up -\`\`\` - -This will start the following services in development mode: - -* Postgres database -* Seamless Auth server -* API server -* Web UI - -All services are configured with hot reload. Changes to the source code will be -picked up automatically. - -### Access the application - -Once all services are running, open: - -\`\`\` -http://localhost:5001 -\`\`\` - -This is the main entry point for the web application. - -### Stopping the stack - -To stop all services: - -\`\`\`bash -docker compose down -\`\`\` - -This will shut down all containers while preserving the local database volume. - -Open separate terminals and run each service independently. - -### Auth server - -\`\`\`bash -cd auth -npm run dev -\`\`\` - -Default port: \`5312\` - ---- - -### API server - -\`\`\`bash -cd api -npm run dev -\`\`\` - -Default port: \`3000\` - ---- - -### Web application - -\`\`\`bash -cd web -npm run dev -\`\`\` - -Default port: \`5001\` - ---- - -## Documentation - -Full Seamless Auth documentation: - -https://seamlessauth.com/docs - ---- - -## Included open source projects - -- Seamless Auth Server - https://github.com/fells-code/seamless-auth-server - -- Seamless Auth React Starter - https://github.com/fells-code/seamless-auth-starter-react - -- Seamless Auth API Starter - https://github.com/fells-code/seamless-auth-starter-express - ---- - -## License - -This generated project inherits the licenses of the open source components it -includes. - -Review each subproject for its specific license before deploying to production. -`; - -const GENERATED_DOCKER_COMPOSE = ` -services: - db: - image: postgres:16 - container_name: seamless-db - ports: - - "5432:5432" - environment: - POSTGRES_USER: myuser - POSTGRES_PASSWORD: mypassword - POSTGRES_DB: postgres - volumes: - - pgdata:/var/lib/postgresql/data - - ./postgres_init:/docker-entrypoint-initdb.d - healthcheck: - test: ["CMD-SHELL", "pg_isready -U myuser -d postgres"] - interval: 5s - timeout: 5s - retries: 5 - - auth: - container_name: seamless-auth - build: - context: ./auth - dockerfile: Dockerfile.dev - ports: - - "${authPort}:${authPort}" - env_file: - - ./auth/.env - environment: - DB_HOST: db - ISSUER: http://auth:${authPort} - volumes: - - ./auth:/app - - /app/node_modules - depends_on: - db: - condition: service_healthy - - api: - container_name: seamless-api - build: ./api - ports: - - "${apiPort}:${apiPort}" - env_file: - - ./api/.env - environment: - AUTH_SERVER_URL: http://auth:${authPort} - DB_HOST: db - volumes: - - ./api:/app - - /app/node_modules - depends_on: - db: - condition: service_healthy - - web: - container_name: seamless-web - build: ./web - ports: - - "${webPort}:${webPort}" - env_file: - - ./web/.env - volumes: - - ./web:/app - - /app/node_modules - depends_on: - - auth - - api - -volumes: - pgdata: -`; - -function writeEnv(dir, values) { - const env = Object.entries(values) - .map(([k, v]) => `${k}=${v}`) - .join("\n"); - fs.writeFileSync(path.join(dir, ".env"), env + "\n"); -} - -async function downloadRepo(repo, dest) { - const url = `https://codeload.github.com/${repo}/zip/refs/heads/main`; - const res = await fetch(url); - if (!res.ok || !res.body) { - throw new Error(`Failed to download ${repo}`); - } - - const zipPath = path.join(dest, "_repo.zip"); - await streamPipeline(res.body, createWriteStream(zipPath)); - - const zip = new AdmZip(zipPath); - zip.extractAllTo(dest, true); - fs.unlinkSync(zipPath); - - const inner = fs.readdirSync(dest).find((f) => f.endsWith("-main")); - if (!inner) throw new Error("Unexpected repo structure"); - - const innerPath = path.join(dest, inner); - for (const file of fs.readdirSync(innerPath)) { - fs.renameSync(path.join(innerPath, file), path.join(dest, file)); - } - fs.rmdirSync(innerPath); -} - -(async () => { - const root = path.join(process.cwd(), projectName); - - if (fs.existsSync(root)) { - console.error("❌ Directory already exists."); - process.exit(1); - } - - fs.mkdirSync(root); - - console.log(`\nCreating Seamless project: ${projectName}\n`); - const API_SERVICE_TOKEN = randomBytes(32).toString("hex"); - - let dbHostPort = "5432"; - - if (!(await isPortAvailable(5432))) { - dbHostPort = "5433"; - console.log(` -⚠️ Port 5432 is already in use on your machine. - Using 5433 for Docker Postgres instead. -`); - } - - if (AUTH) { - const dir = path.join(root, "auth"); - fs.mkdirSync(dir); - console.log("Fetching Seamless Auth OSS..."); - await downloadRepo(REPOS.auth, dir); - - writeEnv(dir, { - PORT: authPort, - NODE_ENV: "development", - - VERSION: "1.0.0", - APP_NAME: "Seamless Auth Example", - APP_ID: "local-dev", - APP_ORIGIN: `http://localhost:${apiPort}`, - ISSUER: `http://localhost:${authPort}`, - - AUTH_MODE: "server", - DEMO: "true", - - DEFAULT_ROLES: "user,betaUser", - AVAILABLE_ROLES: "user,admin,betaUser,team", - - DB_LOGGING: "false", - DB_HOST: "localhost", - DB_PORT: dbHostPort, - DB_NAME: "seamless_auth", - DB_USER: "myuser", - DB_PASSWORD: "mypassword", - - ACCESS_TOKEN_TTL: "30m", - REFRESH_TOKEN_TTL: "1h", - RATE_LIMIT: "100", - DELAY_AFTER: "50", - - API_SERVICE_TOKEN: API_SERVICE_TOKEN, - - JWKS_ACTIVE_KID: "dev-main", - - RPID: "localhost", - ORIGINS: `http://localhost:${webPort}`, - }); - } - - if (API) { - const dir = path.join(root, "api"); - fs.mkdirSync(dir); - console.log("Fetching API starter..."); - await downloadRepo(REPOS.api, dir); - - writeEnv(dir, { - AUTH_SERVER_URL: `http://localhost:${authPort}`, - APP_ORIGIN: `http://localhost:${apiPort}`, - UI_ORIGIN: `http://localhost:${webPort}`, - COOKIE_SIGNING_KEY: randomBytes(32).toString("hex"), - API_SERVICE_TOKEN: API_SERVICE_TOKEN, - JWKS_KID: "dev-main", - - DB_HOST: "localhost", - DB_PORT: dbHostPort, - DB_NAME: "seamless_api", - DB_USER: "myuser", - DB_PASSWORD: "mypassword", - - SQL_LOGGING: "false", - }); - } - - if (WEB) { - const dir = path.join(root, "web"); - fs.mkdirSync(dir); - console.log("Fetching Web starter..."); - await downloadRepo(REPOS.web, dir); - - writeEnv(dir, { - VITE_AUTH_SERVER_URL: `http://localhost:${apiPort}/`, - VITE_API_URL: `http://localhost:${apiPort}/`, - PORT: webPort, - }); - } - - fs.writeFileSync(path.join(root, "README.md"), GENERATED_README(projectName)); - - if (!skipGit) { - execSync("git init", { cwd: root }); - } - - if (AUTH && API && WEB) { - const compose = GENERATED_DOCKER_COMPOSE.replace( - '"5432:5432"', - `"${dbHostPort}:5432"`, - ); - - fs.writeFileSync(path.join(root, "docker-compose.yml"), compose); - } - - if (AUTH) { - const authScriptPath = path.join(root, "auth", "validateEnvs.sh"); - ensureExecutable(authScriptPath); - } - - const pgDir = path.join(root, "postgres_init"); - fs.mkdirSync(pgDir); - fs.writeFileSync( - path.join(pgDir, "init.sql"), - ` -CREATE DATABASE seamless_auth; -CREATE DATABASE seamless_api; - `, - ); - - console.log(` -╔════════════════════════════════════════╗ -║ S E A M L E S S ║ -╚════════════════════════════════════════╝ - -Your local auth stack is ready. - -Start development: - - cd ${projectName} - - # terminal 1 - cd auth && npm i && npm run dev - - # terminal 2 - cd api && npm i && npm run dev - - # terminal 3 - cd web && npm i && npm run dev - - or if using Docker - - docker compose up - - If you already have Postgres running locally, - the generator may map Docker to port 5433 instead. - -Docs: https://docs.seamlessauth.com/docs -Happy hacking. 🚀 -`); -})().catch((err) => { - console.error("\n❌ Error:", err.message); - process.exit(1); -}); diff --git a/package-lock.json b/package-lock.json index 2e188c6..fe3e02a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,17 @@ "version": "0.0.11", "license": "AGPL-3.0-only", "dependencies": { + "@clack/prompts": "^1.0.1", "adm-zip": "^0.5.16" }, "bin": { - "create-seamlessauth": "index.js" + "create-seamless": "dist/index.js" }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/node": "^24.10.1" + "@types/node": "^24.10.13", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } }, "node_modules/.pnpm/degit@2.8.4/node_modules/degit": { @@ -54,6 +57,469 @@ "node": ">=8.0.0" } }, + "node_modules/@clack/core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.1.tgz", + "integrity": "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.1.tgz", + "integrity": "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.0.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@types/adm-zip": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", @@ -65,9 +531,9 @@ } }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", "dependencies": { @@ -83,6 +549,132 @@ "node": ">=12.0" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index 56214fd..18246fd 100644 --- a/package.json +++ b/package.json @@ -12,25 +12,30 @@ }, "license": "AGPL-3.0-only", "author": "Fells Code, LLC", + "bin": { + "create-seamless": "dist/index.js" + }, "type": "module", + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "clean": "rm -rf dist" + }, "main": "index.js", "files": [ - "index.js", "dist", + "templates", "README.md", "LICENSE" ], - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "bin": { - "create-seamlessauth": "index.js" - }, "dependencies": { + "@clack/prompts": "^1.0.1", "adm-zip": "^0.5.16" }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/node": "^24.10.1" + "@types/node": "^24.10.13", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..ba6d001 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,14 @@ +import { inspectProject } from "../core/inspect.js"; +import { handleEmptyProject } from "../prompts/emptyProject.js"; +//import { handleExistingProject } from "../prompts/existingProject.js"; + +export async function initCommand() { + const context = await inspectProject(process.cwd()); + + if (!context.detected.anything) { + await handleEmptyProject(context); + return; + } + + //await handleExistingProject(context); +} diff --git a/src/core/exec.ts b/src/core/exec.ts new file mode 100644 index 0000000..901e492 --- /dev/null +++ b/src/core/exec.ts @@ -0,0 +1,20 @@ +import { spawn } from "child_process"; + +export function runCommand( + command: string, + args: string[], + cwd: string, +): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: "inherit", + cwd, + shell: true, + }); + + child.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`${command} failed`)); + }); + }); +} diff --git a/src/core/inspect.ts b/src/core/inspect.ts new file mode 100644 index 0000000..a05b091 --- /dev/null +++ b/src/core/inspect.ts @@ -0,0 +1,16 @@ +import fs from "fs"; +import path from "path"; +import { detectPackageManager } from "./packageManager.js"; + +export async function inspectProject(root: string) { + const hasPackageJson = fs.existsSync(path.join(root, "package.json")); + + return { + root, + packageManager: detectPackageManager(root), + detected: { + packageJson: hasPackageJson, + anything: hasPackageJson, + }, + }; +} diff --git a/src/core/packageManager.ts b/src/core/packageManager.ts new file mode 100644 index 0000000..f2af457 --- /dev/null +++ b/src/core/packageManager.ts @@ -0,0 +1,10 @@ +import fs from "fs"; +import path from "path"; + +export type PackageManager = "npm" | "pnpm" | "yarn"; + +export function detectPackageManager(root: string): PackageManager { + if (fs.existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm"; + if (fs.existsSync(path.join(root, "yarn.lock"))) return "yarn"; + return "npm"; +} diff --git a/src/core/paths.ts b/src/core/paths.ts new file mode 100644 index 0000000..4df1c52 --- /dev/null +++ b/src/core/paths.ts @@ -0,0 +1,9 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const PROJECT_ROOT = path.resolve(__dirname, "../../"); + +export const TEMPLATE_ROOT = path.join(PROJECT_ROOT, "templates"); diff --git a/src/generators/frontend/react.ts b/src/generators/frontend/react.ts new file mode 100644 index 0000000..6165bd2 --- /dev/null +++ b/src/generators/frontend/react.ts @@ -0,0 +1,93 @@ +import path from "path"; +import fs from "fs"; +import { runCommand } from "../../core/exec.js"; +import { TEMPLATE_ROOT } from "../../core/paths.js"; +import { select } from "@clack/prompts"; + +const VITE_VERSION = "7"; +const REACT_VERSION = "19"; +const ROUTER_VERSION = "7"; +const SEAMLESS_VERSION = "latest"; + +export async function generateReactVite(context: any) { + const { root, packageManager } = context; + + console.log("Scaffolding Vite project..."); + + await runCommand( + packageManager, + packageManager === "npm" + ? ["create", `vite@${VITE_VERSION}`, ".", "--", "--template", "react-ts"] + : ["create", `vite@${VITE_VERSION}`, ".", "--template", "react-ts"], + root, + ); + + const routerMode = await select({ + message: "Use built-in SeamlessAuth router + SDK routes?", + options: [ + { value: "router", label: "Yes (Router Mode)" }, + { value: "simple", label: "No (Custom Auth UI)" }, + ], + }); + + console.log("Installing dependencies..."); + + const baseDeps = [ + `react@${REACT_VERSION}`, + `react-dom@${REACT_VERSION}`, + `@seamless-auth/react@${SEAMLESS_VERSION}`, + ]; + + if (routerMode === "router") { + baseDeps.push(`react-router-dom@${ROUTER_VERSION}`); + } + + await installDeps(packageManager, baseDeps, root); + + console.log("Applying Seamless template..."); + + applyTemplate(root, routerMode); + + console.log("React project ready."); +} + +function applyTemplate(root: string, mode: string | symbol) { + const templateDir = path.join(TEMPLATE_ROOT, "frontend/react"); + + const indexCss = path.join(root, "src/index.css"); + if (fs.existsSync(indexCss)) { + fs.unlinkSync(indexCss); + } + + fs.copyFileSync( + path.join(templateDir, "App.css.tpl"), + path.join(root, "src/App.css"), + ); + + const appTemplate = mode === "router" ? "App.router.tsx.tpl" : "App.tsx.tpl"; + + fs.copyFileSync( + path.join(templateDir, appTemplate), + path.join(root, "src/App.tsx"), + ); + + fs.copyFileSync( + path.join(templateDir, "main.tsx.tpl"), + path.join(root, "src/main.tsx"), + ); + + fs.copyFileSync( + path.join(templateDir, "env.example.tpl"), + path.join(root, ".env.example"), + ); +} + +async function installDeps(pm: string, deps: string[], root: string) { + if (pm === "npm") { + await runCommand("npm", ["install", ...deps], root); + } else if (pm === "pnpm") { + await runCommand("pnpm", ["add", ...deps], root); + } else { + await runCommand("yarn", ["add", ...deps], root); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..3470e1b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,538 @@ +#!/usr/bin/env node + +import { initCommand } from "./commands/init.js"; + +const args = process.argv.slice(2); +const command = args[0]; + +async function run() { + switch (command) { + case "init": + await initCommand(); + break; + default: + console.log("Usage: create-seamless init"); + } +} + +run(); + +// import fs from "fs"; +// import path from "path"; +// import { execSync } from "child_process"; +// import { pipeline } from "stream"; +// import { promisify } from "util"; +// import { createWriteStream } from "fs"; +// import AdmZip from "adm-zip"; +// import { randomBytes } from "crypto"; +// import net from "net"; + +// const streamPipeline = promisify(pipeline); + +// const MIN_NODE_MAJOR = 18; +// const nodeMajor = Number(process.versions.node.split(".")[0]); + +// function ensureExecutable(filePath) { +// if (fs.existsSync(filePath)) { +// fs.chmodSync(filePath, 0o755); +// } +// } + +// function isPortAvailable(port) { +// return new Promise((resolve) => { +// const server = net +// .createServer() +// .once("error", () => resolve(false)) +// .once("listening", () => { +// server.close(); +// resolve(true); +// }) +// .listen(port); +// }); +// } + +// function printHelp() { +// console.log(` +// create-seamless + +// Scaffold a local Seamless Auth development environment. + +// Usage: +// npx create-seamless [project-name] [options] + +// Options: +// --auth Include the Seamless Auth server +// --api Include the Express API example +// --web Include the React web application +// --no-git Skip git initialization + +// --auth-port Auth server port (default: 5312) +// --api-port API server port (default: 3000) +// --web-port Web server port (default: 5001) + +// -h, --help Show this help message + +// If no component flags are provided, all components are included. + +// Docs: +// https://docs.seamlessauth.com +// `); +// } + +// if (nodeMajor < MIN_NODE_MAJOR) { +// console.error(` +// ❌ Seamless requires Node ${MIN_NODE_MAJOR}+. +// You are running Node ${process.versions.node} + +// Upgrade at https://nodejs.org +// `); +// process.exit(1); +// } + +// const args = process.argv.slice(2); + +// if (args.includes("-h") || args.includes("--help")) { +// printHelp(); +// process.exit(0); +// } +// const projectName = args.find((a) => !a.startsWith("--")) ?? "seamless-app"; + +// const hasFlag = (flag) => args.includes(`--${flag}`); +// const getFlag = (flag, fallback) => { +// const i = args.indexOf(`--${flag}`); +// return i !== -1 ? args[i + 1] : fallback; +// }; + +// const includeAuth = hasFlag("auth"); +// const includeWeb = hasFlag("web"); +// const includeApi = hasFlag("api"); +// const skipGit = hasFlag("no-git"); + +// const authPort = getFlag("auth-port", "5312"); +// const apiPort = getFlag("api-port", "3000"); +// const webPort = getFlag("web-port", "5001"); + +// const wantsSomething = includeAuth || includeWeb || includeApi; +// const AUTH = wantsSomething ? includeAuth : true; +// const WEB = wantsSomething ? includeWeb : true; +// const API = wantsSomething ? includeApi : true; + +// const REPOS = { +// auth: "fells-code/seamless-auth-api", +// web: "fells-code/seamless-auth-starter-react", +// api: "fells-code/seamless-auth-starter-express", +// }; + +// const GENERATED_README = (projectName) => `# ${projectName} + +// This project was generated with \`create-seamless\` and provides a fully local, +// open source authentication stack built on **Seamless Auth**. + +// It is designed for development environments where you want: + +// - Passwordless authentication +// - No hosted dependencies +// - No redirects or third-party auth services +// - A production-shaped local setup + +// --- + +// ## Project layout + +// \`\`\`text +// . +// ├─ auth/ # Seamless Auth open source server +// ├─ api/ # Backend API server (optional) +// ├─ web/ # Frontend web application (optional) +// ├─ Docker-compose.yml # Docker compose for one command spin up of dev environment +// └─ README.md +// \`\`\` + +// --- + +// ## Running the stack + +// ### Running with Docker (optional) + +// This project includes a Docker Compose configuration that allows you to run the +// entire Seamless Auth stack locally with a single command. + +// ### Requirements + +// * Docker +// * Docker Compose + +// ### Start the stack + +// From the project root, run: + +// \`\`\`bash +// docker compose up +// \`\`\` + +// This will start the following services in development mode: + +// * Postgres database +// * Seamless Auth server +// * API server +// * Web UI + +// All services are configured with hot reload. Changes to the source code will be +// picked up automatically. + +// ### Access the application + +// Once all services are running, open: + +// \`\`\` +// http://localhost:5001 +// \`\`\` + +// This is the main entry point for the web application. + +// ### Stopping the stack + +// To stop all services: + +// \`\`\`bash +// docker compose down +// \`\`\` + +// This will shut down all containers while preserving the local database volume. + +// Open separate terminals and run each service independently. + +// ### Auth server + +// \`\`\`bash +// cd auth +// npm run dev +// \`\`\` + +// Default port: \`5312\` + +// --- + +// ### API server + +// \`\`\`bash +// cd api +// npm run dev +// \`\`\` + +// Default port: \`3000\` + +// --- + +// ### Web application + +// \`\`\`bash +// cd web +// npm run dev +// \`\`\` + +// Default port: \`5001\` + +// --- + +// ## Documentation + +// Full Seamless Auth documentation: + +// https://seamlessauth.com/docs + +// --- + +// ## Included open source projects + +// - Seamless Auth Server +// https://github.com/fells-code/seamless-auth-server + +// - Seamless Auth React Starter +// https://github.com/fells-code/seamless-auth-starter-react + +// - Seamless Auth API Starter +// https://github.com/fells-code/seamless-auth-starter-express + +// --- + +// ## License + +// This generated project inherits the licenses of the open source components it +// includes. + +// Review each subproject for its specific license before deploying to production. +// `; + +// const GENERATED_DOCKER_COMPOSE = ` +// services: +// db: +// image: postgres:16 +// container_name: seamless-db +// ports: +// - "5432:5432" +// environment: +// POSTGRES_USER: myuser +// POSTGRES_PASSWORD: mypassword +// POSTGRES_DB: postgres +// volumes: +// - pgdata:/var/lib/postgresql/data +// - ./postgres_init:/docker-entrypoint-initdb.d +// healthcheck: +// test: ["CMD-SHELL", "pg_isready -U myuser -d postgres"] +// interval: 5s +// timeout: 5s +// retries: 5 + +// auth: +// container_name: seamless-auth +// build: +// context: ./auth +// dockerfile: Dockerfile.dev +// ports: +// - "${authPort}:${authPort}" +// env_file: +// - ./auth/.env +// environment: +// DB_HOST: db +// ISSUER: http://auth:${authPort} +// volumes: +// - ./auth:/app +// - /app/node_modules +// depends_on: +// db: +// condition: service_healthy + +// api: +// container_name: seamless-api +// build: ./api +// ports: +// - "${apiPort}:${apiPort}" +// env_file: +// - ./api/.env +// environment: +// AUTH_SERVER_URL: http://auth:${authPort} +// DB_HOST: db +// volumes: +// - ./api:/app +// - /app/node_modules +// depends_on: +// db: +// condition: service_healthy + +// web: +// container_name: seamless-web +// build: ./web +// ports: +// - "${webPort}:${webPort}" +// env_file: +// - ./web/.env +// volumes: +// - ./web:/app +// - /app/node_modules +// depends_on: +// - auth +// - api + +// volumes: +// pgdata: +// `; + +// function writeEnv(dir, values) { +// const env = Object.entries(values) +// .map(([k, v]) => `${k}=${v}`) +// .join("\n"); +// fs.writeFileSync(path.join(dir, ".env"), env + "\n"); +// } + +// async function downloadRepo(repo, dest) { +// const url = `https://codeload.github.com/${repo}/zip/refs/heads/main`; +// const res = await fetch(url); +// if (!res.ok || !res.body) { +// throw new Error(`Failed to download ${repo}`); +// } + +// const zipPath = path.join(dest, "_repo.zip"); +// await streamPipeline(res.body, createWriteStream(zipPath)); + +// const zip = new AdmZip(zipPath); +// zip.extractAllTo(dest, true); +// fs.unlinkSync(zipPath); + +// const inner = fs.readdirSync(dest).find((f) => f.endsWith("-main")); +// if (!inner) throw new Error("Unexpected repo structure"); + +// const innerPath = path.join(dest, inner); +// for (const file of fs.readdirSync(innerPath)) { +// fs.renameSync(path.join(innerPath, file), path.join(dest, file)); +// } +// fs.rmdirSync(innerPath); +// } + +// (async () => { +// const root = path.join(process.cwd(), projectName); + +// if (fs.existsSync(root)) { +// console.error("❌ Directory already exists."); +// process.exit(1); +// } + +// fs.mkdirSync(root); + +// console.log(`\nCreating Seamless project: ${projectName}\n`); +// const API_SERVICE_TOKEN = randomBytes(32).toString("hex"); + +// let dbHostPort = "5432"; + +// if (!(await isPortAvailable(5432))) { +// dbHostPort = "5433"; +// console.log(` +// ⚠️ Port 5432 is already in use on your machine. +// Using 5433 for Docker Postgres instead. +// `); +// } + +// if (AUTH) { +// const dir = path.join(root, "auth"); +// fs.mkdirSync(dir); +// console.log("Fetching Seamless Auth OSS..."); +// await downloadRepo(REPOS.auth, dir); + +// writeEnv(dir, { +// PORT: authPort, +// NODE_ENV: "development", + +// VERSION: "1.0.0", +// APP_NAME: "Seamless Auth Example", +// APP_ID: "local-dev", +// APP_ORIGIN: `http://localhost:${apiPort}`, +// ISSUER: `http://localhost:${authPort}`, + +// AUTH_MODE: "server", +// DEMO: "true", + +// DEFAULT_ROLES: "user,betaUser", +// AVAILABLE_ROLES: "user,admin,betaUser,team", + +// DB_LOGGING: "false", +// DB_HOST: "localhost", +// DB_PORT: dbHostPort, +// DB_NAME: "seamless_auth", +// DB_USER: "myuser", +// DB_PASSWORD: "mypassword", + +// ACCESS_TOKEN_TTL: "30m", +// REFRESH_TOKEN_TTL: "1h", +// RATE_LIMIT: "100", +// DELAY_AFTER: "50", + +// API_SERVICE_TOKEN: API_SERVICE_TOKEN, + +// JWKS_ACTIVE_KID: "dev-main", + +// RPID: "localhost", +// ORIGINS: `http://localhost:${webPort}`, +// }); +// } + +// if (API) { +// const dir = path.join(root, "api"); +// fs.mkdirSync(dir); +// console.log("Fetching API starter..."); +// await downloadRepo(REPOS.api, dir); + +// writeEnv(dir, { +// AUTH_SERVER_URL: `http://localhost:${authPort}`, +// APP_ORIGIN: `http://localhost:${apiPort}`, +// UI_ORIGIN: `http://localhost:${webPort}`, +// COOKIE_SIGNING_KEY: randomBytes(32).toString("hex"), +// API_SERVICE_TOKEN: API_SERVICE_TOKEN, +// JWKS_KID: "dev-main", + +// DB_HOST: "localhost", +// DB_PORT: dbHostPort, +// DB_NAME: "seamless_api", +// DB_USER: "myuser", +// DB_PASSWORD: "mypassword", + +// SQL_LOGGING: "false", +// }); +// } + +// if (WEB) { +// const dir = path.join(root, "web"); +// fs.mkdirSync(dir); +// console.log("Fetching Web starter..."); +// await downloadRepo(REPOS.web, dir); + +// writeEnv(dir, { +// VITE_AUTH_SERVER_URL: `http://localhost:${apiPort}/`, +// VITE_API_URL: `http://localhost:${apiPort}/`, +// PORT: webPort, +// }); +// } + +// fs.writeFileSync(path.join(root, "README.md"), GENERATED_README(projectName)); + +// if (!skipGit) { +// execSync("git init", { cwd: root }); +// } + +// if (AUTH && API && WEB) { +// const compose = GENERATED_DOCKER_COMPOSE.replace( +// '"5432:5432"', +// `"${dbHostPort}:5432"`, +// ); + +// fs.writeFileSync(path.join(root, "docker-compose.yml"), compose); +// } + +// if (AUTH) { +// const authScriptPath = path.join(root, "auth", "validateEnvs.sh"); +// ensureExecutable(authScriptPath); +// } + +// const pgDir = path.join(root, "postgres_init"); +// fs.mkdirSync(pgDir); +// fs.writeFileSync( +// path.join(pgDir, "init.sql"), +// ` +// CREATE DATABASE seamless_auth; +// CREATE DATABASE seamless_api; +// `, +// ); + +// console.log(` +// ╔════════════════════════════════════════╗ +// ║ S E A M L E S S ║ +// ╚════════════════════════════════════════╝ + +// Your local auth stack is ready. + +// Start development: + +// cd ${projectName} + +// # terminal 1 +// cd auth && npm i && npm run dev + +// # terminal 2 +// cd api && npm i && npm run dev + +// # terminal 3 +// cd web && npm i && npm run dev + +// or if using Docker + +// docker compose up + +// If you already have Postgres running locally, +// the generator may map Docker to port 5433 instead. + +// Docs: https://docs.seamlessauth.com/docs +// Happy hacking. 🚀 +// `); +// })().catch((err) => { +// console.error("\n❌ Error:", err.message); +// process.exit(1); +// }); diff --git a/src/inspectors/detectBackend.ts b/src/inspectors/detectBackend.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/inspectors/detectDatabase.ts b/src/inspectors/detectDatabase.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/inspectors/detectFrontend.ts b/src/inspectors/detectFrontend.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/prompts/emptyProject.ts b/src/prompts/emptyProject.ts new file mode 100644 index 0000000..5d90665 --- /dev/null +++ b/src/prompts/emptyProject.ts @@ -0,0 +1,23 @@ +import { select, confirm } from "@clack/prompts"; +import { generateReactVite } from "../generators/frontend/react.js"; + +export async function handleEmptyProject(context: any) { + const shouldCreate = await confirm({ + message: "No project detected. Start a new project here?", + }); + + if (!shouldCreate) return; + + const projectType = await select({ + message: "What would you like to create?", + options: [ + { value: "react-vite", label: "React (Vite)" }, + { value: "backend", label: "Backend API (coming soon)" }, + { value: "auth", label: "Seamless Auth Server (coming soon)" }, + ], + }); + + if (projectType === "react-vite") { + await generateReactVite(context); + } +} diff --git a/src/registry.ts b/src/registry.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/frontend/react/App.css.tpl b/templates/frontend/react/App.css.tpl new file mode 100644 index 0000000..37a7c9c --- /dev/null +++ b/templates/frontend/react/App.css.tpl @@ -0,0 +1,157 @@ +/* ========================= + Base Reset +========================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + height: 100%; + font-family: Inter, system-ui, -apple-system, sans-serif; + background: #0b0f14; + color: #e6edf3; + overflow: hidden; +} + +/* ========================= + Animated Grid Background +========================= */ +.grid-bg { + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px); + background-size: 60px 60px; + animation: gridDrift 80s linear infinite; + mask-image: radial-gradient(circle at 35% 25%, black 40%, transparent 75%); + z-index: 0; +} + +@keyframes gridDrift { + from { background-position: 0 0, 0 0; } + to { background-position: 120px 60px, 60px 120px; } +} + +/* ========================= + Layout +========================= */ +.app-container { + position: relative; + display: flex; + height: 100%; + z-index: 1; +} + +.left-panel, +.right-panel { + flex: 1; + display: flex; + padding: 5rem; +} + +.left-panel { + align-items: center; + justify-content: center; + border-right: 1px solid rgba(255,255,255,0.035); +} + +.right-panel { + flex-direction: column; + justify-content: center; + align-items: flex-start; + max-width: 640px; + opacity: 0.92; /* subtle tone difference */ +} + + +/* ========================= + Auth Card +========================= */ +.auth-card { + width: 100%; + max-width: 420px; + padding: 2.25rem; + border-radius: 10px; + background: #11161d; + border: 1px solid rgba(255,255,255,0.06); +} + +.auth-card h2 { + margin-bottom: 1.75rem; + font-weight: 600; + font-size: 1.4rem; +} + +/* Inputs */ +.auth-input { + width: 100%; + padding: 0.85rem; + margin-bottom: 1rem; + border-radius: 6px; + border: 1px solid rgba(255,255,255,0.08); + background: #151b23; + color: #e6edf3; + outline: none; +} + +.auth-input:focus { + border-color: #00e0a4; +} + +/* Buttons */ +.auth-button { + width: 100%; + padding: 0.85rem; + border-radius: 6px; + border: none; + background: #00e0a4; + color: #0b0f14; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease; +} + +.auth-button:hover { + background: #00c28e; +} + +/* ========================= + Brand Section +========================= */ +.brand-title { + font-size: 2.6rem; + font-weight: 700; + margin-bottom: 1.25rem; + letter-spacing: -0.5px; +} + +.brand-subtitle { + opacity: 0.65; + line-height: 1.6; + margin-bottom: 2rem; + max-width: 520px; +} + +.docs-link { + color: #00e0a4; + text-decoration: none; + font-weight: 500; +} + +.docs-link:hover { + text-decoration: underline; +} + +/* ========================= + Micro Label +========================= */ +.micro-label { + margin-top: 3rem; + font-size: 0.75rem; + letter-spacing: 1px; + text-transform: uppercase; + opacity: 0.35; +} diff --git a/templates/frontend/react/App.router.tsx.tpl b/templates/frontend/react/App.router.tsx.tpl new file mode 100644 index 0000000..64e5421 --- /dev/null +++ b/templates/frontend/react/App.router.tsx.tpl @@ -0,0 +1,77 @@ +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { AuthProvider, AuthRoutes, useAuth } from "@seamless-auth/react"; +import "./App.css"; + +const AUTH_SERVER = import.meta.env.VITE_AUTH_SERVER_URL; +const AUTH_MODE = import.meta.env.AUTH_MODE; + +function AuthSurface() { + const { isAuthenticated, user } = useAuth(); + + return ( + + {!isAuthenticated ? ( + } /> + ) : ( + +
+

You are signed in

+
+

Signed in as:

+ +
{JSON.stringify(user, null, 2)}
+
+
+ + } + /> + )} +
+ ); +} + +export default function App() { + return ( + <> +
+ +
+ + +
+
+ +
+
+
+
+ +
+
+
+

Seamless Auth

+
+ +

+ Modern passwordless authentication infrastructure built for + production-grade applications. +

+ + + Learn more → + + +
Powered by SeamlessAuth
+
+
+ + ); +} diff --git a/templates/frontend/react/App.tsx.tpl b/templates/frontend/react/App.tsx.tpl new file mode 100644 index 0000000..1cb7072 --- /dev/null +++ b/templates/frontend/react/App.tsx.tpl @@ -0,0 +1,67 @@ +import { AuthProvider, useAuth } from "@seamless-auth/react"; +import "./App.css"; + +const AUTH_SERVER = import.meta.env.VITE_AUTH_SERVER_URL; + +function LoginPanel() { + const { login, register, user, logout, isAuthenticated } = useAuth(); + + return ( +
+ {user && isAuthenticated ? ( + <> +

Welcome back

+
+

Signed in as:

+ +
{JSON.stringify(user, null, 2)}
+
+ + + ) : ( + <> +

Login or Register

+ +
+
+ + + )} +
+ ); +} + +export default function App() { + return ( + <> +
+
+
+ + + +
+
+

Seamless Auth

+

+ Passwordless authentication you will actually deploy to production. +

+ + Learn more → + +
Powered by SeamlessAuth
+
+
+ + ); +} diff --git a/templates/frontend/react/env.example.tpl b/templates/frontend/react/env.example.tpl new file mode 100644 index 0000000..29d908b --- /dev/null +++ b/templates/frontend/react/env.example.tpl @@ -0,0 +1,2 @@ +VITE_SEAMLESS_API=http://localhost:3000 +VITE_AUTH_MODE=server \ No newline at end of file diff --git a/templates/frontend/react/main.tsx.tpl b/templates/frontend/react/main.tsx.tpl new file mode 100644 index 0000000..fef7889 --- /dev/null +++ b/templates/frontend/react/main.tsx.tpl @@ -0,0 +1,9 @@ +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c3444b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src"], + "exclude": ["node_modules", "templates"] +} From 00bdbf7757b2f0f51ed31c3689328be66d3a300f Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 00:54:35 -0400 Subject: [PATCH 2/7] feat: working refactor and addtion of setting up seamless auth --- src/commands/init.ts | 86 +++- src/core/configure.ts | 56 ++ src/core/env.ts | 46 ++ src/core/fetch.ts | 12 + src/core/help.ts | 57 ++ src/core/jwks.ts | 20 + src/core/secrets.ts | 9 + src/generators/auth/auth.ts | 64 +++ src/generators/backend/express.ts | 24 + src/generators/docker/docker.ts | 259 ++++++++++ src/generators/frontend/react.ts | 100 +--- src/index.ts | 544 +------------------- src/prompts/emptyProject.ts | 4 +- src/prompts/projectSetup.ts | 96 ++++ src/utils/depsInstaller.ts | 21 + src/utils/repoUtils.ts | 32 ++ src/utils/writeEnv.ts | 10 + templates/frontend/react/App.css.tpl | 157 ------ templates/frontend/react/App.router.tsx.tpl | 77 --- templates/frontend/react/App.tsx.tpl | 67 --- templates/frontend/react/env.example.tpl | 2 - templates/frontend/react/main.tsx.tpl | 9 - 22 files changed, 815 insertions(+), 937 deletions(-) create mode 100644 src/core/configure.ts create mode 100644 src/core/env.ts create mode 100644 src/core/fetch.ts create mode 100644 src/core/help.ts create mode 100644 src/core/jwks.ts create mode 100644 src/core/secrets.ts create mode 100644 src/generators/auth/auth.ts create mode 100644 src/generators/backend/express.ts create mode 100644 src/generators/docker/docker.ts create mode 100644 src/prompts/projectSetup.ts create mode 100644 src/utils/depsInstaller.ts create mode 100644 src/utils/repoUtils.ts create mode 100644 src/utils/writeEnv.ts delete mode 100644 templates/frontend/react/App.css.tpl delete mode 100644 templates/frontend/react/App.router.tsx.tpl delete mode 100644 templates/frontend/react/App.tsx.tpl delete mode 100644 templates/frontend/react/env.example.tpl delete mode 100644 templates/frontend/react/main.tsx.tpl diff --git a/src/commands/init.ts b/src/commands/init.ts index ba6d001..90e8713 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,14 +1,84 @@ -import { inspectProject } from "../core/inspect.js"; -import { handleEmptyProject } from "../prompts/emptyProject.js"; -//import { handleExistingProject } from "../prompts/existingProject.js"; +import path from "path"; +import fs from "fs"; +import { runProjectSetupPrompts } from "../prompts/projectSetup.js"; +import { generateReactStarter } from "../generators/frontend/react.js"; +import { generateExpressStarter } from "../generators/backend/express.js"; +import { generateAuthServer } from "../generators/auth/auth.js"; +import { + configureApiEnv, + configureAuthEnv, + configureWebEnv, +} from "../core/configure.js"; +import { generateKid, generateSecret } from "../core/secrets.js"; +import { generateDockerCompose } from "../generators/docker/docker.js"; -export async function initCommand() { - const context = await inspectProject(process.cwd()); +export async function runCLI(projectName?: string) { + const cwd = process.cwd(); - if (!context.detected.anything) { - await handleEmptyProject(context); + let root = cwd; + + // If project name provided → create directory + if (projectName) { + root = path.join(cwd, projectName); + + if (fs.existsSync(root)) { + throw new Error(`Directory already exists: ${projectName}`); + } + + fs.mkdirSync(root); + console.log(`Creating project in ${root}`); + } + + const files = fs.readdirSync(root); + + const isEmpty = files.length === 0; + + if (!isEmpty) { + console.log("Existing project detected."); + console.log("Integration flow coming next."); return; } - //await handleExistingProject(context); + const answers = await runProjectSetupPrompts(); + + if (answers.web && answers.webFramework === "react") { + await generateReactStarter({ root }); + } + + if (answers.api && answers.apiFramework === "express") { + await generateExpressStarter({ root }); + } + let sharedConfig: any = {}; + + if (answers.authMode === "local") { + await generateAuthServer({ root }, "local"); + + sharedConfig = configureAuthEnv(root); + } else { + await generateAuthServer({ root }, "docker"); + + // still generate shared values + sharedConfig = { + apiToken: generateSecret(32), + kid: generateKid(), + }; + } + + if (answers.api) { + configureApiEnv(root, sharedConfig); + } + + if (answers.web) { + configureWebEnv(root); + } + + if (answers.useDocker) { + generateDockerCompose(root, { + authMode: answers.authMode, + includeApi: answers.api, + includeWeb: answers.web, + }); + } + + console.log("Setup complete."); } diff --git a/src/core/configure.ts b/src/core/configure.ts new file mode 100644 index 0000000..54aaff7 --- /dev/null +++ b/src/core/configure.ts @@ -0,0 +1,56 @@ +import path from "path"; +import fs from "fs"; +import { parseEnv, writeEnv } from "./env.js"; +import { generateKid, generateSecret } from "./secrets.js"; + +export function configureAuthEnv(root: string) { + const authEnvPath = path.join(root, "auth", ".env"); + + if (!fs.existsSync(authEnvPath)) return; + + const env = parseEnv(authEnvPath); + + const apiToken = generateSecret(32); + const kid = generateKid(); + + env.API_SERVICE_TOKEN = apiToken; + env.JWKS_ACTIVE_KID = kid; + + // Ensure correct URLs + env.ISSUER = "http://localhost:5312"; + env.APP_ORIGIN = "http://localhost:5173"; + + writeEnv(authEnvPath, env); + + return { + apiToken, + kid, + }; +} +export function configureApiEnv(root: string, shared: any) { + const apiEnvPath = path.join(root, "api", ".env"); + + if (!fs.existsSync(apiEnvPath)) return; + + const env = parseEnv(apiEnvPath); + + env.AUTH_SERVER_URL = "http://localhost:5312"; + env.API_SERVICE_TOKEN = shared.apiToken; + env.JWKS_KID = shared.kid; + env.COOKIE_SIGNING_KEY = generateSecret(32); + + writeEnv(apiEnvPath, env); +} + +export function configureWebEnv(root: string) { + const webEnvPath = path.join(root, "web", ".env"); + + if (!fs.existsSync(webEnvPath)) return; + + const env = parseEnv(webEnvPath); + + env.VITE_AUTH_SERVER_URL = "http://localhost:5312"; + env.VITE_API_URL = "http://localhost:3000"; + + writeEnv(webEnvPath, env); +} diff --git a/src/core/env.ts b/src/core/env.ts new file mode 100644 index 0000000..a97ed76 --- /dev/null +++ b/src/core/env.ts @@ -0,0 +1,46 @@ +import fs from "fs"; + +export function parseEnv(filePath: string): Record { + const content = fs.readFileSync(filePath, "utf-8"); + + const lines = content.split("\n"); + + const env: Record = {}; + + for (const line of lines) { + if (!line || line.startsWith("#")) continue; + + const [key, ...rest] = line.split("="); + if (!key) continue; + + env[key.trim()] = rest.join("=").trim(); + } + + return env; +} + +export function parseEnvString(content: string): Record { + const lines = content.split("\n"); + + const env: Record = {}; + + for (const line of lines) { + if (!line || line.startsWith("#")) continue; + + const [key, ...rest] = line.split("="); + + if (!key) continue; + + env[key.trim()] = rest.join("=").trim(); + } + + return env; +} + +export function writeEnv(filePath: string, env: Record) { + const content = Object.entries(env) + .map(([k, v]) => `${k}=${v}`) + .join("\n"); + + fs.writeFileSync(filePath, content + "\n"); +} diff --git a/src/core/fetch.ts b/src/core/fetch.ts new file mode 100644 index 0000000..4e076fe --- /dev/null +++ b/src/core/fetch.ts @@ -0,0 +1,12 @@ +export async function fetchEnvExample(): Promise { + const url = + "https://raw.githubusercontent.com/fells-code/seamless-auth-api/main/.env.example"; + + const res = await fetch(url); + + if (!res.ok) { + throw new Error("Failed to fetch auth env.example"); + } + + return await res.text(); +} diff --git a/src/core/help.ts b/src/core/help.ts new file mode 100644 index 0000000..2b60240 --- /dev/null +++ b/src/core/help.ts @@ -0,0 +1,57 @@ +export function printHelp() { + console.log(` +create-seamless + +Seamless Auth CLI — scaffold and integrate passwordless authentication. + +──────────────────────────────────────────── + +USAGE + + npx create-seamless + npx create-seamless + npx create-seamless -h + npx create-seamless --help + +──────────────────────────────────────────── + +BEHAVIOR + + npx create-seamless + + Without a name: + • If directory is empty → create a new project + • If directory has files → integrate SeamlessAuth + + npx create-seamless + + With a name: + • Creates a new directory + • Scaffolds a new project inside it + +──────────────────────────────────────────── + +WHAT YOU CAN BUILD + + • A web application starter with Seamless Auth + • An API server starter with Seamless Auth + • SeamlessAuth server (local or Docker) + +──────────────────────────────────────────── + +EXAMPLES + + npx create-seamless + → Interactive setup in current directory + + npx create-seamless my-app + → Create new project in ./my-app + +──────────────────────────────────────────── + +DOCS + + https://docs.seamlessauth.com + +`); +} diff --git a/src/core/jwks.ts b/src/core/jwks.ts new file mode 100644 index 0000000..fed23b0 --- /dev/null +++ b/src/core/jwks.ts @@ -0,0 +1,20 @@ +import { generateKeyPairSync } from "crypto"; + +export function generateJWKS() { + const { publicKey, privateKey } = generateKeyPairSync("rsa", { + modulusLength: 2048, + publicKeyEncoding: { + type: "spki", + format: "pem", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "pem", + }, + }); + + return { + publicKey, + privateKey, + }; +} diff --git a/src/core/secrets.ts b/src/core/secrets.ts new file mode 100644 index 0000000..09b04f7 --- /dev/null +++ b/src/core/secrets.ts @@ -0,0 +1,9 @@ +import { randomBytes } from "crypto"; + +export function generateSecret(length = 32) { + return randomBytes(length).toString("hex"); +} + +export function generateKid() { + return "dev-" + randomBytes(6).toString("hex"); +} diff --git a/src/generators/auth/auth.ts b/src/generators/auth/auth.ts new file mode 100644 index 0000000..ef8aad5 --- /dev/null +++ b/src/generators/auth/auth.ts @@ -0,0 +1,64 @@ +import fs from "fs"; +import path from "path"; +import { runCommand } from "../../core/exec.js"; +import { writeEnv } from "../../utils/writeEnv.js"; + +const AUTH_REPO = "https://github.com/fells-code/seamless-auth-api"; +const AUTH_PORT = 5312; + +export async function generateAuthServer( + context: any, + mode: "local" | "docker" | Symbol, +) { + const { root } = context; + + if (mode === "local") { + await setupLocalAuth(root); + } else { + await setupDockerAuth(root); + } +} + +async function setupLocalAuth(root: string) { + const authDir = path.join(root, "auth"); + + console.log("Cloning SeamlessAuth server..."); + + await runCommand("git", ["clone", AUTH_REPO, "auth"], root); + + console.log("Writing auth environment..."); + + writeEnv(authDir, { + PORT: AUTH_PORT, + NODE_ENV: "development", + AUTH_MODE: "server", + ISSUER: `http://localhost:${AUTH_PORT}`, + }); + + console.log("Auth server ready in /auth"); +} + +async function setupDockerAuth(root: string) { + console.log("Creating docker-compose for SeamlessAuth..."); + + const dockerCompose = ` +services: + auth: + image: ghcr.io/fells-code/seamless-auth-api:v0.1.2 + container_name: seamless-auth + ports: + - "5312:5312" + environment: + PORT: 5312 + NODE_ENV: development + AUTH_MODE: server + ISSUER: http://localhost:5312 +`; + + fs.writeFileSync( + path.join(root, "docker-compose.yml"), + dockerCompose.trim() + "\n", + ); + + console.log("Docker setup ready."); +} diff --git a/src/generators/backend/express.ts b/src/generators/backend/express.ts new file mode 100644 index 0000000..55c31ab --- /dev/null +++ b/src/generators/backend/express.ts @@ -0,0 +1,24 @@ +import path from "path"; +import { + cloneRepo, + removeGitDir, + copyEnvExample, +} from "../../utils/repoUtils.js"; + +const API_STARTER_REPO = + "https://github.com/fells-code/seamless-auth-starter-express.git"; + +export async function generateExpressStarter(context: { root: string }) { + const { root } = context; + + const apiDir = path.join(root, "api"); + + console.log("Cloning Seamless Auth Express starter..."); + + await cloneRepo(API_STARTER_REPO, apiDir); + + removeGitDir(apiDir); + copyEnvExample(apiDir); + + console.log("API starter ready."); +} diff --git a/src/generators/docker/docker.ts b/src/generators/docker/docker.ts new file mode 100644 index 0000000..524c4d7 --- /dev/null +++ b/src/generators/docker/docker.ts @@ -0,0 +1,259 @@ +import fs from "fs"; +import path from "path"; +import { fetchEnvExample } from "../../core/fetch.js"; +import { parseEnv, parseEnvString } from "../../core/env.js"; +import { generateKid, generateSecret } from "../../core/secrets.js"; +import { generateJWKS } from "../../core/jwks.js"; + +export async function generateDockerCompose( + root: string, + options: { + authMode: "local" | "docker"; + includeApi: boolean | Symbol; + includeWeb: boolean | Symbol; + }, +) { + const compose = await buildCompose(options, root); + + fs.writeFileSync( + path.join(root, "docker-compose.yml"), + compose.trim() + "\n", + ); + + console.log("Docker compose created."); +} + +async function buildCompose(options: any, root: string) { + const { authMode, includeApi, includeWeb } = options; + + const { service: authBlock, shared } = await authService(authMode, root); + + return ` +services: + db: + image: postgres:16 + container_name: seamless-db + ports: + - "5432:5432" + environment: + POSTGRES_USER: myuser + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: postgres + volumes: + - pgdata:/var/lib/postgresql/data + +${authBlock} + +${includeApi ? apiService(shared) : ""} + +${includeWeb ? webService() : ""} + +volumes: + pgdata: +`; +} + +async function authService(mode: "local" | "docker", root: string) { + if (mode === "local") { + return { + service: ` + auth: + container_name: seamless-auth + build: + context: ./auth + dockerfile: Dockerfile.dev + ports: + - "5312:5312" + env_file: + - ./auth/.env + environment: + DB_HOST: db + ISSUER: http://auth:5312 + volumes: + - ./auth:/app + - /app/node_modules + depends_on: + - db +`, + shared: extractSharedFromLocalEnv(root), + }; + } + + return await authServiceDocker(); +} +function apiService(shared: any) { + return ` + api: + container_name: api + build: ./api + ports: + - "3000:3000" + env_file: + - ./api/.env + environment: + AUTH_SERVER_URL: http://auth:5312 + UI_ORIGIN: http://localhost:5173 + DB_HOST: db + API_SERVICE_TOKEN: ${shared.apiToken} + JWKS_KID: ${shared.kid} + volumes: + - ./api:/app + - /app/node_modules + depends_on: + - db + - auth +`; +} + +function webService() { + return ` + web: + container_name: web + build: ./web + ports: + - "5173:5173" + env_file: + - ./web/.env + environment: + VITE_API_URL: http://localhost:3000 + VITE_AUTH_SERVER_URL: http://localhost:3000/ + volumes: + - ./web:/app + - /app/node_modules + depends_on: + - auth + - api +`; +} + +async function authServiceDocker() { + const raw = await fetchEnvExample(); + const parsed = parseEnvString(raw); + + const { env, shared } = buildAuthEnv(parsed, "docker"); + + const envBlock = envToDockerBlock(env); + + return { + service: ` + auth: + image: ghcr.io/fells-code/seamless-auth-api:v0.1.4 + container_name: seamless-auth + ports: + - "5312:5312" + environment: +${envBlock} + depends_on: + - db +`, + shared, + }; +} + +export function buildAuthEnv( + env: Record, + mode: "local" | "docker", +) { + const apiToken = generateSecret(32); + const kid = generateKid(); + + env.PORT = "5312"; + + env.NODE_ENV = mode === "docker" ? "production" : "development"; + + env.AUTH_MODE = "server"; + env.ISSUER = "http://auth:5312"; + + env.DB_HOST = "db"; + env.DB_PORT = "5432"; + + env.API_SERVICE_TOKEN = apiToken; + + const jwks = buildJWKSConfig(); + + env.SEAMLESS_JWKS_ACTIVE_KID = jwks.kid; + env.JWKS_ACTIVE_KID = jwks.kid; + + env[`SEAMLESS_JWKS_KEY_${jwks.kid}_PRIVATE`] = jwks.privateKey; + + env.JWKS_PUBLIC_KEYS = jwks.publicJwksJson; + + env.APP_ORIGIN = "http://localhost:5173"; + env.ORIGINS = "http://localhost:5173"; + + return { + env, + shared: { + apiToken, + kid, + }, + }; +} + +export function envToDockerBlock(env: Record) { + return Object.entries(env) + .map(([k, v]) => { + if (v.includes("\n")) { + return ` ${k}: |\n${indentMultiline(v, 8)}`; + } + + return ` ${k}: ${v}`; + }) + .join("\n"); +} + +function indentMultiline(value: string, spaces: number) { + const indent = " ".repeat(spaces); + return value + .split("\n") + .map((line) => `${indent}${line}`) + .join("\n"); +} + +export function extractSharedFromLocalEnv(root: string) { + const authEnvPath = path.join(root, "auth", ".env"); + + if (!fs.existsSync(authEnvPath)) { + throw new Error("Auth .env file not found. Cannot extract shared config."); + } + + const env = parseEnv(authEnvPath); + + const apiToken = env.API_SERVICE_TOKEN; + const kid = env.JWKS_ACTIVE_KID; + + if (!apiToken || !kid) { + throw new Error( + "Missing API_SERVICE_TOKEN or JWKS_ACTIVE_KID in auth .env", + ); + } + + return { + apiToken, + kid, + }; +} + +export function buildJWKSConfig() { + const kid = "main"; + + const { publicKey, privateKey } = generateJWKS(); + + return { + kid, + privateKey, + publicKey, + publicJwksJson: JSON.stringify( + { + keys: [ + { + kid, + pem: publicKey.replace(/\n/g, "\\n"), + }, + ], + }, + null, + 2, + ), + }; +} diff --git a/src/generators/frontend/react.ts b/src/generators/frontend/react.ts index 6165bd2..238e431 100644 --- a/src/generators/frontend/react.ts +++ b/src/generators/frontend/react.ts @@ -1,93 +1,23 @@ import path from "path"; -import fs from "fs"; -import { runCommand } from "../../core/exec.js"; -import { TEMPLATE_ROOT } from "../../core/paths.js"; -import { select } from "@clack/prompts"; +import { + cloneRepo, + removeGitDir, + copyEnvExample, +} from "../../utils/repoUtils.js"; -const VITE_VERSION = "7"; -const REACT_VERSION = "19"; -const ROUTER_VERSION = "7"; -const SEAMLESS_VERSION = "latest"; +const WEB_STARTER_REPO = + "https://github.com/fells-code/seamless-auth-starter-react.git"; -export async function generateReactVite(context: any) { - const { root, packageManager } = context; +export async function generateReactStarter(context: { root: string }) { + const { root } = context; + const webDir = path.join(root, "web"); - console.log("Scaffolding Vite project..."); + console.log("Cloning Seamless Auth React starter..."); - await runCommand( - packageManager, - packageManager === "npm" - ? ["create", `vite@${VITE_VERSION}`, ".", "--", "--template", "react-ts"] - : ["create", `vite@${VITE_VERSION}`, ".", "--template", "react-ts"], - root, - ); + await cloneRepo(WEB_STARTER_REPO, webDir); - const routerMode = await select({ - message: "Use built-in SeamlessAuth router + SDK routes?", - options: [ - { value: "router", label: "Yes (Router Mode)" }, - { value: "simple", label: "No (Custom Auth UI)" }, - ], - }); + removeGitDir(webDir); + copyEnvExample(webDir); - console.log("Installing dependencies..."); - - const baseDeps = [ - `react@${REACT_VERSION}`, - `react-dom@${REACT_VERSION}`, - `@seamless-auth/react@${SEAMLESS_VERSION}`, - ]; - - if (routerMode === "router") { - baseDeps.push(`react-router-dom@${ROUTER_VERSION}`); - } - - await installDeps(packageManager, baseDeps, root); - - console.log("Applying Seamless template..."); - - applyTemplate(root, routerMode); - - console.log("React project ready."); -} - -function applyTemplate(root: string, mode: string | symbol) { - const templateDir = path.join(TEMPLATE_ROOT, "frontend/react"); - - const indexCss = path.join(root, "src/index.css"); - if (fs.existsSync(indexCss)) { - fs.unlinkSync(indexCss); - } - - fs.copyFileSync( - path.join(templateDir, "App.css.tpl"), - path.join(root, "src/App.css"), - ); - - const appTemplate = mode === "router" ? "App.router.tsx.tpl" : "App.tsx.tpl"; - - fs.copyFileSync( - path.join(templateDir, appTemplate), - path.join(root, "src/App.tsx"), - ); - - fs.copyFileSync( - path.join(templateDir, "main.tsx.tpl"), - path.join(root, "src/main.tsx"), - ); - - fs.copyFileSync( - path.join(templateDir, "env.example.tpl"), - path.join(root, ".env.example"), - ); -} - -async function installDeps(pm: string, deps: string[], root: string) { - if (pm === "npm") { - await runCommand("npm", ["install", ...deps], root); - } else if (pm === "pnpm") { - await runCommand("pnpm", ["add", ...deps], root); - } else { - await runCommand("yarn", ["add", ...deps], root); - } + console.log("Web starter ready."); } diff --git a/src/index.ts b/src/index.ts index 3470e1b..bb846d0 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,538 +1,22 @@ #!/usr/bin/env node -import { initCommand } from "./commands/init.js"; +import { runCLI } from "./commands/init.js"; +import { printHelp } from "./core/help.js"; const args = process.argv.slice(2); -const command = args[0]; -async function run() { - switch (command) { - case "init": - await initCommand(); - break; - default: - console.log("Usage: create-seamless init"); - } -} - -run(); - -// import fs from "fs"; -// import path from "path"; -// import { execSync } from "child_process"; -// import { pipeline } from "stream"; -// import { promisify } from "util"; -// import { createWriteStream } from "fs"; -// import AdmZip from "adm-zip"; -// import { randomBytes } from "crypto"; -// import net from "net"; - -// const streamPipeline = promisify(pipeline); - -// const MIN_NODE_MAJOR = 18; -// const nodeMajor = Number(process.versions.node.split(".")[0]); - -// function ensureExecutable(filePath) { -// if (fs.existsSync(filePath)) { -// fs.chmodSync(filePath, 0o755); -// } -// } - -// function isPortAvailable(port) { -// return new Promise((resolve) => { -// const server = net -// .createServer() -// .once("error", () => resolve(false)) -// .once("listening", () => { -// server.close(); -// resolve(true); -// }) -// .listen(port); -// }); -// } - -// function printHelp() { -// console.log(` -// create-seamless - -// Scaffold a local Seamless Auth development environment. - -// Usage: -// npx create-seamless [project-name] [options] - -// Options: -// --auth Include the Seamless Auth server -// --api Include the Express API example -// --web Include the React web application -// --no-git Skip git initialization - -// --auth-port Auth server port (default: 5312) -// --api-port API server port (default: 3000) -// --web-port Web server port (default: 5001) - -// -h, --help Show this help message - -// If no component flags are provided, all components are included. - -// Docs: -// https://docs.seamlessauth.com -// `); -// } - -// if (nodeMajor < MIN_NODE_MAJOR) { -// console.error(` -// ❌ Seamless requires Node ${MIN_NODE_MAJOR}+. -// You are running Node ${process.versions.node} - -// Upgrade at https://nodejs.org -// `); -// process.exit(1); -// } - -// const args = process.argv.slice(2); - -// if (args.includes("-h") || args.includes("--help")) { -// printHelp(); -// process.exit(0); -// } -// const projectName = args.find((a) => !a.startsWith("--")) ?? "seamless-app"; - -// const hasFlag = (flag) => args.includes(`--${flag}`); -// const getFlag = (flag, fallback) => { -// const i = args.indexOf(`--${flag}`); -// return i !== -1 ? args[i + 1] : fallback; -// }; - -// const includeAuth = hasFlag("auth"); -// const includeWeb = hasFlag("web"); -// const includeApi = hasFlag("api"); -// const skipGit = hasFlag("no-git"); - -// const authPort = getFlag("auth-port", "5312"); -// const apiPort = getFlag("api-port", "3000"); -// const webPort = getFlag("web-port", "5001"); - -// const wantsSomething = includeAuth || includeWeb || includeApi; -// const AUTH = wantsSomething ? includeAuth : true; -// const WEB = wantsSomething ? includeWeb : true; -// const API = wantsSomething ? includeApi : true; - -// const REPOS = { -// auth: "fells-code/seamless-auth-api", -// web: "fells-code/seamless-auth-starter-react", -// api: "fells-code/seamless-auth-starter-express", -// }; - -// const GENERATED_README = (projectName) => `# ${projectName} - -// This project was generated with \`create-seamless\` and provides a fully local, -// open source authentication stack built on **Seamless Auth**. - -// It is designed for development environments where you want: - -// - Passwordless authentication -// - No hosted dependencies -// - No redirects or third-party auth services -// - A production-shaped local setup - -// --- - -// ## Project layout - -// \`\`\`text -// . -// ├─ auth/ # Seamless Auth open source server -// ├─ api/ # Backend API server (optional) -// ├─ web/ # Frontend web application (optional) -// ├─ Docker-compose.yml # Docker compose for one command spin up of dev environment -// └─ README.md -// \`\`\` - -// --- - -// ## Running the stack - -// ### Running with Docker (optional) - -// This project includes a Docker Compose configuration that allows you to run the -// entire Seamless Auth stack locally with a single command. - -// ### Requirements - -// * Docker -// * Docker Compose - -// ### Start the stack - -// From the project root, run: - -// \`\`\`bash -// docker compose up -// \`\`\` - -// This will start the following services in development mode: - -// * Postgres database -// * Seamless Auth server -// * API server -// * Web UI - -// All services are configured with hot reload. Changes to the source code will be -// picked up automatically. - -// ### Access the application - -// Once all services are running, open: - -// \`\`\` -// http://localhost:5001 -// \`\`\` - -// This is the main entry point for the web application. - -// ### Stopping the stack - -// To stop all services: - -// \`\`\`bash -// docker compose down -// \`\`\` - -// This will shut down all containers while preserving the local database volume. - -// Open separate terminals and run each service independently. - -// ### Auth server - -// \`\`\`bash -// cd auth -// npm run dev -// \`\`\` - -// Default port: \`5312\` - -// --- - -// ### API server - -// \`\`\`bash -// cd api -// npm run dev -// \`\`\` - -// Default port: \`3000\` - -// --- - -// ### Web application - -// \`\`\`bash -// cd web -// npm run dev -// \`\`\` +const firstArg = args[0]; -// Default port: \`5001\` - -// --- - -// ## Documentation - -// Full Seamless Auth documentation: - -// https://seamlessauth.com/docs - -// --- - -// ## Included open source projects - -// - Seamless Auth Server -// https://github.com/fells-code/seamless-auth-server - -// - Seamless Auth React Starter -// https://github.com/fells-code/seamless-auth-starter-react - -// - Seamless Auth API Starter -// https://github.com/fells-code/seamless-auth-starter-express - -// --- - -// ## License - -// This generated project inherits the licenses of the open source components it -// includes. - -// Review each subproject for its specific license before deploying to production. -// `; - -// const GENERATED_DOCKER_COMPOSE = ` -// services: -// db: -// image: postgres:16 -// container_name: seamless-db -// ports: -// - "5432:5432" -// environment: -// POSTGRES_USER: myuser -// POSTGRES_PASSWORD: mypassword -// POSTGRES_DB: postgres -// volumes: -// - pgdata:/var/lib/postgresql/data -// - ./postgres_init:/docker-entrypoint-initdb.d -// healthcheck: -// test: ["CMD-SHELL", "pg_isready -U myuser -d postgres"] -// interval: 5s -// timeout: 5s -// retries: 5 - -// auth: -// container_name: seamless-auth -// build: -// context: ./auth -// dockerfile: Dockerfile.dev -// ports: -// - "${authPort}:${authPort}" -// env_file: -// - ./auth/.env -// environment: -// DB_HOST: db -// ISSUER: http://auth:${authPort} -// volumes: -// - ./auth:/app -// - /app/node_modules -// depends_on: -// db: -// condition: service_healthy - -// api: -// container_name: seamless-api -// build: ./api -// ports: -// - "${apiPort}:${apiPort}" -// env_file: -// - ./api/.env -// environment: -// AUTH_SERVER_URL: http://auth:${authPort} -// DB_HOST: db -// volumes: -// - ./api:/app -// - /app/node_modules -// depends_on: -// db: -// condition: service_healthy - -// web: -// container_name: seamless-web -// build: ./web -// ports: -// - "${webPort}:${webPort}" -// env_file: -// - ./web/.env -// volumes: -// - ./web:/app -// - /app/node_modules -// depends_on: -// - auth -// - api - -// volumes: -// pgdata: -// `; - -// function writeEnv(dir, values) { -// const env = Object.entries(values) -// .map(([k, v]) => `${k}=${v}`) -// .join("\n"); -// fs.writeFileSync(path.join(dir, ".env"), env + "\n"); -// } - -// async function downloadRepo(repo, dest) { -// const url = `https://codeload.github.com/${repo}/zip/refs/heads/main`; -// const res = await fetch(url); -// if (!res.ok || !res.body) { -// throw new Error(`Failed to download ${repo}`); -// } - -// const zipPath = path.join(dest, "_repo.zip"); -// await streamPipeline(res.body, createWriteStream(zipPath)); - -// const zip = new AdmZip(zipPath); -// zip.extractAllTo(dest, true); -// fs.unlinkSync(zipPath); - -// const inner = fs.readdirSync(dest).find((f) => f.endsWith("-main")); -// if (!inner) throw new Error("Unexpected repo structure"); - -// const innerPath = path.join(dest, inner); -// for (const file of fs.readdirSync(innerPath)) { -// fs.renameSync(path.join(innerPath, file), path.join(dest, file)); -// } -// fs.rmdirSync(innerPath); -// } - -// (async () => { -// const root = path.join(process.cwd(), projectName); - -// if (fs.existsSync(root)) { -// console.error("❌ Directory already exists."); -// process.exit(1); -// } - -// fs.mkdirSync(root); - -// console.log(`\nCreating Seamless project: ${projectName}\n`); -// const API_SERVICE_TOKEN = randomBytes(32).toString("hex"); - -// let dbHostPort = "5432"; - -// if (!(await isPortAvailable(5432))) { -// dbHostPort = "5433"; -// console.log(` -// ⚠️ Port 5432 is already in use on your machine. -// Using 5433 for Docker Postgres instead. -// `); -// } - -// if (AUTH) { -// const dir = path.join(root, "auth"); -// fs.mkdirSync(dir); -// console.log("Fetching Seamless Auth OSS..."); -// await downloadRepo(REPOS.auth, dir); - -// writeEnv(dir, { -// PORT: authPort, -// NODE_ENV: "development", - -// VERSION: "1.0.0", -// APP_NAME: "Seamless Auth Example", -// APP_ID: "local-dev", -// APP_ORIGIN: `http://localhost:${apiPort}`, -// ISSUER: `http://localhost:${authPort}`, - -// AUTH_MODE: "server", -// DEMO: "true", - -// DEFAULT_ROLES: "user,betaUser", -// AVAILABLE_ROLES: "user,admin,betaUser,team", - -// DB_LOGGING: "false", -// DB_HOST: "localhost", -// DB_PORT: dbHostPort, -// DB_NAME: "seamless_auth", -// DB_USER: "myuser", -// DB_PASSWORD: "mypassword", - -// ACCESS_TOKEN_TTL: "30m", -// REFRESH_TOKEN_TTL: "1h", -// RATE_LIMIT: "100", -// DELAY_AFTER: "50", - -// API_SERVICE_TOKEN: API_SERVICE_TOKEN, - -// JWKS_ACTIVE_KID: "dev-main", - -// RPID: "localhost", -// ORIGINS: `http://localhost:${webPort}`, -// }); -// } - -// if (API) { -// const dir = path.join(root, "api"); -// fs.mkdirSync(dir); -// console.log("Fetching API starter..."); -// await downloadRepo(REPOS.api, dir); - -// writeEnv(dir, { -// AUTH_SERVER_URL: `http://localhost:${authPort}`, -// APP_ORIGIN: `http://localhost:${apiPort}`, -// UI_ORIGIN: `http://localhost:${webPort}`, -// COOKIE_SIGNING_KEY: randomBytes(32).toString("hex"), -// API_SERVICE_TOKEN: API_SERVICE_TOKEN, -// JWKS_KID: "dev-main", - -// DB_HOST: "localhost", -// DB_PORT: dbHostPort, -// DB_NAME: "seamless_api", -// DB_USER: "myuser", -// DB_PASSWORD: "mypassword", - -// SQL_LOGGING: "false", -// }); -// } - -// if (WEB) { -// const dir = path.join(root, "web"); -// fs.mkdirSync(dir); -// console.log("Fetching Web starter..."); -// await downloadRepo(REPOS.web, dir); - -// writeEnv(dir, { -// VITE_AUTH_SERVER_URL: `http://localhost:${apiPort}/`, -// VITE_API_URL: `http://localhost:${apiPort}/`, -// PORT: webPort, -// }); -// } - -// fs.writeFileSync(path.join(root, "README.md"), GENERATED_README(projectName)); - -// if (!skipGit) { -// execSync("git init", { cwd: root }); -// } - -// if (AUTH && API && WEB) { -// const compose = GENERATED_DOCKER_COMPOSE.replace( -// '"5432:5432"', -// `"${dbHostPort}:5432"`, -// ); - -// fs.writeFileSync(path.join(root, "docker-compose.yml"), compose); -// } - -// if (AUTH) { -// const authScriptPath = path.join(root, "auth", "validateEnvs.sh"); -// ensureExecutable(authScriptPath); -// } - -// const pgDir = path.join(root, "postgres_init"); -// fs.mkdirSync(pgDir); -// fs.writeFileSync( -// path.join(pgDir, "init.sql"), -// ` -// CREATE DATABASE seamless_auth; -// CREATE DATABASE seamless_api; -// `, -// ); - -// console.log(` -// ╔════════════════════════════════════════╗ -// ║ S E A M L E S S ║ -// ╚════════════════════════════════════════╝ - -// Your local auth stack is ready. - -// Start development: - -// cd ${projectName} - -// # terminal 1 -// cd auth && npm i && npm run dev - -// # terminal 2 -// cd api && npm i && npm run dev - -// # terminal 3 -// cd web && npm i && npm run dev - -// or if using Docker - -// docker compose up +async function main() { + if (firstArg === "-h" || firstArg === "--help") { + printHelp(); + return; + } -// If you already have Postgres running locally, -// the generator may map Docker to port 5433 instead. + await runCLI(firstArg); +} -// Docs: https://docs.seamlessauth.com/docs -// Happy hacking. 🚀 -// `); -// })().catch((err) => { -// console.error("\n❌ Error:", err.message); -// process.exit(1); -// }); +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); diff --git a/src/prompts/emptyProject.ts b/src/prompts/emptyProject.ts index 5d90665..49bc575 100644 --- a/src/prompts/emptyProject.ts +++ b/src/prompts/emptyProject.ts @@ -1,5 +1,5 @@ import { select, confirm } from "@clack/prompts"; -import { generateReactVite } from "../generators/frontend/react.js"; +import { generateReactStarter } from "../generators/frontend/react.js"; export async function handleEmptyProject(context: any) { const shouldCreate = await confirm({ @@ -18,6 +18,6 @@ export async function handleEmptyProject(context: any) { }); if (projectType === "react-vite") { - await generateReactVite(context); + await generateReactStarter(context); } } diff --git a/src/prompts/projectSetup.ts b/src/prompts/projectSetup.ts new file mode 100644 index 0000000..5391f0f --- /dev/null +++ b/src/prompts/projectSetup.ts @@ -0,0 +1,96 @@ +import { confirm, select, text } from "@clack/prompts"; + +type WebFramework = "react" | null; +type ApiFramework = "express" | null; +type AuthMode = "local" | "docker"; + +export async function runProjectSetupPrompts() { + // ----------------------------- + // Web + // ----------------------------- + const web = await confirm({ + message: "Do you want to create a web application?", + }); + + let webFramework: WebFramework = null; + + if (web) { + const result = await select({ + message: "Which framework?", + options: [ + { value: "react", label: "React (Vite)" }, + { value: "next", label: "Next.js (coming soon)", disabled: true }, + { value: "vue", label: "Vue (coming soon)", disabled: true }, + { value: "angular", label: "Angular (coming soon)", disabled: true }, + ], + }); + + webFramework = result as WebFramework; + } + + // ----------------------------- + // API + // ----------------------------- + const api = await confirm({ + message: "Do you want to create an API server?", + }); + + let apiFramework: ApiFramework = null; + + if (api) { + const result = await select({ + message: "Which backend?", + options: [ + { value: "express", label: "Express" }, + { value: "next", label: "Next.js (coming soon)", disabled: true }, + { value: "fastify", label: "Fastify (coming soon)", disabled: true }, + { value: "fast-api", label: "FastAPI (coming soon)", disabled: true }, + ], + }); + + apiFramework = result as ApiFramework; + } + + // ----------------------------- + // Auth Mode + // ----------------------------- + const authMode = (await select({ + message: "How would you like to run SeamlessAuth?", + options: [ + { + value: "local", + label: "Local development server (npm run dev yourself)", + }, + { + value: "docker", + label: "Docker container (recommended - just run docker compose up)", + }, + ], + })) as AuthMode; + + // ----------------------------- + // Docker + // ----------------------------- + let useDocker = await confirm({ + message: "Do you want to run your stack with Docker?", + }); + + if (authMode === "docker" && !useDocker) { + console.log( + "\nAuth server requires Docker — enabling Docker mode automatically.\n", + ); + useDocker = true; + } + + // ----------------------------- + // Final Config + // ----------------------------- + return { + web, + webFramework, + api, + apiFramework, + authMode, + useDocker, + }; +} diff --git a/src/utils/depsInstaller.ts b/src/utils/depsInstaller.ts new file mode 100644 index 0000000..024d0a1 --- /dev/null +++ b/src/utils/depsInstaller.ts @@ -0,0 +1,21 @@ +import { runCommand } from "../core/exec.js"; + +export async function installDeps(pm: string, deps: string[], root: string) { + if (pm === "npm") { + await runCommand("npm", ["install", ...deps], root); + } else if (pm === "pnpm") { + await runCommand("pnpm", ["add", ...deps], root); + } else { + await runCommand("yarn", ["add", ...deps], root); + } +} + +export async function installDevDeps(pm: string, deps: string[], root: string) { + if (pm === "npm") { + await runCommand("npm", ["install", "-D", ...deps], root); + } else if (pm === "pnpm") { + await runCommand("pnpm", ["add", "-D", ...deps], root); + } else { + await runCommand("yarn", ["add", "-D", ...deps], root); + } +} diff --git a/src/utils/repoUtils.ts b/src/utils/repoUtils.ts new file mode 100644 index 0000000..412584b --- /dev/null +++ b/src/utils/repoUtils.ts @@ -0,0 +1,32 @@ +import fs from "fs"; +import path from "path"; +import { runCommand } from "../core/exec.js"; + +export async function cloneRepo(repoUrl: string, dest: string) { + const parentDir = path.dirname(dest); + const folderName = path.basename(dest); + + fs.mkdirSync(parentDir, { recursive: true }); + + await runCommand( + "git", + ["clone", "--depth", "1", repoUrl, folderName], + parentDir, + ); +} + +export function removeGitDir(projectRoot: string) { + const gitDir = path.join(projectRoot, ".git"); + if (fs.existsSync(gitDir)) { + fs.rmSync(gitDir, { recursive: true, force: true }); + } +} + +export function copyEnvExample(projectRoot: string) { + const envExample = path.join(projectRoot, ".env.example"); + const env = path.join(projectRoot, ".env"); + + if (fs.existsSync(envExample) && !fs.existsSync(env)) { + fs.copyFileSync(envExample, env); + } +} diff --git a/src/utils/writeEnv.ts b/src/utils/writeEnv.ts new file mode 100644 index 0000000..d175219 --- /dev/null +++ b/src/utils/writeEnv.ts @@ -0,0 +1,10 @@ +import path from "path"; +import fs from "fs"; + +export function writeEnv(dir: string, values: Record) { + const env = Object.entries(values) + .map(([k, v]) => `${k}=${v}`) + .join("\n"); + + fs.writeFileSync(path.join(dir, ".env"), env + "\n"); +} diff --git a/templates/frontend/react/App.css.tpl b/templates/frontend/react/App.css.tpl deleted file mode 100644 index 37a7c9c..0000000 --- a/templates/frontend/react/App.css.tpl +++ /dev/null @@ -1,157 +0,0 @@ -/* ========================= - Base Reset -========================= */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body, #root { - height: 100%; - font-family: Inter, system-ui, -apple-system, sans-serif; - background: #0b0f14; - color: #e6edf3; - overflow: hidden; -} - -/* ========================= - Animated Grid Background -========================= */ -.grid-bg { - position: fixed; - inset: 0; - background-image: - linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px); - background-size: 60px 60px; - animation: gridDrift 80s linear infinite; - mask-image: radial-gradient(circle at 35% 25%, black 40%, transparent 75%); - z-index: 0; -} - -@keyframes gridDrift { - from { background-position: 0 0, 0 0; } - to { background-position: 120px 60px, 60px 120px; } -} - -/* ========================= - Layout -========================= */ -.app-container { - position: relative; - display: flex; - height: 100%; - z-index: 1; -} - -.left-panel, -.right-panel { - flex: 1; - display: flex; - padding: 5rem; -} - -.left-panel { - align-items: center; - justify-content: center; - border-right: 1px solid rgba(255,255,255,0.035); -} - -.right-panel { - flex-direction: column; - justify-content: center; - align-items: flex-start; - max-width: 640px; - opacity: 0.92; /* subtle tone difference */ -} - - -/* ========================= - Auth Card -========================= */ -.auth-card { - width: 100%; - max-width: 420px; - padding: 2.25rem; - border-radius: 10px; - background: #11161d; - border: 1px solid rgba(255,255,255,0.06); -} - -.auth-card h2 { - margin-bottom: 1.75rem; - font-weight: 600; - font-size: 1.4rem; -} - -/* Inputs */ -.auth-input { - width: 100%; - padding: 0.85rem; - margin-bottom: 1rem; - border-radius: 6px; - border: 1px solid rgba(255,255,255,0.08); - background: #151b23; - color: #e6edf3; - outline: none; -} - -.auth-input:focus { - border-color: #00e0a4; -} - -/* Buttons */ -.auth-button { - width: 100%; - padding: 0.85rem; - border-radius: 6px; - border: none; - background: #00e0a4; - color: #0b0f14; - font-weight: 600; - cursor: pointer; - transition: background 0.15s ease; -} - -.auth-button:hover { - background: #00c28e; -} - -/* ========================= - Brand Section -========================= */ -.brand-title { - font-size: 2.6rem; - font-weight: 700; - margin-bottom: 1.25rem; - letter-spacing: -0.5px; -} - -.brand-subtitle { - opacity: 0.65; - line-height: 1.6; - margin-bottom: 2rem; - max-width: 520px; -} - -.docs-link { - color: #00e0a4; - text-decoration: none; - font-weight: 500; -} - -.docs-link:hover { - text-decoration: underline; -} - -/* ========================= - Micro Label -========================= */ -.micro-label { - margin-top: 3rem; - font-size: 0.75rem; - letter-spacing: 1px; - text-transform: uppercase; - opacity: 0.35; -} diff --git a/templates/frontend/react/App.router.tsx.tpl b/templates/frontend/react/App.router.tsx.tpl deleted file mode 100644 index 64e5421..0000000 --- a/templates/frontend/react/App.router.tsx.tpl +++ /dev/null @@ -1,77 +0,0 @@ -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import { AuthProvider, AuthRoutes, useAuth } from "@seamless-auth/react"; -import "./App.css"; - -const AUTH_SERVER = import.meta.env.VITE_AUTH_SERVER_URL; -const AUTH_MODE = import.meta.env.AUTH_MODE; - -function AuthSurface() { - const { isAuthenticated, user } = useAuth(); - - return ( - - {!isAuthenticated ? ( - } /> - ) : ( - -
-

You are signed in

-
-

Signed in as:

- -
{JSON.stringify(user, null, 2)}
-
-
- - } - /> - )} -
- ); -} - -export default function App() { - return ( - <> -
- -
- - -
-
- -
-
-
-
- -
-
-
-

Seamless Auth

-
- -

- Modern passwordless authentication infrastructure built for - production-grade applications. -

- - - Learn more → - - -
Powered by SeamlessAuth
-
-
- - ); -} diff --git a/templates/frontend/react/App.tsx.tpl b/templates/frontend/react/App.tsx.tpl deleted file mode 100644 index 1cb7072..0000000 --- a/templates/frontend/react/App.tsx.tpl +++ /dev/null @@ -1,67 +0,0 @@ -import { AuthProvider, useAuth } from "@seamless-auth/react"; -import "./App.css"; - -const AUTH_SERVER = import.meta.env.VITE_AUTH_SERVER_URL; - -function LoginPanel() { - const { login, register, user, logout, isAuthenticated } = useAuth(); - - return ( -
- {user && isAuthenticated ? ( - <> -

Welcome back

-
-

Signed in as:

- -
{JSON.stringify(user, null, 2)}
-
- - - ) : ( - <> -

Login or Register

- -
-
- - - )} -
- ); -} - -export default function App() { - return ( - <> -
-
-
- - - -
-
-

Seamless Auth

-

- Passwordless authentication you will actually deploy to production. -

- - Learn more → - -
Powered by SeamlessAuth
-
-
- - ); -} diff --git a/templates/frontend/react/env.example.tpl b/templates/frontend/react/env.example.tpl deleted file mode 100644 index 29d908b..0000000 --- a/templates/frontend/react/env.example.tpl +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SEAMLESS_API=http://localhost:3000 -VITE_AUTH_MODE=server \ No newline at end of file diff --git a/templates/frontend/react/main.tsx.tpl b/templates/frontend/react/main.tsx.tpl deleted file mode 100644 index fef7889..0000000 --- a/templates/frontend/react/main.tsx.tpl +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import App from "./App" - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - -) From aae73d423d5f6ed8c70d4ca68e72c58a23b39895 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 01:31:04 -0400 Subject: [PATCH 3/7] feat: working docker image and local auth branches --- package-lock.json | 12 +++- package.json | 5 +- src/commands/init.ts | 12 +++- src/core/output.ts | 115 ++++++++++++++++++++++++++++++++ src/generators/docker/docker.ts | 92 ++++++++++++++----------- src/prompts/projectSetup.ts | 19 +----- 6 files changed, 194 insertions(+), 61 deletions(-) create mode 100644 src/core/output.ts diff --git a/package-lock.json b/package-lock.json index fe3e02a..8747a87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "AGPL-3.0-only", "dependencies": { "@clack/prompts": "^1.0.1", - "adm-zip": "^0.5.16" + "adm-zip": "^0.5.16", + "kleur": "^4.1.5" }, "bin": { "create-seamless": "dist/index.js" @@ -619,6 +620,15 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index 18246fd..9730331 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ ], "dependencies": { "@clack/prompts": "^1.0.1", - "adm-zip": "^0.5.16" + "adm-zip": "^0.5.16", + "kleur": "^4.1.5" }, "devDependencies": { "@types/adm-zip": "^0.5.7", @@ -38,4 +39,4 @@ "tsx": "^4.21.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/src/commands/init.ts b/src/commands/init.ts index 90e8713..e672fe5 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,13 +11,13 @@ import { } from "../core/configure.js"; import { generateKid, generateSecret } from "../core/secrets.js"; import { generateDockerCompose } from "../generators/docker/docker.js"; +import { printSuccessOutput } from "../core/output.js"; export async function runCLI(projectName?: string) { const cwd = process.cwd(); let root = cwd; - // If project name provided → create directory if (projectName) { root = path.join(cwd, projectName); @@ -57,7 +57,6 @@ export async function runCLI(projectName?: string) { } else { await generateAuthServer({ root }, "docker"); - // still generate shared values sharedConfig = { apiToken: generateSecret(32), kid: generateKid(), @@ -80,5 +79,12 @@ export async function runCLI(projectName?: string) { }); } - console.log("Setup complete."); + printSuccessOutput({ + projectName, + root, + webFramework: answers.webFramework, + apiFramework: answers.apiFramework, + authMode: answers.authMode, + useDocker: answers.useDocker, + }); } diff --git a/src/core/output.ts b/src/core/output.ts new file mode 100644 index 0000000..df455cd --- /dev/null +++ b/src/core/output.ts @@ -0,0 +1,115 @@ +import kleur from "kleur"; + +export function printSuccessOutput(config: { + projectName?: string; + root: string; + webFramework: string | null; + apiFramework: string | null; + authMode: "local" | "docker"; + useDocker: boolean | symbol; +}) { + const { projectName, webFramework, apiFramework, authMode, useDocker } = + config; + + const title = kleur.bold().cyan("SEAMLESS"); + + console.log(` +╔════════════════════════════════════════╗ +║ ${title} ║ +╚════════════════════════════════════════╝ +`); + + console.log(kleur.green("✔ Your SeamlessAuth project is ready.\n")); + + if (projectName) { + console.log(kleur.dim("Project created in: ") + kleur.bold(projectName)); + console.log(kleur.cyan(`cd ${projectName}\n`)); + } + + console.log(kleur.bold("Project includes:\n")); + + if (webFramework) { + console.log( + " • " + + kleur.white("Web app") + + kleur.dim(` (${formatFramework(webFramework)})`), + ); + } + + if (apiFramework) { + console.log( + " • " + + kleur.white("API server") + + kleur.dim(` (${formatFramework(apiFramework)})`), + ); + } + + console.log( + " • " + + kleur.white("Auth server") + + kleur.dim(authMode === "local" ? " (local source)" : " (Docker image)"), + ); + + console.log(""); + + console.log(kleur.bold("Getting started:\n")); + + if (useDocker) { + console.log(kleur.cyan(" docker compose up\n")); + } else { + if (authMode === "local") { + console.log(kleur.dim("# Auth server")); + console.log(" cd auth && npm install && npm run dev\n"); + } + + if (apiFramework) { + console.log(kleur.dim("# API server")); + console.log(" cd api && npm install && npm run dev\n"); + } + + if (webFramework) { + console.log(kleur.dim("# Web app")); + console.log(" cd web && npm install && npm run dev\n"); + } + } + + console.log(kleur.bold("Available services:\n")); + + console.log(" Auth: " + kleur.cyan("http://localhost:5312")); + + if (apiFramework) { + console.log(" API: " + kleur.cyan("http://localhost:3000")); + } + + if (webFramework) { + console.log(" Web: " + kleur.cyan("http://localhost:5173")); + } + + console.log(""); + + console.log(kleur.bold("Notes:\n")); + + console.log(kleur.dim(" • Web connects to API automatically")); + console.log(kleur.dim(" • API connects to Auth automatically")); + console.log(kleur.dim(" • All secrets and keys are pre-configured\n")); + + console.log( + kleur.dim("Docs: ") + kleur.cyan("https://docs.seamlessauth.com\n"), + ); + + console.log(kleur.bold().green("Happy hacking. 🚀\n")); +} + +function formatFramework(name: string) { + const map: Record = { + react: "React", + express: "Express", + angular: "Angular", + next: "Next.js", + fastapi: "FastAPI", + fastify: "Fastify", + vue: "Vue", + }; + + return map[name] || name; +} diff --git a/src/generators/docker/docker.ts b/src/generators/docker/docker.ts index 524c4d7..6ea10a9 100644 --- a/src/generators/docker/docker.ts +++ b/src/generators/docker/docker.ts @@ -55,6 +55,8 @@ volumes: async function authService(mode: "local" | "docker", root: string) { if (mode === "local") { + const shared = await configureAuthLocalEnv(root); + return { service: ` auth: @@ -75,7 +77,7 @@ async function authService(mode: "local" | "docker", root: string) { depends_on: - db `, - shared: extractSharedFromLocalEnv(root), + shared, }; } @@ -150,35 +152,36 @@ ${envBlock} }; } -export function buildAuthEnv( - env: Record, - mode: "local" | "docker", -) { +function buildAuthEnv(env: Record, mode: "local" | "docker") { const apiToken = generateSecret(32); - const kid = generateKid(); env.PORT = "5312"; - env.NODE_ENV = mode === "docker" ? "production" : "development"; env.AUTH_MODE = "server"; env.ISSUER = "http://auth:5312"; - env.DB_HOST = "db"; + env.DB_HOST = mode === "docker" ? "db" : "localhost"; env.DB_PORT = "5432"; env.API_SERVICE_TOKEN = apiToken; - const jwks = buildJWKSConfig(); + let kid = "main"; + env.JWKS_ACTIVE_KID = kid; - env.SEAMLESS_JWKS_ACTIVE_KID = jwks.kid; - env.JWKS_ACTIVE_KID = jwks.kid; + if (mode === "docker") { + const jwks = buildJWKSConfig(); - env[`SEAMLESS_JWKS_KEY_${jwks.kid}_PRIVATE`] = jwks.privateKey; + kid = jwks.kid; - env.JWKS_PUBLIC_KEYS = jwks.publicJwksJson; + env.SEAMLESS_JWKS_ACTIVE_KID = jwks.kid; + env.JWKS_ACTIVE_KID = jwks.kid; + + env[`SEAMLESS_JWKS_KEY_${jwks.kid}_PRIVATE`] = jwks.privateKey; + env.JWKS_PUBLIC_KEYS = jwks.publicJwksJson; + } - env.APP_ORIGIN = "http://localhost:5173"; + env.APP_ORIGIN = "http://localhost:3000"; env.ORIGINS = "http://localhost:5173"; return { @@ -210,30 +213,6 @@ function indentMultiline(value: string, spaces: number) { .join("\n"); } -export function extractSharedFromLocalEnv(root: string) { - const authEnvPath = path.join(root, "auth", ".env"); - - if (!fs.existsSync(authEnvPath)) { - throw new Error("Auth .env file not found. Cannot extract shared config."); - } - - const env = parseEnv(authEnvPath); - - const apiToken = env.API_SERVICE_TOKEN; - const kid = env.JWKS_ACTIVE_KID; - - if (!apiToken || !kid) { - throw new Error( - "Missing API_SERVICE_TOKEN or JWKS_ACTIVE_KID in auth .env", - ); - } - - return { - apiToken, - kid, - }; -} - export function buildJWKSConfig() { const kid = "main"; @@ -257,3 +236,40 @@ export function buildJWKSConfig() { ), }; } + +export async function configureAuthLocalEnv(root: string) { + const authDir = path.join(root, "auth"); + const envExamplePath = path.join(authDir, ".env.example"); + const envPath = path.join(authDir, ".env"); + + if (!fs.existsSync(envExamplePath)) { + throw new Error(".env.example not found in auth directory"); + } + + const raw = fs.readFileSync(envExamplePath, "utf-8"); + + const parsed = parseEnvString(raw); + + const { env, shared } = buildAuthEnv(parsed, "local"); + + writeEnvFile(envPath, env); + + return shared; +} + +function writeEnvFile(filePath: string, env: Record) { + const content = Object.entries(env) + .map(([k, v]) => { + if (v.includes("\n")) { + return `${k}="${escapeMultiline(v)}"`; + } + return `${k}=${v}`; + }) + .join("\n"); + + fs.writeFileSync(filePath, content + "\n"); +} + +function escapeMultiline(value: string) { + return value.replace(/\n/g, "\\n"); +} diff --git a/src/prompts/projectSetup.ts b/src/prompts/projectSetup.ts index 5391f0f..f90de51 100644 --- a/src/prompts/projectSetup.ts +++ b/src/prompts/projectSetup.ts @@ -1,13 +1,10 @@ import { confirm, select, text } from "@clack/prompts"; -type WebFramework = "react" | null; -type ApiFramework = "express" | null; +type WebFramework = string | null; +type ApiFramework = string | null; type AuthMode = "local" | "docker"; export async function runProjectSetupPrompts() { - // ----------------------------- - // Web - // ----------------------------- const web = await confirm({ message: "Do you want to create a web application?", }); @@ -28,9 +25,6 @@ export async function runProjectSetupPrompts() { webFramework = result as WebFramework; } - // ----------------------------- - // API - // ----------------------------- const api = await confirm({ message: "Do you want to create an API server?", }); @@ -51,9 +45,6 @@ export async function runProjectSetupPrompts() { apiFramework = result as ApiFramework; } - // ----------------------------- - // Auth Mode - // ----------------------------- const authMode = (await select({ message: "How would you like to run SeamlessAuth?", options: [ @@ -68,9 +59,6 @@ export async function runProjectSetupPrompts() { ], })) as AuthMode; - // ----------------------------- - // Docker - // ----------------------------- let useDocker = await confirm({ message: "Do you want to run your stack with Docker?", }); @@ -82,9 +70,6 @@ export async function runProjectSetupPrompts() { useDocker = true; } - // ----------------------------- - // Final Config - // ----------------------------- return { web, webFramework, From fb99ecf02632a91b19281537af12fe3a5b6949e1 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 13:35:01 -0400 Subject: [PATCH 4/7] fix: ensure that dev run arm still has proper envs --- src/commands/init.ts | 39 +++++++++++++++------------------ src/generators/docker/docker.ts | 21 ++++++++++++++---- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index e672fe5..431a919 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -4,13 +4,12 @@ import { runProjectSetupPrompts } from "../prompts/projectSetup.js"; import { generateReactStarter } from "../generators/frontend/react.js"; import { generateExpressStarter } from "../generators/backend/express.js"; import { generateAuthServer } from "../generators/auth/auth.js"; +import { configureApiEnv, configureWebEnv } from "../core/configure.js"; import { - configureApiEnv, - configureAuthEnv, - configureWebEnv, -} from "../core/configure.js"; -import { generateKid, generateSecret } from "../core/secrets.js"; -import { generateDockerCompose } from "../generators/docker/docker.js"; + configureAuthLocalEnv, + extractSharedFromExistingEnv, + generateDockerCompose, +} from "../generators/docker/docker.js"; import { printSuccessOutput } from "../core/output.js"; export async function runCLI(projectName?: string) { @@ -48,19 +47,25 @@ export async function runCLI(projectName?: string) { if (answers.api && answers.apiFramework === "express") { await generateExpressStarter({ root }); } + let sharedConfig: any = {}; if (answers.authMode === "local") { await generateAuthServer({ root }, "local"); - sharedConfig = configureAuthEnv(root); - } else { - await generateAuthServer({ root }, "docker"); + sharedConfig = await configureAuthLocalEnv(root); + } + + if (answers.useDocker) { + const dockerShared = await generateDockerCompose(root, { + authMode: answers.authMode, + includeApi: answers.api, + includeWeb: answers.web, + }); - sharedConfig = { - apiToken: generateSecret(32), - kid: generateKid(), - }; + if (answers.authMode === "docker") { + sharedConfig = dockerShared; + } } if (answers.api) { @@ -71,14 +76,6 @@ export async function runCLI(projectName?: string) { configureWebEnv(root); } - if (answers.useDocker) { - generateDockerCompose(root, { - authMode: answers.authMode, - includeApi: answers.api, - includeWeb: answers.web, - }); - } - printSuccessOutput({ projectName, root, diff --git a/src/generators/docker/docker.ts b/src/generators/docker/docker.ts index 6ea10a9..c1f4d9d 100644 --- a/src/generators/docker/docker.ts +++ b/src/generators/docker/docker.ts @@ -2,7 +2,7 @@ import fs from "fs"; import path from "path"; import { fetchEnvExample } from "../../core/fetch.js"; import { parseEnv, parseEnvString } from "../../core/env.js"; -import { generateKid, generateSecret } from "../../core/secrets.js"; +import { generateSecret } from "../../core/secrets.js"; import { generateJWKS } from "../../core/jwks.js"; export async function generateDockerCompose( @@ -13,7 +13,7 @@ export async function generateDockerCompose( includeWeb: boolean | Symbol; }, ) { - const compose = await buildCompose(options, root); + const { compose, shared } = await buildCompose(options, root); fs.writeFileSync( path.join(root, "docker-compose.yml"), @@ -21,6 +21,7 @@ export async function generateDockerCompose( ); console.log("Docker compose created."); + return shared; } async function buildCompose(options: any, root: string) { @@ -28,7 +29,8 @@ async function buildCompose(options: any, root: string) { const { service: authBlock, shared } = await authService(authMode, root); - return ` + return { + compose: ` services: db: image: postgres:16 @@ -50,7 +52,9 @@ ${includeWeb ? webService() : ""} volumes: pgdata: -`; +`, + shared, + }; } async function authService(mode: "local" | "docker", root: string) { @@ -273,3 +277,12 @@ function writeEnvFile(filePath: string, env: Record) { function escapeMultiline(value: string) { return value.replace(/\n/g, "\\n"); } + +export function extractSharedFromExistingEnv(root: string) { + const env = parseEnv(path.join(root, "auth", ".env")); + + return { + apiToken: env.API_SERVICE_TOKEN, + kid: env.JWKS_ACTIVE_KID || "main", + }; +} From 1671a7b65155b7b65356b7043f5379b51a33e485 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 15:52:56 -0400 Subject: [PATCH 5/7] feat: working docker flows locally and remotely --- src/commands/init.ts | 1 - src/core/configure.ts | 26 +------------------------- src/core/help.ts | 2 -- src/generators/docker/docker.ts | 4 ++-- src/inspectors/detectBackend.ts | 0 src/inspectors/detectDatabase.ts | 0 src/inspectors/detectFrontend.ts | 0 src/prompts/emptyProject.ts | 23 ----------------------- src/registry.ts | 0 src/utils/depsInstaller.ts | 21 --------------------- 10 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 src/inspectors/detectBackend.ts delete mode 100644 src/inspectors/detectDatabase.ts delete mode 100644 src/inspectors/detectFrontend.ts delete mode 100644 src/prompts/emptyProject.ts delete mode 100644 src/registry.ts delete mode 100644 src/utils/depsInstaller.ts diff --git a/src/commands/init.ts b/src/commands/init.ts index 431a919..c58881d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -7,7 +7,6 @@ import { generateAuthServer } from "../generators/auth/auth.js"; import { configureApiEnv, configureWebEnv } from "../core/configure.js"; import { configureAuthLocalEnv, - extractSharedFromExistingEnv, generateDockerCompose, } from "../generators/docker/docker.js"; import { printSuccessOutput } from "../core/output.js"; diff --git a/src/core/configure.ts b/src/core/configure.ts index 54aaff7..d16660a 100644 --- a/src/core/configure.ts +++ b/src/core/configure.ts @@ -1,32 +1,8 @@ import path from "path"; import fs from "fs"; import { parseEnv, writeEnv } from "./env.js"; -import { generateKid, generateSecret } from "./secrets.js"; +import { generateSecret } from "./secrets.js"; -export function configureAuthEnv(root: string) { - const authEnvPath = path.join(root, "auth", ".env"); - - if (!fs.existsSync(authEnvPath)) return; - - const env = parseEnv(authEnvPath); - - const apiToken = generateSecret(32); - const kid = generateKid(); - - env.API_SERVICE_TOKEN = apiToken; - env.JWKS_ACTIVE_KID = kid; - - // Ensure correct URLs - env.ISSUER = "http://localhost:5312"; - env.APP_ORIGIN = "http://localhost:5173"; - - writeEnv(authEnvPath, env); - - return { - apiToken, - kid, - }; -} export function configureApiEnv(root: string, shared: any) { const apiEnvPath = path.join(root, "api", ".env"); diff --git a/src/core/help.ts b/src/core/help.ts index 2b60240..e03284f 100644 --- a/src/core/help.ts +++ b/src/core/help.ts @@ -21,13 +21,11 @@ BEHAVIOR Without a name: • If directory is empty → create a new project - • If directory has files → integrate SeamlessAuth npx create-seamless With a name: • Creates a new directory - • Scaffolds a new project inside it ──────────────────────────────────────────── diff --git a/src/generators/docker/docker.ts b/src/generators/docker/docker.ts index c1f4d9d..c42b172 100644 --- a/src/generators/docker/docker.ts +++ b/src/generators/docker/docker.ts @@ -143,7 +143,7 @@ async function authServiceDocker() { return { service: ` auth: - image: ghcr.io/fells-code/seamless-auth-api:v0.1.4 + image: ghcr.io/fells-code/seamless-auth-api:v0.1.5 container_name: seamless-auth ports: - "5312:5312" @@ -231,7 +231,7 @@ export function buildJWKSConfig() { keys: [ { kid, - pem: publicKey.replace(/\n/g, "\\n"), + pem: publicKey, }, ], }, diff --git a/src/inspectors/detectBackend.ts b/src/inspectors/detectBackend.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/inspectors/detectDatabase.ts b/src/inspectors/detectDatabase.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/inspectors/detectFrontend.ts b/src/inspectors/detectFrontend.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/prompts/emptyProject.ts b/src/prompts/emptyProject.ts deleted file mode 100644 index 49bc575..0000000 --- a/src/prompts/emptyProject.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { select, confirm } from "@clack/prompts"; -import { generateReactStarter } from "../generators/frontend/react.js"; - -export async function handleEmptyProject(context: any) { - const shouldCreate = await confirm({ - message: "No project detected. Start a new project here?", - }); - - if (!shouldCreate) return; - - const projectType = await select({ - message: "What would you like to create?", - options: [ - { value: "react-vite", label: "React (Vite)" }, - { value: "backend", label: "Backend API (coming soon)" }, - { value: "auth", label: "Seamless Auth Server (coming soon)" }, - ], - }); - - if (projectType === "react-vite") { - await generateReactStarter(context); - } -} diff --git a/src/registry.ts b/src/registry.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/depsInstaller.ts b/src/utils/depsInstaller.ts deleted file mode 100644 index 024d0a1..0000000 --- a/src/utils/depsInstaller.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { runCommand } from "../core/exec.js"; - -export async function installDeps(pm: string, deps: string[], root: string) { - if (pm === "npm") { - await runCommand("npm", ["install", ...deps], root); - } else if (pm === "pnpm") { - await runCommand("pnpm", ["add", ...deps], root); - } else { - await runCommand("yarn", ["add", ...deps], root); - } -} - -export async function installDevDeps(pm: string, deps: string[], root: string) { - if (pm === "npm") { - await runCommand("npm", ["install", "-D", ...deps], root); - } else if (pm === "pnpm") { - await runCommand("pnpm", ["add", "-D", ...deps], root); - } else { - await runCommand("yarn", ["add", "-D", ...deps], root); - } -} From 49a189c54a02b9e51fbfc4d7ea0a7e583bf15267 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 19:03:33 -0400 Subject: [PATCH 6/7] feat: output improvement --- src/core/output.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/output.ts b/src/core/output.ts index df455cd..9ed688f 100644 --- a/src/core/output.ts +++ b/src/core/output.ts @@ -59,7 +59,22 @@ export function printSuccessOutput(config: { } else { if (authMode === "local") { console.log(kleur.dim("# Auth server")); - console.log(" cd auth && npm install && npm run dev\n"); + + console.log( + kleur.yellow( + " ⚠ Requires a local PostgreSQL instance running on localhost, port 5432\n", + ), + ); + + console.log(" cd auth"); + console.log(" npm install\n"); + + console.log(kleur.dim(" # Initialize database")); + console.log(" npm run db:create"); + console.log(" npm run db:migrate\n"); + + console.log(kleur.dim(" # Start auth server")); + console.log(" npm run dev\n"); } if (apiFramework) { From 5801f123ee8f78b791ef0a5d11d6b630a22e810e Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 21 Mar 2026 19:05:06 -0400 Subject: [PATCH 7/7] 0.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8747a87..b78e393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-seamless", - "version": "0.0.11", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-seamless", - "version": "0.0.11", + "version": "0.1.0", "license": "AGPL-3.0-only", "dependencies": { "@clack/prompts": "^1.0.1", diff --git a/package.json b/package.json index 9730331..3b1112a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-seamless", - "version": "0.0.11", + "version": "0.1.0", "description": "The starter script for Seamless Auth", "homepage": "https://github.com/fells-code/create-seamless#readme", "bugs": {