@brewnet/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +184 -0
  2. package/dist/admin-server-DQVIEHV3.js +14 -0
  3. package/dist/admin-server-DQVIEHV3.js.map +1 -0
  4. package/dist/boilerplate-manager-P6QYUU7Q.js +29 -0
  5. package/dist/boilerplate-manager-P6QYUU7Q.js.map +1 -0
  6. package/dist/chunk-2VWMDHGI.js +1393 -0
  7. package/dist/chunk-2VWMDHGI.js.map +1 -0
  8. package/dist/chunk-4TJMJZMO.js +1173 -0
  9. package/dist/chunk-4TJMJZMO.js.map +1 -0
  10. package/dist/chunk-BAVGYMGA.js +114 -0
  11. package/dist/chunk-BAVGYMGA.js.map +1 -0
  12. package/dist/chunk-DH2VK3YI.js +293 -0
  13. package/dist/chunk-DH2VK3YI.js.map +1 -0
  14. package/dist/chunk-HCHY5UIQ.js +301 -0
  15. package/dist/chunk-HCHY5UIQ.js.map +1 -0
  16. package/dist/chunk-JFPHGZ6Z.js +254 -0
  17. package/dist/chunk-JFPHGZ6Z.js.map +1 -0
  18. package/dist/chunk-SIXBB6JU.js +2973 -0
  19. package/dist/chunk-SIXBB6JU.js.map +1 -0
  20. package/dist/chunk-SYV6PK3R.js +181 -0
  21. package/dist/chunk-SYV6PK3R.js.map +1 -0
  22. package/dist/chunk-ZKMWE5AH.js +444 -0
  23. package/dist/chunk-ZKMWE5AH.js.map +1 -0
  24. package/dist/cloudflare-client-TFT6VCXF.js +32 -0
  25. package/dist/cloudflare-client-TFT6VCXF.js.map +1 -0
  26. package/dist/compose-generator-O7GSIJ2S.js +19 -0
  27. package/dist/compose-generator-O7GSIJ2S.js.map +1 -0
  28. package/dist/frameworks-Z7VXDGP4.js +18 -0
  29. package/dist/frameworks-Z7VXDGP4.js.map +1 -0
  30. package/dist/index.d.ts +22 -0
  31. package/dist/index.js +7897 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/services/admin-daemon.d.ts +2 -0
  34. package/dist/services/admin-daemon.js +33 -0
  35. package/dist/services/admin-daemon.js.map +1 -0
  36. package/dist/stacks-M4FBTVO5.js +16 -0
  37. package/dist/stacks-M4FBTVO5.js.map +1 -0
  38. package/dist/state-2SI3P4JG.js +27 -0
  39. package/dist/state-2SI3P4JG.js.map +1 -0
  40. package/package.json +44 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/boilerplate-manager.ts"],"sourcesContent":["/**\n * brewnet create-app — Boilerplate Manager\n *\n * Implements all steps of the `brewnet create-app` execution flow:\n * 1. cloneStack — shallow git clone from brewnet-boilerplate\n * 2. generateEnv — .env from .env.example with secure secrets\n * 3. reinitGit — clean git history for the scaffolded project\n * 4. startContainers — docker compose up -d --build\n * 5. pollHealth — HTTP GET /health with timeout\n * 6. verifyEndpoints — GET /api/hello + POST /api/echo\n *\n * All functions are stateless and throw on failure.\n * Callers are responsible for user-facing spinner/error output.\n *\n * @module services/boilerplate-manager\n */\n\nimport { readFileSync, writeFileSync, rmSync, readdirSync, statSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport { randomBytes } from 'node:crypto';\nimport { createServer } from 'node:net';\nimport { execa } from 'execa';\nimport yaml from 'js-yaml';\nimport { addQuickTunnelAppLabels } from './compose-generator.js';\nimport { BOILERPLATE_REPO_URL } from '@brewnet/shared';\nimport type { StackHealthResult } from '@brewnet/shared';\n\n// ---------------------------------------------------------------------------\n// Port utility\n// ---------------------------------------------------------------------------\n\n/**\n * Find the first free TCP port starting from `start`.\n * Tries up to 20 consecutive ports; falls back to `start` if none are free.\n *\n * @param start - Preferred port (e.g. 3000 or 8080)\n */\n/** Ports reserved for system services — never assigned to app containers. */\nconst RESERVED_PORTS = new Set([8088]); // admin server\n\nexport async function findFreePort(start: number): Promise<number> {\n for (let port = start; port < start + 40; port++) {\n if (RESERVED_PORTS.has(port)) continue;\n const free = await new Promise<boolean>((resolve) => {\n const srv = createServer();\n srv.once('error', () => resolve(false));\n srv.once('listening', () => { srv.close(() => resolve(true)); });\n // Bind to 0.0.0.0 (all interfaces) so we catch IPv6 dual-stack processes\n // that occupy 0.0.0.0 but leave 127.0.0.1 appearing free on macOS.\n srv.listen(port, '0.0.0.0');\n });\n if (free) return port;\n }\n return start; // last-resort fallback\n}\n\n// Re-export for callers that need the constant\nexport { BOILERPLATE_REPO_URL };\n\n// ---------------------------------------------------------------------------\n// T004 — cloneStack\n// ---------------------------------------------------------------------------\n\n/**\n * Shallow-clone the selected boilerplate stack into projectDir.\n *\n * Uses `--depth=1` (mandatory for fast download) and checks out\n * the `stack/<stackId>` orphan branch.\n *\n * @param stackId - One of the 16 valid stack IDs (e.g. \"go-gin\")\n * @param projectDir - Absolute path of the destination directory (must not exist)\n * @throws {Error} if git clone fails (network error, unknown branch, etc.)\n */\nexport async function cloneStack(stackId: string, projectDir: string): Promise<void> {\n // If directory already exists (previous run), remove and re-clone for a clean state.\n // git clone refuses to write into a non-empty directory.\n const { existsSync: dirExists, rmSync } = await import('node:fs');\n if (dirExists(projectDir)) {\n rmSync(projectDir, { recursive: true, force: true });\n }\n await execa('git', [\n 'clone',\n '--depth=1',\n '-b',\n `stack/${stackId}`,\n BOILERPLATE_REPO_URL,\n projectDir,\n ]);\n // Next.js stacks need absolute paths for <Image> component.\n // All other stacks keep relative paths (./...) so they work under\n // Traefik sub-path routing (/apps/{name}/) via Quick Tunnel.\n if (stackId.startsWith('nodejs-nextjs')) {\n patchImagePaths(projectDir, '/brewnet-site-banner.png');\n }\n // Non-Next.js stacks: boilerplate already uses \"./brewnet-site-banner.png\" — no patch needed.\n}\n\n/**\n * Patch image paths in cloned boilerplate source files.\n *\n * Next.js <Image> component requires absolute paths (`/brewnet-site-banner.png`).\n * Other stacks (Vite/React) must keep relative paths (`./brewnet-site-banner.png`)\n * because under Quick Tunnel sub-path routing (/apps/{name}/), an absolute root\n * path `/image.png` resolves to the tunnel root (Traefik catch-all landing page).\n * Relative `./image.png` resolves correctly to `/apps/{name}/image.png` when the\n * trailing-slash redirect middleware ensures the browser URL has a trailing slash.\n */\nfunction patchImagePaths(dir: string, replacement: string, search = './brewnet-site-banner.png'): void {\n const jsxExts = new Set(['.tsx', '.ts', '.jsx', '.js']);\n const needle = `src=\"${search}\"`;\n const insert = `src=\"${replacement}\"`;\n const walk = (d: string) => {\n for (const entry of readdirSync(d)) {\n if (entry === 'node_modules' || entry === '.git' || entry === 'dist') continue;\n const full = join(d, entry);\n if (statSync(full).isDirectory()) { walk(full); continue; }\n if (!jsxExts.has(extname(entry))) continue;\n const original = readFileSync(full, 'utf-8');\n const patched = original.replaceAll(needle, insert);\n if (patched !== original) writeFileSync(full, patched, 'utf-8');\n }\n };\n walk(dir);\n}\n\n// ---------------------------------------------------------------------------\n// T005 — reinitGit\n// ---------------------------------------------------------------------------\n\n/**\n * Remove the boilerplate git history and start a fresh repository.\n *\n * Steps:\n * 1. Delete .git directory (severs link to brewnet-boilerplate)\n * 2. git init\n * 3. git add -A\n * 4. git commit -m \"chore: initial project from brewnet create-app\"\n *\n * @param projectDir - Absolute path to the scaffolded project\n */\nexport async function reinitGit(projectDir: string): Promise<void> {\n // Remove existing .git directory\n rmSync(join(projectDir, '.git'), { recursive: true, force: true });\n\n // Initialize fresh repository\n await execa('git', ['init'], { cwd: projectDir });\n await execa('git', ['add', '-A'], { cwd: projectDir });\n await execa('git', ['commit', '-m', 'chore: initial project from brewnet create-app'], {\n cwd: projectDir,\n });\n}\n\n// ---------------------------------------------------------------------------\n// T006 — generateEnv\n// ---------------------------------------------------------------------------\n\n/** Prisma DB provider values mapped from DB_DRIVER. */\nconst PRISMA_PROVIDER: Record<string, string> = {\n postgres: 'postgresql',\n mysql: 'mysql',\n sqlite3: 'sqlite',\n};\n\n/** Build the Prisma DATABASE_URL using the given connection parameters. */\nfunction buildPrismaDatabaseUrl(\n dbDriver: string,\n dbUser: string,\n dbPassword: string,\n dbName: string,\n): string {\n switch (dbDriver) {\n case 'postgres':\n return `postgresql://${dbUser}:${dbPassword}@postgres:5432/${dbName}`;\n case 'mysql':\n return `mysql://${dbUser}:${dbPassword}@mysql:3306/${dbName}`;\n default:\n return 'file:/app/data/brewnet_db.db';\n }\n}\n\n/**\n * Optional overrides for generateEnv(). When provided, these values replace\n * the defaults that come from the boilerplate .env.example.\n */\nexport interface GenerateEnvOpts {\n /** DB username. Overrides DB_USER and MYSQL_USER (default: \"brewnet\"). */\n dbUser?: string;\n /**\n * DB password to use instead of a randomly generated secret.\n * Pass the wizard admin password here for consistent credentials.\n */\n dbPassword?: string;\n /** DB name. Overrides DB_NAME and MYSQL_DATABASE (default: \"brewnet_db\"). */\n dbName?: string;\n /**\n * Override the host-side port binding (BACKEND_PORT in .env).\n * All boilerplate stacks use `${BACKEND_PORT:-default}:containerPort` in\n * docker-compose.yml, so setting this lets callers pick a free port\n * automatically without touching the container-internal port.\n */\n hostPort?: number;\n /**\n * Override the host-side frontend port binding (FRONTEND_PORT in .env).\n * Non-unified stacks (e.g. nodejs-express) have a separate frontend\n * container on port 3000. Set this to avoid \"port already in use\" errors.\n */\n frontendPort?: number;\n}\n\n/**\n * Generate .env from .env.example by injecting secure secrets and optional\n * credential overrides.\n *\n * What the function changes (all other values keep .env.example defaults):\n * - DB_DRIVER → dbDriver argument\n * - DB_USER / DB_NAME → opts.dbUser / opts.dbName (or defaults)\n * - DB_PASSWORD → opts.dbPassword or 64-char hex secret\n * - MYSQL_USER / MYSQL_DATABASE → same overrides as postgres\n * - MYSQL_PASSWORD → 64-char hex secret (always random)\n * - MYSQL_ROOT_PASSWORD → 64-char hex secret (always random)\n * - PRISMA_DB_PROVIDER → derived from dbDriver (nodejs-* stacks only)\n * - DATABASE_URL → connection URL (nodejs-* stacks only)\n *\n * IMPORTANT: Generated secrets are NEVER logged or displayed.\n *\n * @param projectDir - Absolute path to the scaffolded project\n * @param stackId - Stack ID used to detect nodejs-* stacks for Prisma vars\n * @param dbDriver - \"sqlite3\" | \"postgres\" | \"mysql\"\n * @param opts - Optional credential overrides (wizard DB settings)\n */\nexport function generateEnv(\n projectDir: string,\n stackId: string,\n dbDriver: string,\n opts?: GenerateEnvOpts,\n): void {\n const examplePath = join(projectDir, '.env.example');\n const envPath = join(projectDir, '.env');\n\n // 1. Read .env.example as base content\n let content = readFileSync(examplePath, 'utf-8');\n\n // 2. Resolve credentials — use provided values or generate random secrets\n const dbUser = opts?.dbUser ?? 'brewnet';\n const dbName = opts?.dbName ?? 'brewnet_db';\n const dbPassword = opts?.dbPassword ?? randomBytes(32).toString('hex');\n const mysqlPassword = randomBytes(32).toString('hex');\n const mysqlRoot = randomBytes(32).toString('hex');\n\n // 3. Apply regex substitutions (line-by-line, preserves comments/formatting)\n content = content\n .replace(/^DB_DRIVER=.*/m, `DB_DRIVER=${dbDriver}`)\n .replace(/^DB_USER=.*/m, `DB_USER=${dbUser}`)\n .replace(/^DB_NAME=.*/m, `DB_NAME=${dbName}`)\n .replace(/^DB_PASSWORD=.*/m, `DB_PASSWORD=${dbPassword}`)\n .replace(/^MYSQL_USER=.*/m, `MYSQL_USER=${dbUser}`)\n .replace(/^MYSQL_DATABASE=.*/m, `MYSQL_DATABASE=${dbName}`)\n .replace(/^MYSQL_PASSWORD=.*/m, `MYSQL_PASSWORD=${mysqlPassword}`)\n .replace(/^MYSQL_ROOT_PASSWORD=.*/m, `MYSQL_ROOT_PASSWORD=${mysqlRoot}`);\n\n // 4. Override host ports if free ports were selected (avoids \"port already in use\").\n // Stacks use `${BACKEND_PORT:-default}:containerPort` so only the host side changes.\n if (opts?.hostPort !== undefined) {\n if (/^BACKEND_PORT=/m.test(content)) {\n content = content.replace(/^BACKEND_PORT=.*/m, `BACKEND_PORT=${opts.hostPort}`);\n } else {\n content += `\\nBACKEND_PORT=${opts.hostPort}\\n`;\n }\n }\n if (opts?.frontendPort !== undefined) {\n if (/^FRONTEND_PORT=/m.test(content)) {\n content = content.replace(/^FRONTEND_PORT=.*/m, `FRONTEND_PORT=${opts.frontendPort}`);\n } else {\n content += `\\nFRONTEND_PORT=${opts.frontendPort}\\n`;\n }\n }\n\n // 5. Node.js stacks (Prisma): set PRISMA_DB_PROVIDER + DATABASE_URL\n if (stackId.startsWith('nodejs-')) {\n const provider = PRISMA_PROVIDER[dbDriver] ?? 'sqlite';\n const databaseUrl = buildPrismaDatabaseUrl(dbDriver, dbUser, dbPassword, dbName);\n content = content\n .replace(/^PRISMA_DB_PROVIDER=.*/m, `PRISMA_DB_PROVIDER=${provider}`)\n .replace(/^DATABASE_URL=.*/m, `DATABASE_URL=${databaseUrl}`);\n }\n\n // 5. Write .env — not committed, not displayed\n writeFileSync(envPath, content, 'utf-8');\n}\n\n// ---------------------------------------------------------------------------\n// T006b-pre — patchNextConfig (basePath injection)\n// ---------------------------------------------------------------------------\n\n/**\n * Inject `basePath: '/apps/{appName}'` into next.config.{ts,mjs,js} so that\n * Next.js generates all asset/image paths under the sub-path prefix.\n *\n * Without basePath, Next.js emits `/_next/static/...` absolute root paths.\n * Under Quick Tunnel sub-path routing (/apps/{appName}/), the browser requests\n * `https://tunnel/_next/static/...` which misses Traefik's PathPrefix rule and\n * returns the landing-page HTML instead of CSS/JS assets.\n *\n * With basePath set, Next.js emits `/apps/{appName}/_next/static/...` which\n * correctly matches Traefik's PathPrefix route.\n *\n * @param projectDir - Absolute path to the Next.js project\n * @param appName - Logical app name used as path segment (e.g. \"nextjs-full\")\n */\nexport function patchNextConfig(projectDir: string, appName: string): void {\n const candidates = ['next.config.ts', 'next.config.mjs', 'next.config.js'];\n let configPath: string | null = null;\n for (const c of candidates) {\n const p = join(projectDir, c);\n if (existsSync(p)) { configPath = p; break; }\n }\n if (!configPath) return;\n\n let content = readFileSync(configPath, 'utf-8');\n const basePath = `/apps/${appName}`;\n\n // Already patched — skip\n if (content.includes('basePath')) return;\n\n // Insert basePath + unoptimized images after output: 'standalone'.\n // images.unoptimized is required because Next.js standalone _next/image optimizer\n // fails to resolve local images when basePath is set (fetches /img.png instead of\n // /apps/{name}/img.png internally → 400 \"not a valid image\").\n // With unoptimized: true, <Image> renders a plain <img> tag pointing directly to\n // /apps/{name}/img.png which Traefik routes correctly.\n content = content.replace(\n /output:\\s*['\"]standalone['\"]/,\n `output: 'standalone',\\n basePath: '${basePath}',\\n images: { unoptimized: true }`,\n );\n\n // Fallback: non-standalone configs (user-owned repos without output: 'standalone')\n if (!content.includes('basePath')) {\n content = content.replace(\n /((?:const|let|var)\\s+\\w+(?:\\s*:\\s*[\\w<>, |&]+)?\\s*=\\s*\\{|module\\.exports\\s*=\\s*\\{|export\\s+default\\s+\\{)/,\n `$1\\n basePath: '${basePath}',`,\n );\n }\n\n writeFileSync(configPath, content, 'utf-8');\n\n // Re-patch image paths to include basePath prefix.\n // With images.unoptimized=true, <Image src=\"/foo.png\"> renders <img src=\"/foo.png\">.\n // The browser resolves absolute paths from the tunnel root, missing Traefik's\n // PathPrefix route. Must be /apps/{appName}/foo.png for correct routing.\n // cloneStack() already converted ./brewnet-site-banner.png → /brewnet-site-banner.png\n // for Next.js stacks; now add the basePath prefix.\n patchImagePaths(projectDir, `${basePath}/brewnet-site-banner.png`, '/brewnet-site-banner.png');\n\n // Also patch docker-compose.yml healthcheck: /health → /apps/{appName}/health\n // With basePath set, Next.js serves all routes (including /health) under the prefix.\n const composePath = join(projectDir, 'docker-compose.yml');\n if (existsSync(composePath)) {\n let compose = readFileSync(composePath, 'utf-8');\n const oldHealthPath = 'http://127.0.0.1:3000/health';\n const newHealthPath = `http://127.0.0.1:3000${basePath}/health`;\n if (compose.includes(oldHealthPath) && !compose.includes(newHealthPath)) {\n compose = compose.replaceAll(oldHealthPath, newHealthPath);\n }\n // Fallback: scaffolded templates use root path (/) instead of /health\n const oldRootPath = 'http://127.0.0.1:3000/';\n const newRootPath = `http://127.0.0.1:3000${basePath}/`;\n if (compose.includes(oldRootPath) && !compose.includes(newRootPath)) {\n compose = compose.replaceAll(oldRootPath, newRootPath);\n }\n writeFileSync(composePath, compose, 'utf-8');\n }\n}\n\n// ---------------------------------------------------------------------------\n// T006b — injectTraefikForQuickTunnel\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the container-internal port from a docker-compose ports entry.\n * e.g. \"${BACKEND_PORT:-8080}:8080\" → 8080\n * \"3000:80\" → 80\n * \"${PORT}:3000\" → 3000\n */\nfunction parseContainerPort(portSpec: string): number | null {\n const str = String(portSpec);\n // \"host:container\" or \"${VAR:-default}:container\"\n const colonIdx = str.lastIndexOf(':');\n if (colonIdx >= 0) {\n const containerPart = str.slice(colonIdx + 1).replace(/\\/.*$/, ''); // strip /tcp, /udp\n const n = parseInt(containerPart, 10);\n return isNaN(n) ? null : n;\n }\n const n = parseInt(str, 10);\n return isNaN(n) ? null : n;\n}\n\n/**\n * Inject Traefik PathPrefix labels and brewnet external network into a\n * boilerplate's docker-compose.yml so that HTTP services are routable\n * through Quick Tunnel at /apps/{appName} (backend) and /apps/{appName}-ui (frontend).\n *\n * Reads the compose file to:\n * 1. Detect backend service (backend, app, web, api, server) and its internal port\n * 2. Detect frontend service (frontend, ui) and its internal port\n * 3. Inject Traefik labels + force brewnet to external: true\n *\n * @param projectDir - Absolute path to the boilerplate project\n * @param appName - Logical name used as path segment (e.g. \"nodejs-nestjs\")\n * @param _backendPort - Host port (unused — we detect internal port from compose)\n */\nexport function injectTraefikForQuickTunnel(\n projectDir: string,\n appName: string,\n _backendPort: number,\n): void {\n const composePath = join(projectDir, 'docker-compose.yml');\n if (!existsSync(composePath)) return;\n\n const raw = readFileSync(composePath, 'utf-8');\n const doc = yaml.load(raw) as Record<string, unknown>;\n const services = doc['services'] as Record<string, Record<string, unknown>> | undefined;\n if (!services) return;\n\n // Detect Next.js projects: basePath handles sub-path routing internally,\n // so Traefik must NOT strip the prefix (noStrip: true).\n const isNextjs = ['next.config.ts', 'next.config.mjs', 'next.config.js']\n .some((f) => existsSync(join(projectDir, f)));\n if (isNextjs) {\n patchNextConfig(projectDir, appName);\n }\n\n // addQuickTunnelAppLabels imported at top level (static import for ESM compatibility)\n\n // Non-HTTP services to skip\n const skipServices = new Set(['postgres', 'postgresql', 'mysql', 'mariadb', 'redis', 'db']);\n\n // Backend: common names\n const backendNames = ['backend', 'app', 'web', 'api', 'server'];\n const backendKey = backendNames.find((n) => services[n] && !skipServices.has(n));\n\n // Frontend: common names\n const frontendNames = ['frontend', 'ui'];\n const frontendKey = frontendNames.find((n) => services[n]);\n\n // Single-service stacks: just use the first HTTP service\n const allSvcKeys = Object.keys(services).filter((k) => !skipServices.has(k));\n const singleService = allSvcKeys.length === 1 ? allSvcKeys[0]! : null;\n\n if (singleService && !backendKey && !frontendKey) {\n // Unified single-service stack\n const svc = services[singleService]!;\n const ports = (svc['ports'] ?? []) as string[];\n const containerPort = ports.length > 0 ? parseContainerPort(ports[0]!) ?? 8080 : 8080;\n addQuickTunnelAppLabels(composePath, appName, singleService, containerPort, isNextjs);\n return;\n }\n\n // Multi-service: inject labels for both backend and frontend\n if (backendKey) {\n const svc = services[backendKey]!;\n const ports = (svc['ports'] ?? []) as string[];\n const containerPort = ports.length > 0 ? parseContainerPort(ports[0]!) ?? 8080 : 8080;\n addQuickTunnelAppLabels(composePath, appName, backendKey, containerPort, isNextjs);\n }\n\n if (frontendKey) {\n const svc = services[frontendKey]!;\n const ports = (svc['ports'] ?? []) as string[];\n const containerPort = ports.length > 0 ? parseContainerPort(ports[0]!) ?? 80 : 80;\n // Re-read the compose file since addQuickTunnelAppLabels writes it\n addQuickTunnelAppLabels(composePath, `${appName}-ui`, frontendKey, containerPort);\n }\n}\n\n// ---------------------------------------------------------------------------\n// T007 — startContainers\n// ---------------------------------------------------------------------------\n\n/**\n * Start the stack containers in detached mode with a fresh build.\n *\n * Runs `docker compose up -d --build` in the project directory.\n * This is a long-running operation; callers should update spinners accordingly.\n *\n * @param projectDir - Absolute path to the scaffolded project\n * @throws {Error} if docker compose exits with a non-zero status\n */\nexport async function startContainers(projectDir: string): Promise<void> {\n await execa('docker', ['compose', 'up', '-d', '--build'], {\n cwd: projectDir,\n });\n}\n\n// ---------------------------------------------------------------------------\n// T008 — pollHealth\n// ---------------------------------------------------------------------------\n\n/**\n * Poll GET <baseUrl>/health until HTTP 200 with body.status === \"ok\".\n *\n * Uses `127.0.0.1` in baseUrl (not `localhost`) to avoid Alpine Linux\n * IPv6 resolution failures where localhost resolves to ::1.\n *\n * @param baseUrl - e.g. \"http://127.0.0.1:8080\" (no trailing slash)\n * @param timeoutMs - Maximum wait time in milliseconds (120_000 or 600_000 for Rust)\n * @returns StackHealthResult with healthy, elapsedMs, and dbConnected\n */\nexport async function pollHealth(baseUrl: string, timeoutMs: number): Promise<StackHealthResult> {\n const start = Date.now();\n const deadline = start + timeoutMs;\n\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`${baseUrl}/health`, {\n signal: AbortSignal.timeout(3000),\n });\n if (res.ok) {\n const body = (await res.json()) as { status?: string; db_connected?: boolean };\n if (body.status === 'ok') {\n return {\n healthy: true,\n elapsedMs: Date.now() - start,\n dbConnected: body.db_connected,\n };\n }\n }\n } catch {\n // Not ready yet — continue polling\n }\n\n // Wait 1 second between attempts\n await new Promise<void>((resolve) => setTimeout(resolve, 1000));\n }\n\n return {\n healthy: false,\n elapsedMs: Date.now() - start,\n error: `Health check timed out after ${Math.round(timeoutMs / 1000)}s`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// T009 — verifyEndpoints\n// ---------------------------------------------------------------------------\n\n/**\n * Verify that GET /api/hello and POST /api/echo respond correctly.\n *\n * Contract (from CONNECT_BOILERPLATE.md Section 7):\n * - GET /api/hello → HTTP 200, body.message field exists\n * - POST /api/echo → HTTP 200, body.test === \"brewnet\"\n *\n * @param baseUrl - e.g. \"http://127.0.0.1:8080\" (no trailing slash)\n * @throws {Error} if either endpoint fails or returns unexpected data\n */\nexport async function verifyEndpoints(baseUrl: string): Promise<void> {\n // 1. GET /api/hello\n const helloRes = await fetch(`${baseUrl}/api/hello`, {\n signal: AbortSignal.timeout(5000),\n });\n if (!helloRes.ok) {\n throw new Error(`GET /api/hello returned HTTP ${helloRes.status}`);\n }\n const helloBody = (await helloRes.json()) as { message?: unknown };\n if (!helloBody.message) {\n throw new Error(`GET /api/hello response missing \"message\" field`);\n }\n\n // 2. POST /api/echo\n const echoRes = await fetch(`${baseUrl}/api/echo`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ test: 'brewnet' }),\n signal: AbortSignal.timeout(5000),\n });\n if (!echoRes.ok) {\n throw new Error(`POST /api/echo returned HTTP ${echoRes.status}`);\n }\n const echoBody = (await echoRes.json()) as { test?: unknown };\n if (echoBody.test !== 'brewnet') {\n throw new Error(\n `POST /api/echo response mismatch: expected test=\"brewnet\", got \"${String(echoBody.test)}\"`,\n );\n }\n}\n"],"mappings":"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","names":["rmSync","n"]}
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../shared/dist/utils/constants.js
4
+ var SCHEMA_VERSION = 7;
5
+ var DOCKER_COMPOSE_FILENAME = "docker-compose.yml";
6
+ var DB_VERSIONS = {
7
+ postgresql: ["18.3", "17.9", "16.13"],
8
+ mysql: ["8.4", "8.0", "5.7"],
9
+ sqlite: ["3"]
10
+ };
11
+ var BOILERPLATE_REPO_URL = "https://github.com/claude-code-expert/brewnet-boilerplate.git";
12
+ var DOCKER_LOG_MAX_SIZE = "10m";
13
+ var DOCKER_LOG_MAX_FILES = "3";
14
+ var CLI_LOG_RETENTION_DAYS = 30;
15
+ var ACCESS_LOG_MAX_BYTES = 50 * 1024 * 1024;
16
+ var LOG_QUERY_DEFAULT_LIMIT = 100;
17
+ var LOG_QUERY_MAX_LIMIT = 1e3;
18
+
19
+ // ../shared/dist/types/errors.js
20
+ var ErrorCode;
21
+ (function(ErrorCode2) {
22
+ ErrorCode2["BN001"] = "BN001";
23
+ ErrorCode2["BN002"] = "BN002";
24
+ ErrorCode2["BN003"] = "BN003";
25
+ ErrorCode2["BN004"] = "BN004";
26
+ ErrorCode2["BN005"] = "BN005";
27
+ ErrorCode2["BN006"] = "BN006";
28
+ ErrorCode2["BN007"] = "BN007";
29
+ ErrorCode2["BN008"] = "BN008";
30
+ ErrorCode2["BN009"] = "BN009";
31
+ ErrorCode2["BN010"] = "BN010";
32
+ })(ErrorCode || (ErrorCode = {}));
33
+ var ERROR_HTTP_STATUS = {
34
+ [ErrorCode.BN001]: 503,
35
+ [ErrorCode.BN002]: 409,
36
+ [ErrorCode.BN003]: 500,
37
+ [ErrorCode.BN004]: 401,
38
+ [ErrorCode.BN005]: 429,
39
+ [ErrorCode.BN006]: 500,
40
+ [ErrorCode.BN007]: 400,
41
+ [ErrorCode.BN008]: 404,
42
+ [ErrorCode.BN009]: 500,
43
+ [ErrorCode.BN010]: 403
44
+ };
45
+ var ERROR_MESSAGES = {
46
+ [ErrorCode.BN001]: "Docker daemon is not running. Please start Docker and try again.",
47
+ [ErrorCode.BN002]: "Port is already in use. Please free the port or choose a different one.",
48
+ [ErrorCode.BN003]: "SSL certificate issuance failed. Check your domain configuration.",
49
+ [ErrorCode.BN004]: "Invalid license key. Please verify your license.",
50
+ [ErrorCode.BN005]: "Rate limit exceeded. Please wait and try again.",
51
+ [ErrorCode.BN006]: "Build failed. Check the build logs for details.",
52
+ [ErrorCode.BN007]: "Invalid Git repository. Ensure the repository URL is correct.",
53
+ [ErrorCode.BN008]: "Resource not found.",
54
+ [ErrorCode.BN009]: "Database error. Check the database configuration and logs.",
55
+ [ErrorCode.BN010]: "This feature requires a Pro plan. Upgrade to access it."
56
+ };
57
+
58
+ // ../shared/dist/schemas/wizard-state.schema.js
59
+ import { z } from "zod";
60
+ var languageSchema = z.enum([
61
+ "python",
62
+ "nodejs",
63
+ "java",
64
+ "rust",
65
+ "go",
66
+ "kotlin"
67
+ ]);
68
+ var frontendTechSchema = z.enum([
69
+ "react",
70
+ "vue",
71
+ "none"
72
+ ]);
73
+ var webServerServiceSchema = z.enum(["traefik", "nginx", "caddy"]);
74
+ var fileServerServiceSchema = z.enum(["nextcloud", "minio", ""]);
75
+ var dbPrimarySchema = z.enum(["postgresql", "mysql", "sqlite", ""]);
76
+ var cacheServiceSchema = z.enum([""]);
77
+ var domainProviderSchema = z.enum(["local", "tunnel", "quick-tunnel"]);
78
+ var sslModeSchema = z.enum(["self-signed", "letsencrypt", "cloudflare"]);
79
+ var setupTypeSchema = z.enum(["full", "partial"]);
80
+ var fileBrowserModeSchema = z.enum(["directory", "standalone", ""]);
81
+ var devModeSchema = z.enum(["hot-reload", "production"]);
82
+ var adminConfigSchema = z.object({
83
+ username: z.string().min(1, "Admin username is required"),
84
+ password: z.string().min(8, "Admin password must be at least 8 characters"),
85
+ storage: z.literal("local")
86
+ });
87
+ var webServerConfigSchema = z.object({
88
+ enabled: z.literal(true),
89
+ service: webServerServiceSchema
90
+ });
91
+ var fileServerConfigSchema = z.object({
92
+ enabled: z.boolean(),
93
+ service: fileServerServiceSchema
94
+ });
95
+ var gitServerConfigSchema = z.object({
96
+ enabled: z.literal(true),
97
+ service: z.literal("gitea"),
98
+ port: z.number().int().min(1).max(65535),
99
+ sshPort: z.number().int().min(1).max(65535)
100
+ });
101
+ var dbServerConfigSchema = z.object({
102
+ enabled: z.boolean(),
103
+ primary: dbPrimarySchema,
104
+ primaryVersion: z.string(),
105
+ dbName: z.string(),
106
+ dbUser: z.string(),
107
+ dbPassword: z.string(),
108
+ adminUI: z.boolean(),
109
+ pgadminEmail: z.string(),
110
+ cache: cacheServiceSchema
111
+ });
112
+ var mediaConfigSchema = z.object({
113
+ enabled: z.boolean(),
114
+ services: z.array(z.string())
115
+ });
116
+ var sshServerConfigSchema = z.object({
117
+ enabled: z.boolean(),
118
+ port: z.number().int().min(1).max(65535),
119
+ passwordAuth: z.boolean(),
120
+ sftp: z.boolean()
121
+ });
122
+ var appServerConfigSchema = z.object({
123
+ enabled: z.boolean()
124
+ });
125
+ var fileBrowserConfigSchema = z.object({
126
+ enabled: z.boolean(),
127
+ mode: fileBrowserModeSchema
128
+ });
129
+ var serverComponentsSchema = z.object({
130
+ webServer: webServerConfigSchema,
131
+ fileServer: fileServerConfigSchema,
132
+ gitServer: gitServerConfigSchema,
133
+ dbServer: dbServerConfigSchema,
134
+ media: mediaConfigSchema,
135
+ sshServer: sshServerConfigSchema,
136
+ appServer: appServerConfigSchema,
137
+ fileBrowser: fileBrowserConfigSchema
138
+ });
139
+ var devStackConfigSchema = z.object({
140
+ languages: z.array(languageSchema),
141
+ frameworks: z.record(z.string(), z.string()),
142
+ frontend: frontendTechSchema.nullable()
143
+ });
144
+ var boilerplateConfigSchema = z.object({
145
+ generate: z.boolean(),
146
+ sampleData: z.boolean(),
147
+ devMode: devModeSchema
148
+ });
149
+ var tunnelModeSchema = z.enum(["quick", "named", "none"]);
150
+ var cloudflareConfigSchema = z.object({
151
+ enabled: z.boolean(),
152
+ tunnelMode: tunnelModeSchema,
153
+ quickTunnelUrl: z.string(),
154
+ accountId: z.string(),
155
+ apiToken: z.string(),
156
+ tunnelId: z.string(),
157
+ tunnelToken: z.string(),
158
+ tunnelName: z.string(),
159
+ zoneId: z.string(),
160
+ zoneName: z.string()
161
+ });
162
+ var domainConfigSchema = z.object({
163
+ provider: domainProviderSchema,
164
+ name: z.string(),
165
+ ssl: sslModeSchema,
166
+ cloudflare: cloudflareConfigSchema
167
+ });
168
+ var domainScenarioSchema = z.enum(["A", "B", "C"]);
169
+ var domainConnectionSchema = z.object({
170
+ appName: z.string().min(1, "App name is required"),
171
+ subdomain: z.string().min(1, "Subdomain is required").regex(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, "Subdomain must be a valid DNS label (lowercase alphanumeric + hyphens, max 63 chars)"),
172
+ domain: z.string().min(1, "Domain is required"),
173
+ hostname: z.string().min(1, "Hostname is required"),
174
+ tunnelId: z.string().min(1, "Tunnel ID is required"),
175
+ cnameRecordId: z.string(),
176
+ containerPort: z.number().int().min(1).max(65535),
177
+ connectedAt: z.string().datetime(),
178
+ scenario: domainScenarioSchema
179
+ });
180
+ var wizardStateSchema = z.object({
181
+ schemaVersion: z.literal(7),
182
+ projectName: z.string().min(1, "Project name is required").regex(/^[a-z0-9][a-z0-9-]*$/, "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens"),
183
+ projectPath: z.string().min(1, "Project path is required"),
184
+ setupType: setupTypeSchema,
185
+ admin: adminConfigSchema,
186
+ servers: serverComponentsSchema,
187
+ devStack: devStackConfigSchema,
188
+ boilerplate: boilerplateConfigSchema,
189
+ domain: domainConfigSchema,
190
+ domainConnections: z.array(domainConnectionSchema),
191
+ portRemapping: z.record(z.coerce.number(), z.number().int().min(1).max(65535))
192
+ });
193
+
194
+ // ../shared/dist/schemas/config.schema.js
195
+ import { z as z2 } from "zod";
196
+ var configAdminSchema = z2.object({
197
+ username: z2.string().min(1),
198
+ storage: z2.literal("local")
199
+ });
200
+ var configWebServerSchema = z2.object({
201
+ enabled: z2.literal(true),
202
+ service: webServerServiceSchema
203
+ });
204
+ var configFileServerSchema = z2.object({
205
+ enabled: z2.boolean(),
206
+ service: fileServerServiceSchema
207
+ });
208
+ var configGitServerSchema = z2.object({
209
+ enabled: z2.literal(true),
210
+ service: z2.literal("gitea"),
211
+ port: z2.number().int().min(1).max(65535),
212
+ sshPort: z2.number().int().min(1).max(65535)
213
+ });
214
+ var configDbServerSchema = z2.object({
215
+ enabled: z2.boolean(),
216
+ primary: dbPrimarySchema,
217
+ primaryVersion: z2.string(),
218
+ dbName: z2.string(),
219
+ dbUser: z2.string(),
220
+ adminUI: z2.boolean(),
221
+ pgadminEmail: z2.string(),
222
+ cache: cacheServiceSchema
223
+ // dbPassword is intentionally excluded from export
224
+ });
225
+ var configMediaSchema = z2.object({
226
+ enabled: z2.boolean(),
227
+ services: z2.array(z2.string())
228
+ });
229
+ var configSshServerSchema = z2.object({
230
+ enabled: z2.boolean(),
231
+ port: z2.number().int().min(1).max(65535),
232
+ passwordAuth: z2.boolean(),
233
+ sftp: z2.boolean()
234
+ });
235
+ var configAppServerSchema = z2.object({
236
+ enabled: z2.boolean()
237
+ });
238
+ var configFileBrowserSchema = z2.object({
239
+ enabled: z2.boolean(),
240
+ mode: fileBrowserModeSchema
241
+ });
242
+ var configServersSchema = z2.object({
243
+ webServer: configWebServerSchema,
244
+ fileServer: configFileServerSchema,
245
+ gitServer: configGitServerSchema,
246
+ dbServer: configDbServerSchema,
247
+ media: configMediaSchema,
248
+ sshServer: configSshServerSchema,
249
+ appServer: configAppServerSchema,
250
+ fileBrowser: configFileBrowserSchema
251
+ });
252
+ var configDevStackSchema = z2.object({
253
+ languages: z2.array(languageSchema),
254
+ frameworks: z2.record(z2.string(), z2.string()),
255
+ frontend: frontendTechSchema.nullable()
256
+ });
257
+ var configBoilerplateSchema = z2.object({
258
+ generate: z2.boolean(),
259
+ sampleData: z2.boolean(),
260
+ devMode: devModeSchema
261
+ });
262
+ var configCloudflareSchema = z2.object({
263
+ enabled: z2.boolean(),
264
+ tunnelName: z2.string()
265
+ // tunnelToken is intentionally excluded from export
266
+ });
267
+ var configDomainSchema = z2.object({
268
+ provider: domainProviderSchema,
269
+ name: z2.string(),
270
+ ssl: sslModeSchema,
271
+ cloudflare: configCloudflareSchema
272
+ });
273
+ var brewnetConfigSchema = z2.object({
274
+ schemaVersion: z2.literal(7),
275
+ projectName: z2.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens"),
276
+ projectPath: z2.string().min(1),
277
+ setupType: setupTypeSchema,
278
+ admin: configAdminSchema,
279
+ servers: configServersSchema,
280
+ devStack: configDevStackSchema,
281
+ boilerplate: configBoilerplateSchema,
282
+ domain: configDomainSchema
283
+ });
284
+ function validateBrewnetConfig(data) {
285
+ return brewnetConfigSchema.parse(data);
286
+ }
287
+
288
+ export {
289
+ validateBrewnetConfig,
290
+ SCHEMA_VERSION,
291
+ DOCKER_COMPOSE_FILENAME,
292
+ DB_VERSIONS,
293
+ BOILERPLATE_REPO_URL,
294
+ DOCKER_LOG_MAX_SIZE,
295
+ DOCKER_LOG_MAX_FILES,
296
+ CLI_LOG_RETENTION_DAYS,
297
+ ACCESS_LOG_MAX_BYTES,
298
+ LOG_QUERY_DEFAULT_LIMIT,
299
+ LOG_QUERY_MAX_LIMIT
300
+ };
301
+ //# sourceMappingURL=chunk-HCHY5UIQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../shared/src/utils/constants.ts","../../shared/src/types/errors.ts","../../shared/src/schemas/wizard-state.schema.ts","../../shared/src/schemas/config.schema.ts"],"sourcesContent":["// T013 — Shared constants used across the Brewnet CLI and Dashboard\n\n// ─── Schema & Versioning ────────────────────────────────────────────────────\n\n/** Current wizard state schema version */\nexport const SCHEMA_VERSION = 7 as const;\n\n/** Minimum schema version that can be migrated (older versions are reset) */\nexport const MIN_MIGRATABLE_SCHEMA_VERSION = 3;\n\n// ─── Project Defaults ────────────────────────────────────────────────────────\n\nexport const DEFAULT_PROJECT_NAME = 'my-homeserver';\nexport const DEFAULT_PROJECT_PATH_PREFIX = '~/brewnet';\nexport const DEFAULT_DATA_DIR = '~/.brewnet';\nexport const DEFAULT_CONFIG_FILENAME = 'brewnet.config.json';\n\n// ─── Admin & Credentials ────────────────────────────────────────────────────\n\nexport const DEFAULT_ADMIN_USERNAME = 'admin';\nexport const DEFAULT_ADMIN_PASSWORD_LENGTH = 20;\nexport const DEFAULT_SERVICE_PASSWORD_LENGTH = 16;\nexport const DEFAULT_DB_NAME = 'brewnet';\nexport const DEFAULT_DB_USER = 'brewnet';\n\n// ─── Port Defaults ───────────────────────────────────────────────────────────\n\nexport const DEFAULT_SSH_PORT = 2222;\nexport const DEFAULT_GIT_WEB_PORT = 3000;\nexport const DEFAULT_GIT_SSH_PORT = 3022;\nexport const DEFAULT_HTTP_PORT = 80;\nexport const DEFAULT_HTTPS_PORT = 443;\nexport const DEFAULT_TRAEFIK_DASHBOARD_PORT = 8080;\n\n// ─── Health Check ────────────────────────────────────────────────────────────\n\n/** Maximum time to wait for all health checks to pass (ms) */\nexport const HEALTH_CHECK_TIMEOUT_MS = 120_000;\n\n/** Interval between health check polls (ms) */\nexport const HEALTH_CHECK_INTERVAL_MS = 2_000;\n\n/** Number of consecutive successes required to consider a service healthy */\nexport const HEALTH_CHECK_SUCCESS_THRESHOLD = 3;\n\n/** Number of consecutive failures before marking a service as unhealthy */\nexport const HEALTH_CHECK_FAILURE_THRESHOLD = 5;\n\n// ─── Docker ──────────────────────────────────────────────────────────────────\n\nexport const DOCKER_COMPOSE_FILENAME = 'docker-compose.yml';\nexport const DOCKER_NETWORK_EXTERNAL = 'brewnet';\nexport const DOCKER_NETWORK_INTERNAL = 'brewnet-internal';\nexport const DOCKER_RESTART_POLICY = 'unless-stopped';\nexport const DOCKER_COMPOSE_VERSION = '3.8';\n\n// ─── File Permissions ────────────────────────────────────────────────────────\n\n/** Permission for .env files containing secrets (owner read/write only) */\nexport const ENV_FILE_PERMISSIONS = 0o600;\n\n/** Permission for SSH private keys (owner read only) */\nexport const SSH_KEY_PERMISSIONS = 0o600;\n\n/** Permission for SSH authorized_keys (owner read/write only) */\nexport const SSH_AUTHORIZED_KEYS_PERMISSIONS = 0o600;\n\n/** Permission for directories (owner rwx, group rx, others rx) */\nexport const DEFAULT_DIR_PERMISSIONS = 0o755;\n\n// ─── Storage Keys ────────────────────────────────────────────────────────────\n\n/** localStorage key for wizard state (used by demo and dashboard) */\nexport const WIZARD_STATE_STORAGE_KEY = 'brewnet_wizard_state';\n\n// ─── Service Images ─────────────────────────────────────────────────────────\n\nexport const SERVICE_IMAGES = {\n traefik: 'traefik:v3.1',\n nginx: 'nginx:1.27-alpine',\n caddy: 'caddy:2-alpine',\n nextcloud: 'nextcloud:29-apache',\n minio: 'minio/minio:latest',\n gitea: 'gitea/gitea:1.22',\n postgresql: 'postgres:18.3-alpine',\n mysql: 'mysql:8.4',\n sqlite: '',\n redis: 'redis:7-alpine',\n valkey: 'valkey/valkey:8-alpine',\n keydb: 'eqalpha/keydb:latest',\n jellyfin: 'jellyfin/jellyfin:latest',\n\n filebrowser: 'filebrowser/filebrowser:latest',\n pgadmin: 'dpage/pgadmin4:latest',\n phpmyadmin: 'phpmyadmin:latest',\n} as const;\n\n// ─── Database Versions ───────────────────────────────────────────────────────\n\nexport const DB_VERSIONS: Record<string, string[]> = {\n postgresql: ['18.3', '17.9', '16.13'],\n mysql: ['8.4', '8.0', '5.7'],\n sqlite: ['3'],\n};\n\n// ─── Default Web Server ──────────────────────────────────────────────────────\n\nexport const DEFAULT_WEB_SERVER = 'traefik' as const;\n\n// ─── Domain Defaults ─────────────────────────────────────────────────────────\n\nexport const DEFAULT_DOMAIN_PROVIDER = 'local' as const;\nexport const DEFAULT_SSL_MODE = 'self-signed' as const;\n\n// ─── Boilerplate Defaults ────────────────────────────────────────────────────\n\nexport const DEFAULT_DEV_MODE = 'hot-reload' as const;\n\n/** Git repository URL for Brewnet boilerplate templates (not yet populated) */\nexport const BOILERPLATE_REPO_URL = 'https://github.com/claude-code-expert/brewnet-boilerplate.git';\n\n// ─── CLI Metadata ────────────────────────────────────────────────────────────\n\nexport const CLI_NAME = 'brewnet';\nexport const CLI_DESCRIPTION = 'Your Home Server, Brewed Fresh';\n\n// ─── Rate Limiting ───────────────────────────────────────────────────────────\n\n/** Maximum API requests per minute for free tier */\nexport const RATE_LIMIT_FREE = 60;\n\n/** Maximum API requests per minute for Pro tier */\nexport const RATE_LIMIT_PRO = 300;\n\n// ─── Backup ──────────────────────────────────────────────────────────────────\n\n/** Maximum number of backup archives to retain */\nexport const MAX_BACKUP_RETENTION = 10;\n\n/** Default backup directory name within ~/.brewnet */\nexport const BACKUP_DIR_NAME = 'backups';\n\n// ─── Timeouts ────────────────────────────────────────────────────────────────\n\n/** Timeout for Docker image pull operations (ms) */\nexport const DOCKER_PULL_TIMEOUT_MS = 300_000;\n\n/** Timeout for Docker container start operations (ms) */\nexport const DOCKER_START_TIMEOUT_MS = 60_000;\n\n/** Timeout for SSL certificate issuance (ms) */\nexport const SSL_ISSUANCE_TIMEOUT_MS = 120_000;\n\n/** Timeout for DNS propagation check (ms) */\nexport const DNS_PROPAGATION_TIMEOUT_MS = 300_000;\n\n// ─── Logging ────────────────────────────────────────────────────────────────\n\n/** Maximum log file size per container (Docker json-file driver) */\nexport const DOCKER_LOG_MAX_SIZE = '10m';\n\n/** Maximum number of rotated log files per container */\nexport const DOCKER_LOG_MAX_FILES = '3';\n\n/** Days before CLI log files are deleted */\nexport const CLI_LOG_RETENTION_DAYS = 30;\n\n/** Maximum access/tunnel log file size before copytruncate rotation (bytes) */\nexport const ACCESS_LOG_MAX_BYTES = 50 * 1024 * 1024;\n\n/** Default page size for log queries */\nexport const LOG_QUERY_DEFAULT_LIMIT = 100;\n\n/** Maximum page size for log queries */\nexport const LOG_QUERY_MAX_LIMIT = 1000;\n\n/** Admin Panel log auto-refresh interval (ms) */\nexport const LOG_POLL_INTERVAL_MS = 5000;\n","// T010 — Error codes, backup records, log entries, and generated config types\n\n// ─── Error Codes ─────────────────────────────────────────────────────────────\n\nexport enum ErrorCode {\n /** Docker daemon not running */\n BN001 = 'BN001',\n /** Port already in use */\n BN002 = 'BN002',\n /** SSL certificate issuance failed */\n BN003 = 'BN003',\n /** Invalid license key */\n BN004 = 'BN004',\n /** Rate limit exceeded */\n BN005 = 'BN005',\n /** Build failed */\n BN006 = 'BN006',\n /** Invalid Git repository */\n BN007 = 'BN007',\n /** Resource not found */\n BN008 = 'BN008',\n /** Database error */\n BN009 = 'BN009',\n /** Feature requires Pro plan */\n BN010 = 'BN010',\n}\n\n/** Maps error codes to their corresponding HTTP status codes */\nexport const ERROR_HTTP_STATUS: Record<ErrorCode, number> = {\n [ErrorCode.BN001]: 503,\n [ErrorCode.BN002]: 409,\n [ErrorCode.BN003]: 500,\n [ErrorCode.BN004]: 401,\n [ErrorCode.BN005]: 429,\n [ErrorCode.BN006]: 500,\n [ErrorCode.BN007]: 400,\n [ErrorCode.BN008]: 404,\n [ErrorCode.BN009]: 500,\n [ErrorCode.BN010]: 403,\n};\n\n/** Human-readable descriptions for each error code */\nexport const ERROR_MESSAGES: Record<ErrorCode, string> = {\n [ErrorCode.BN001]: 'Docker daemon is not running. Please start Docker and try again.',\n [ErrorCode.BN002]: 'Port is already in use. Please free the port or choose a different one.',\n [ErrorCode.BN003]: 'SSL certificate issuance failed. Check your domain configuration.',\n [ErrorCode.BN004]: 'Invalid license key. Please verify your license.',\n [ErrorCode.BN005]: 'Rate limit exceeded. Please wait and try again.',\n [ErrorCode.BN006]: 'Build failed. Check the build logs for details.',\n [ErrorCode.BN007]: 'Invalid Git repository. Ensure the repository URL is correct.',\n [ErrorCode.BN008]: 'Resource not found.',\n [ErrorCode.BN009]: 'Database error. Check the database configuration and logs.',\n [ErrorCode.BN010]: 'This feature requires a Pro plan. Upgrade to access it.',\n};\n\n// ─── Brewnet Error Class ─────────────────────────────────────────────────────\n\nexport class BrewnetError extends Error {\n public readonly code: ErrorCode;\n public readonly httpStatus: number;\n public readonly metadata?: Record<string, unknown>;\n\n constructor(code: ErrorCode, message?: string, metadata?: Record<string, unknown>) {\n super(message ?? ERROR_MESSAGES[code]);\n this.name = 'BrewnetError';\n this.code = code;\n this.httpStatus = ERROR_HTTP_STATUS[code];\n this.metadata = metadata;\n\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Log Entry ───────────────────────────────────────────────────────────────\n\nexport type LogLevel = 'info' | 'warn' | 'error';\n\nexport interface LogEntry {\n /** Timestamp of the log entry */\n timestamp: Date;\n /** Severity level */\n level: LogLevel;\n /** CLI command that generated this log (e.g., \"brewnet add jellyfin\") */\n command: string;\n /** Human-readable log message */\n message: string;\n /** Optional structured metadata */\n metadata?: Record<string, unknown>;\n}\n\n// ─── Backup Record ───────────────────────────────────────────────────────────\n\nexport interface BackupRecord {\n /** Unique backup identifier (UUID) */\n id: string;\n /** Filesystem path to the backup archive */\n path: string;\n /** Backup archive size in bytes */\n size: number;\n /** List of service IDs included in this backup */\n services: string[];\n /** When the backup was created */\n createdAt: Date;\n /** Name of the project that was backed up */\n projectName: string;\n /** Filesystem path of the project that was backed up */\n projectPath: string;\n}\n\n// ─── Generated Config ────────────────────────────────────────────────────────\n\nexport interface GeneratedFile {\n /** Relative file path within the project directory */\n path: string;\n /** Full file content as a string */\n content: string;\n /** Unix file permissions (e.g., 0o600 for .env files) */\n permissions?: number;\n}\n\nexport interface GeneratedConfig {\n /** Absolute path to the project directory */\n projectPath: string;\n /** List of files to be written */\n files: GeneratedFile[];\n}\n","// T011 — Zod schema for WizardState validation\n// Validates the complete wizard state object including all sub-schemas.\n\nimport { z } from 'zod';\nimport type { WizardState } from '../types/wizard-state.js';\n\n// ─── Primitive Schemas ───────────────────────────────────────────────────────\n\nexport const languageSchema = z.enum([\n 'python', 'nodejs', 'java', 'rust', 'go', 'kotlin',\n]);\n\nexport const frontendTechSchema = z.enum([\n 'react', 'vue', 'none',\n]);\n\nexport const webServerServiceSchema = z.enum(['traefik', 'nginx', 'caddy']);\n\nexport const fileServerServiceSchema = z.enum(['nextcloud', 'minio', '']);\n\nexport const dbPrimarySchema = z.enum(['postgresql', 'mysql', 'sqlite', '']);\n\nexport const cacheServiceSchema = z.enum(['']);\n\nexport const domainProviderSchema = z.enum(['local', 'tunnel', 'quick-tunnel']);\n\nexport const sslModeSchema = z.enum(['self-signed', 'letsencrypt', 'cloudflare']);\n\nexport const setupTypeSchema = z.enum(['full', 'partial']);\n\nexport const fileBrowserModeSchema = z.enum(['directory', 'standalone', '']);\n\nexport const devModeSchema = z.enum(['hot-reload', 'production']);\n\n// ─── Sub-Schemas ─────────────────────────────────────────────────────────────\n\nexport const adminConfigSchema = z.object({\n username: z.string().min(1, 'Admin username is required'),\n password: z.string().min(8, 'Admin password must be at least 8 characters'),\n storage: z.literal('local'),\n});\n\nexport const webServerConfigSchema = z.object({\n enabled: z.literal(true),\n service: webServerServiceSchema,\n});\n\nexport const fileServerConfigSchema = z.object({\n enabled: z.boolean(),\n service: fileServerServiceSchema,\n});\n\nexport const gitServerConfigSchema = z.object({\n enabled: z.literal(true),\n service: z.literal('gitea'),\n port: z.number().int().min(1).max(65535),\n sshPort: z.number().int().min(1).max(65535),\n});\n\nexport const dbServerConfigSchema = z.object({\n enabled: z.boolean(),\n primary: dbPrimarySchema,\n primaryVersion: z.string(),\n dbName: z.string(),\n dbUser: z.string(),\n dbPassword: z.string(),\n adminUI: z.boolean(),\n pgadminEmail: z.string(),\n cache: cacheServiceSchema,\n});\n\nexport const mediaConfigSchema = z.object({\n enabled: z.boolean(),\n services: z.array(z.string()),\n});\n\nexport const sshServerConfigSchema = z.object({\n enabled: z.boolean(),\n port: z.number().int().min(1).max(65535),\n passwordAuth: z.boolean(),\n sftp: z.boolean(),\n});\n\nexport const appServerConfigSchema = z.object({\n enabled: z.boolean(),\n});\n\nexport const fileBrowserConfigSchema = z.object({\n enabled: z.boolean(),\n mode: fileBrowserModeSchema,\n});\n\nexport const serverComponentsSchema = z.object({\n webServer: webServerConfigSchema,\n fileServer: fileServerConfigSchema,\n gitServer: gitServerConfigSchema,\n dbServer: dbServerConfigSchema,\n media: mediaConfigSchema,\n sshServer: sshServerConfigSchema,\n appServer: appServerConfigSchema,\n fileBrowser: fileBrowserConfigSchema,\n});\n\nexport const devStackConfigSchema = z.object({\n languages: z.array(languageSchema),\n frameworks: z.record(z.string(), z.string()),\n frontend: frontendTechSchema.nullable(),\n});\n\nexport const boilerplateConfigSchema = z.object({\n generate: z.boolean(),\n sampleData: z.boolean(),\n devMode: devModeSchema,\n});\n\nexport const tunnelModeSchema = z.enum(['quick', 'named', 'none']);\n\nexport const cloudflareConfigSchema = z.object({\n enabled: z.boolean(),\n tunnelMode: tunnelModeSchema,\n quickTunnelUrl: z.string(),\n accountId: z.string(),\n apiToken: z.string(),\n tunnelId: z.string(),\n tunnelToken: z.string(),\n tunnelName: z.string(),\n zoneId: z.string(),\n zoneName: z.string(),\n});\n\nexport const domainConfigSchema = z.object({\n provider: domainProviderSchema,\n name: z.string(),\n ssl: sslModeSchema,\n cloudflare: cloudflareConfigSchema,\n});\n\nexport const domainScenarioSchema = z.enum(['A', 'B', 'C']);\n\nexport const domainConnectionSchema = z.object({\n appName: z.string().min(1, 'App name is required'),\n subdomain: z.string().min(1, 'Subdomain is required').regex(\n /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/,\n 'Subdomain must be a valid DNS label (lowercase alphanumeric + hyphens, max 63 chars)',\n ),\n domain: z.string().min(1, 'Domain is required'),\n hostname: z.string().min(1, 'Hostname is required'),\n tunnelId: z.string().min(1, 'Tunnel ID is required'),\n cnameRecordId: z.string(),\n containerPort: z.number().int().min(1).max(65535),\n connectedAt: z.string().datetime(),\n scenario: domainScenarioSchema,\n});\n\n// ─── Root Schema ─────────────────────────────────────────────────────────────\n\nexport const wizardStateSchema = z.object({\n schemaVersion: z.literal(7),\n projectName: z.string().min(1, 'Project name is required').regex(\n /^[a-z0-9][a-z0-9-]*$/,\n 'Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens',\n ),\n projectPath: z.string().min(1, 'Project path is required'),\n setupType: setupTypeSchema,\n admin: adminConfigSchema,\n servers: serverComponentsSchema,\n devStack: devStackConfigSchema,\n boilerplate: boilerplateConfigSchema,\n domain: domainConfigSchema,\n domainConnections: z.array(domainConnectionSchema),\n portRemapping: z.record(z.coerce.number(), z.number().int().min(1).max(65535)),\n}) satisfies z.ZodType<WizardState>;\n\nexport type ValidatedWizardState = z.infer<typeof wizardStateSchema>;\n\n// ─── Validation Helpers ──────────────────────────────────────────────────────\n\n/**\n * Validates a WizardState object and returns the parsed result.\n * Throws a ZodError if validation fails.\n */\nexport function validateWizardState(data: unknown): WizardState {\n return wizardStateSchema.parse(data);\n}\n\n/**\n * Safely validates a WizardState object without throwing.\n * Returns a discriminated union with success/error.\n */\nexport function safeValidateWizardState(data: unknown): z.SafeParseReturnType<unknown, WizardState> {\n return wizardStateSchema.safeParse(data);\n}\n","// T012 — Zod schema for brewnet.config.json import/export\n// A subset of WizardState used when exporting or importing wizard configurations.\n// Sensitive fields (passwords, tokens) are excluded from export by default.\n\nimport { z } from 'zod';\nimport {\n setupTypeSchema,\n webServerServiceSchema,\n fileServerServiceSchema,\n dbPrimarySchema,\n cacheServiceSchema,\n domainProviderSchema,\n sslModeSchema,\n fileBrowserModeSchema,\n devModeSchema,\n languageSchema,\n frontendTechSchema,\n} from './wizard-state.schema.js';\n\n// ─── Config Sub-Schemas (export-safe, no secrets) ────────────────────────────\n\nconst configAdminSchema = z.object({\n username: z.string().min(1),\n storage: z.literal('local'),\n});\n\nconst configWebServerSchema = z.object({\n enabled: z.literal(true),\n service: webServerServiceSchema,\n});\n\nconst configFileServerSchema = z.object({\n enabled: z.boolean(),\n service: fileServerServiceSchema,\n});\n\nconst configGitServerSchema = z.object({\n enabled: z.literal(true),\n service: z.literal('gitea'),\n port: z.number().int().min(1).max(65535),\n sshPort: z.number().int().min(1).max(65535),\n});\n\nconst configDbServerSchema = z.object({\n enabled: z.boolean(),\n primary: dbPrimarySchema,\n primaryVersion: z.string(),\n dbName: z.string(),\n dbUser: z.string(),\n adminUI: z.boolean(),\n pgadminEmail: z.string(),\n cache: cacheServiceSchema,\n // dbPassword is intentionally excluded from export\n});\n\nconst configMediaSchema = z.object({\n enabled: z.boolean(),\n services: z.array(z.string()),\n});\n\nconst configSshServerSchema = z.object({\n enabled: z.boolean(),\n port: z.number().int().min(1).max(65535),\n passwordAuth: z.boolean(),\n sftp: z.boolean(),\n});\n\nconst configAppServerSchema = z.object({\n enabled: z.boolean(),\n});\n\nconst configFileBrowserSchema = z.object({\n enabled: z.boolean(),\n mode: fileBrowserModeSchema,\n});\n\nconst configServersSchema = z.object({\n webServer: configWebServerSchema,\n fileServer: configFileServerSchema,\n gitServer: configGitServerSchema,\n dbServer: configDbServerSchema,\n media: configMediaSchema,\n sshServer: configSshServerSchema,\n appServer: configAppServerSchema,\n fileBrowser: configFileBrowserSchema,\n});\n\nconst configDevStackSchema = z.object({\n languages: z.array(languageSchema),\n frameworks: z.record(z.string(), z.string()),\n frontend: frontendTechSchema.nullable(),\n});\n\nconst configBoilerplateSchema = z.object({\n generate: z.boolean(),\n sampleData: z.boolean(),\n devMode: devModeSchema,\n});\n\nconst configCloudflareSchema = z.object({\n enabled: z.boolean(),\n tunnelName: z.string(),\n // tunnelToken is intentionally excluded from export\n});\n\nconst configDomainSchema = z.object({\n provider: domainProviderSchema,\n name: z.string(),\n ssl: sslModeSchema,\n cloudflare: configCloudflareSchema,\n});\n\n// ─── Root Config Schema ──────────────────────────────────────────────────────\n\nexport const brewnetConfigSchema = z.object({\n schemaVersion: z.literal(7),\n projectName: z.string().min(1).regex(\n /^[a-z0-9][a-z0-9-]*$/,\n 'Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, and hyphens',\n ),\n projectPath: z.string().min(1),\n setupType: setupTypeSchema,\n admin: configAdminSchema,\n servers: configServersSchema,\n devStack: configDevStackSchema,\n boilerplate: configBoilerplateSchema,\n domain: configDomainSchema,\n});\n\nexport type BrewnetConfig = z.infer<typeof brewnetConfigSchema>;\n\n// ─── Validation Helpers ──────────────────────────────────────────────────────\n\n/**\n * Validates a brewnet.config.json object. Throws on failure.\n */\nexport function validateBrewnetConfig(data: unknown): BrewnetConfig {\n return brewnetConfigSchema.parse(data);\n}\n\n/**\n * Safely validates a brewnet.config.json object without throwing.\n */\nexport function safeValidateBrewnetConfig(data: unknown): z.SafeParseReturnType<unknown, BrewnetConfig> {\n return brewnetConfigSchema.safeParse(data);\n}\n"],"mappings":"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","names":["ErrorCode","z","z"]}