@brewnet/cli 0.0.1 → 0.0.2

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 (33) hide show
  1. package/dist/admin-server-UODBPGWR.js +16 -0
  2. package/dist/app-manager-FIHPVUP7.js +51 -0
  3. package/dist/{boilerplate-manager-P6QYUU7Q.js → boilerplate-manager-WEFTHL2O.js} +3 -3
  4. package/dist/{chunk-DH2VK3YI.js → chunk-54WFZCU6.js} +2 -2
  5. package/dist/{chunk-4TJMJZMO.js → chunk-AXSHZEB3.js} +63 -1
  6. package/dist/{chunk-4TJMJZMO.js.map → chunk-AXSHZEB3.js.map} +1 -1
  7. package/dist/chunk-FZZ3HP2G.js +1151 -0
  8. package/dist/chunk-FZZ3HP2G.js.map +1 -0
  9. package/dist/chunk-JIPAYMOA.js +58 -0
  10. package/dist/chunk-JIPAYMOA.js.map +1 -0
  11. package/dist/{chunk-SIXBB6JU.js → chunk-Q6UUZR2V.js} +238 -1171
  12. package/dist/chunk-Q6UUZR2V.js.map +1 -0
  13. package/dist/{chunk-2VWMDHGI.js → chunk-YAYXULLO.js} +9 -61
  14. package/dist/chunk-YAYXULLO.js.map +1 -0
  15. package/dist/{chunk-JFPHGZ6Z.js → chunk-YXFDB5YX.js} +17 -2
  16. package/dist/chunk-YXFDB5YX.js.map +1 -0
  17. package/dist/{cloudflare-client-TFT6VCXF.js → cloudflare-client-F2TGQXGS.js} +2 -2
  18. package/dist/{compose-generator-O7GSIJ2S.js → compose-generator-OFJ2YWMB.js} +4 -2
  19. package/dist/compose-generator-OFJ2YWMB.js.map +1 -0
  20. package/dist/index.js +122 -168
  21. package/dist/index.js.map +1 -1
  22. package/dist/services/admin-daemon.js +6 -4
  23. package/dist/services/admin-daemon.js.map +1 -1
  24. package/package.json +1 -1
  25. package/dist/admin-server-DQVIEHV3.js +0 -14
  26. package/dist/chunk-2VWMDHGI.js.map +0 -1
  27. package/dist/chunk-JFPHGZ6Z.js.map +0 -1
  28. package/dist/chunk-SIXBB6JU.js.map +0 -1
  29. /package/dist/{admin-server-DQVIEHV3.js.map → admin-server-UODBPGWR.js.map} +0 -0
  30. /package/dist/{boilerplate-manager-P6QYUU7Q.js.map → app-manager-FIHPVUP7.js.map} +0 -0
  31. /package/dist/{cloudflare-client-TFT6VCXF.js.map → boilerplate-manager-WEFTHL2O.js.map} +0 -0
  32. /package/dist/{chunk-DH2VK3YI.js.map → chunk-54WFZCU6.js.map} +0 -0
  33. /package/dist/{compose-generator-O7GSIJ2S.js.map → cloudflare-client-F2TGQXGS.js.map} +0 -0
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createAdminServer
4
- } from "../chunk-SIXBB6JU.js";
5
- import "../chunk-2VWMDHGI.js";
6
- import "../chunk-JFPHGZ6Z.js";
7
- import "../chunk-4TJMJZMO.js";
4
+ } from "../chunk-Q6UUZR2V.js";
5
+ import "../chunk-FZZ3HP2G.js";
6
+ import "../chunk-YAYXULLO.js";
7
+ import "../chunk-JIPAYMOA.js";
8
+ import "../chunk-YXFDB5YX.js";
9
+ import "../chunk-AXSHZEB3.js";
8
10
  import "../chunk-ZKMWE5AH.js";
9
11
  import "../chunk-HCHY5UIQ.js";
10
12
  import "../chunk-SYV6PK3R.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/admin-daemon.ts"],"sourcesContent":["/**\n * Admin server daemon entry point.\n *\n * Spawned as a detached child process by `brewnet admin` and `brewnet init`.\n * Runs independently of the parent terminal — survives terminal close, shell exit, etc.\n *\n * Usage (internal — not called directly by users):\n * node admin-daemon.js [--port 8088] [--path /path/to/project]\n *\n * @module services/admin-daemon\n */\n\nimport { createAdminServer } from './admin-server.js';\n\nconst args = process.argv.slice(2);\nfunction getArg(name: string, fallback: string): string {\n const idx = args.indexOf(`--${name}`);\n return idx >= 0 && args[idx + 1] ? args[idx + 1]! : fallback;\n}\n\nconst port = parseInt(getArg('port', '8088'), 10);\nconst projectPath = getArg('path', '') || undefined;\n\n// Ignore SIGHUP — this process has no controlling terminal\nprocess.on('SIGHUP', () => { /* detached — ignore */ });\n\n// Suppress stdout/stderr errors when pipe is broken (parent terminal gone)\nprocess.stdout?.on?.('error', () => { /* ignore */ });\nprocess.stderr?.on?.('error', () => { /* ignore */ });\n\nconst { start } = createAdminServer({ port, projectPath });\n\nstart()\n .then(() => {\n // Signal parent that we're ready (if still connected)\n if (process.send) process.send({ status: 'ready', port });\n })\n .catch((err) => {\n if (process.send) process.send({ status: 'error', error: String(err) });\n process.exit(1);\n });\n"],"mappings":";;;;;;;;;;;;AAcA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,SAAS,OAAO,MAAc,UAA0B;AACtD,QAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,EAAE;AACpC,SAAO,OAAO,KAAK,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAK;AACtD;AAEA,IAAM,OAAO,SAAS,OAAO,QAAQ,MAAM,GAAG,EAAE;AAChD,IAAM,cAAc,OAAO,QAAQ,EAAE,KAAK;AAG1C,QAAQ,GAAG,UAAU,MAAM;AAA0B,CAAC;AAGtD,QAAQ,QAAQ,KAAK,SAAS,MAAM;AAAe,CAAC;AACpD,QAAQ,QAAQ,KAAK,SAAS,MAAM;AAAe,CAAC;AAEpD,IAAM,EAAE,MAAM,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,EACH,KAAK,MAAM;AAEV,MAAI,QAAQ,KAAM,SAAQ,KAAK,EAAE,QAAQ,SAAS,KAAK,CAAC;AAC1D,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,MAAI,QAAQ,KAAM,SAAQ,KAAK,EAAE,QAAQ,SAAS,OAAO,OAAO,GAAG,EAAE,CAAC;AACtE,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/services/admin-daemon.ts"],"sourcesContent":["/**\n * Admin server daemon entry point.\n *\n * Spawned as a detached child process by `brewnet admin` and `brewnet init`.\n * Runs independently of the parent terminal — survives terminal close, shell exit, etc.\n *\n * Usage (internal — not called directly by users):\n * node admin-daemon.js [--port 8088] [--path /path/to/project]\n *\n * @module services/admin-daemon\n */\n\nimport { createAdminServer } from './admin-server.js';\n\nconst args = process.argv.slice(2);\nfunction getArg(name: string, fallback: string): string {\n const idx = args.indexOf(`--${name}`);\n return idx >= 0 && args[idx + 1] ? args[idx + 1]! : fallback;\n}\n\nconst port = parseInt(getArg('port', '8088'), 10);\nconst projectPath = getArg('path', '') || undefined;\n\n// Ignore SIGHUP — this process has no controlling terminal\nprocess.on('SIGHUP', () => { /* detached — ignore */ });\n\n// Suppress stdout/stderr errors when pipe is broken (parent terminal gone)\nprocess.stdout?.on?.('error', () => { /* ignore */ });\nprocess.stderr?.on?.('error', () => { /* ignore */ });\n\nconst { start } = createAdminServer({ port, projectPath });\n\nstart()\n .then(() => {\n // Signal parent that we're ready (if still connected)\n if (process.send) process.send({ status: 'ready', port });\n })\n .catch((err) => {\n if (process.send) process.send({ status: 'error', error: String(err) });\n process.exit(1);\n });\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,SAAS,OAAO,MAAc,UAA0B;AACtD,QAAM,MAAM,KAAK,QAAQ,KAAK,IAAI,EAAE;AACpC,SAAO,OAAO,KAAK,KAAK,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAK;AACtD;AAEA,IAAM,OAAO,SAAS,OAAO,QAAQ,MAAM,GAAG,EAAE;AAChD,IAAM,cAAc,OAAO,QAAQ,EAAE,KAAK;AAG1C,QAAQ,GAAG,UAAU,MAAM;AAA0B,CAAC;AAGtD,QAAQ,QAAQ,KAAK,SAAS,MAAM;AAAe,CAAC;AACpD,QAAQ,QAAQ,KAAK,SAAS,MAAM;AAAe,CAAC;AAEpD,IAAM,EAAE,MAAM,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,EACH,KAAK,MAAM;AAEV,MAAI,QAAQ,KAAM,SAAQ,KAAK,EAAE,QAAQ,SAAS,KAAK,CAAC;AAC1D,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,MAAI,QAAQ,KAAM,SAAQ,KAAK,EAAE,QAAQ,SAAS,OAAO,OAAO,GAAG,EAAE,CAAC;AACtE,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brewnet/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- createAdminServer
4
- } from "./chunk-SIXBB6JU.js";
5
- import "./chunk-2VWMDHGI.js";
6
- import "./chunk-JFPHGZ6Z.js";
7
- import "./chunk-4TJMJZMO.js";
8
- import "./chunk-ZKMWE5AH.js";
9
- import "./chunk-HCHY5UIQ.js";
10
- import "./chunk-SYV6PK3R.js";
11
- export {
12
- createAdminServer
13
- };
14
- //# sourceMappingURL=admin-server-DQVIEHV3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/services/service-manager.ts","../src/utils/log-aggregator.ts","../src/services/backup-manager.ts","../src/utils/errors.ts","../src/services/domain-manager.ts","../src/services/app-registry.ts"],"sourcesContent":["/**\n * Brewnet CLI — Service Manager (T094)\n *\n * Provides functions to add and remove Docker services from an existing\n * Brewnet project's docker-compose.yml. All mutations create a backup of\n * the compose file before writing changes.\n *\n * @module services/service-manager\n */\n\nimport { readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync } from 'node:fs';\nimport { join, basename } from 'node:path';\nimport yaml from 'js-yaml';\nimport { DOCKER_COMPOSE_FILENAME } from '@brewnet/shared';\nimport { getServiceDefinition } from '../config/services.js';\nimport type { ServiceDefinition } from '../config/services.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ServiceOperationResult {\n success: boolean;\n composePath?: string;\n backupPath?: string;\n error?: string;\n}\n\ninterface ComposeService {\n image: string;\n container_name?: string;\n restart?: string;\n security_opt?: string[];\n networks?: string[];\n ports?: string[];\n volumes?: string[];\n environment?: Record<string, string>;\n labels?: Record<string, string>;\n depends_on?: string[];\n healthcheck?: Record<string, unknown>;\n command?: string | string[];\n}\n\ninterface ComposeFile {\n version?: string;\n services: Record<string, ComposeService>;\n networks?: Record<string, unknown>;\n volumes?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst BREWNET_PREFIX = 'brewnet';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read and parse a docker-compose.yml file.\n */\nfunction readComposeFile(composePath: string): ComposeFile {\n const content = readFileSync(composePath, 'utf-8');\n return yaml.load(content) as ComposeFile;\n}\n\n/**\n * Write a ComposeFile object as YAML to disk.\n */\nfunction writeComposeFile(composePath: string, compose: ComposeFile): void {\n const yamlContent = yaml.dump(compose, {\n indent: 2,\n lineWidth: 120,\n noRefs: true,\n sortKeys: false,\n quotingType: '\"',\n forceQuotes: false,\n });\n writeFileSync(composePath, yamlContent, 'utf-8');\n}\n\n/**\n * Create a timestamped backup of the compose file.\n * Returns the backup file path.\n */\nfunction backupComposeFile(composePath: string): string {\n const timestamp = Date.now();\n let backupPath = `${composePath}.bak.${timestamp}`;\n\n // Avoid filename collisions when multiple backups happen in the same millisecond\n const dir = join(composePath, '..');\n const base = basename(composePath);\n const existing = readdirSync(dir).filter((f) => f.startsWith(`${base}.bak.`));\n let suffix = 0;\n while (existing.includes(basename(backupPath))) {\n suffix++;\n backupPath = `${composePath}.bak.${timestamp}.${suffix}`;\n }\n\n copyFileSync(composePath, backupPath);\n return backupPath;\n}\n\n/**\n * Volume definitions per service ID. Mirrors compose-generator.ts volume\n * mappings so that `addService` produces consistent compose output.\n */\nfunction getServiceVolumes(serviceId: string): string[] {\n switch (serviceId) {\n case 'traefik':\n return [\n '/var/run/docker.sock:/var/run/docker.sock:ro',\n `${BREWNET_PREFIX}_traefik_certs:/letsencrypt`,\n ];\n case 'gitea':\n return [`${BREWNET_PREFIX}_gitea_data:/data`];\n case 'postgresql':\n return [`${BREWNET_PREFIX}_postgres_data:/var/lib/postgresql/data`];\n case 'mysql':\n return [`${BREWNET_PREFIX}_mysql_data:/var/lib/mysql`];\n case 'redis':\n return [`${BREWNET_PREFIX}_redis_data:/data`];\n case 'valkey':\n return [`${BREWNET_PREFIX}_valkey_data:/data`];\n case 'keydb':\n return [`${BREWNET_PREFIX}_keydb_data:/data`];\n case 'nextcloud':\n return [`${BREWNET_PREFIX}_nextcloud_data:/var/www/html`];\n case 'minio':\n return [`${BREWNET_PREFIX}_minio_data:/data`];\n case 'jellyfin':\n return [\n `${BREWNET_PREFIX}_jellyfin_config:/config`,\n `${BREWNET_PREFIX}_jellyfin_media:/media`,\n ];\n case 'openssh-server':\n return [`${BREWNET_PREFIX}_ssh_config:/config`];\n case 'docker-mailserver':\n return [\n `${BREWNET_PREFIX}_mail_data:/var/mail`,\n `${BREWNET_PREFIX}_mail_state:/var/mail-state`,\n `${BREWNET_PREFIX}_mail_config:/tmp/docker-mailserver`,\n ];\n case 'pgadmin':\n return [`${BREWNET_PREFIX}_pgadmin_data:/var/lib/pgadmin`];\n case 'filebrowser':\n return [\n `${BREWNET_PREFIX}_filebrowser_data:/srv`,\n `${BREWNET_PREFIX}_filebrowser_db:/database`,\n ];\n case 'cloudflared':\n return [];\n default:\n return [];\n }\n}\n\n/**\n * Build a ComposeService block from a ServiceDefinition.\n * Used when adding a service to an existing compose file.\n */\nfunction buildServiceBlock(def: ServiceDefinition): ComposeService {\n const svc: ComposeService = {\n image: def.image,\n container_name: `${BREWNET_PREFIX}-${def.id}`,\n restart: 'unless-stopped',\n security_opt: ['no-new-privileges:true'],\n networks: [...def.networks],\n };\n\n // Volumes\n const volumes = getServiceVolumes(def.id);\n if (volumes.length > 0) {\n svc.volumes = volumes;\n }\n\n // Traefik labels — only when service has a subdomain\n if (def.subdomain && def.traefikLabels) {\n svc.labels = { ...def.traefikLabels };\n }\n\n return svc;\n}\n\n/**\n * Extract named volume keys from a list of volume mount strings.\n * Named volumes are those that do NOT start with '/' or '.'.\n */\nfunction extractNamedVolumes(volumeMounts: string[]): string[] {\n const names: string[] = [];\n for (const vol of volumeMounts) {\n const name = vol.split(':')[0];\n if (name && !name.startsWith('/') && !name.startsWith('.')) {\n names.push(name);\n }\n }\n return names;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Add a service to an existing docker-compose.yml.\n *\n * 1. Look up serviceId in SERVICE_REGISTRY\n * 2. Read existing compose file\n * 3. Check for duplicates\n * 4. Backup existing compose\n * 5. Add service block and named volumes\n * 6. Write updated YAML\n */\nexport async function addService(\n serviceId: string,\n projectPath: string,\n): Promise<ServiceOperationResult> {\n // Validate service ID\n const def = getServiceDefinition(serviceId);\n if (!def) {\n return { success: false, error: `Unknown service: ${serviceId}` };\n }\n\n // Resolve compose file path\n const composePath = join(projectPath, DOCKER_COMPOSE_FILENAME);\n if (!existsSync(composePath)) {\n return {\n success: false,\n error: `docker-compose.yml not found at ${composePath}`,\n };\n }\n\n // Read existing compose\n const compose = readComposeFile(composePath);\n\n // Check for duplicate\n if (compose.services && compose.services[serviceId]) {\n return {\n success: false,\n error: `Service \"${serviceId}\" already exists in compose`,\n };\n }\n\n // Backup before modification\n const backupPath = backupComposeFile(composePath);\n\n // Build and add service block\n const serviceBlock = buildServiceBlock(def);\n if (!compose.services) {\n compose.services = {};\n }\n compose.services[serviceId] = serviceBlock;\n\n // Register named volumes at the top level\n const volumes = getServiceVolumes(serviceId);\n const namedVolumes = extractNamedVolumes(volumes);\n if (namedVolumes.length > 0) {\n if (!compose.volumes) {\n compose.volumes = {};\n }\n for (const vol of namedVolumes) {\n if (!(vol in (compose.volumes as Record<string, unknown>))) {\n (compose.volumes as Record<string, unknown>)[vol] = null;\n }\n }\n }\n\n // Write updated compose\n writeComposeFile(composePath, compose);\n\n return { success: true, composePath, backupPath };\n}\n\n/**\n * Remove a service from an existing docker-compose.yml.\n *\n * 1. Read existing compose file\n * 2. Check service exists\n * 3. Backup existing compose\n * 4. Remove service from services section\n * 5. If purge: also remove associated named volumes\n * 6. Write updated YAML\n */\nexport async function removeService(\n serviceId: string,\n projectPath: string,\n options?: { purge?: boolean },\n): Promise<ServiceOperationResult> {\n // Resolve compose file path\n const composePath = join(projectPath, DOCKER_COMPOSE_FILENAME);\n if (!existsSync(composePath)) {\n return {\n success: false,\n error: `docker-compose.yml not found at ${composePath}`,\n };\n }\n\n // Read existing compose\n const compose = readComposeFile(composePath);\n\n // Check if service exists\n if (!compose.services || !compose.services[serviceId]) {\n return {\n success: false,\n error: `Service \"${serviceId}\" not found in compose`,\n };\n }\n\n // Backup before modification\n const backupPath = backupComposeFile(composePath);\n\n // Collect volume names before removing the service (for purge)\n const serviceEntry = compose.services[serviceId];\n const serviceVolumeMounts = serviceEntry.volumes || [];\n const namedVolumes = extractNamedVolumes(serviceVolumeMounts);\n\n // Remove service\n delete compose.services[serviceId];\n\n // If purge, also remove associated named volumes from top-level\n if (options?.purge && compose.volumes && namedVolumes.length > 0) {\n for (const vol of namedVolumes) {\n if (vol in (compose.volumes as Record<string, unknown>)) {\n delete (compose.volumes as Record<string, unknown>)[vol];\n }\n }\n }\n\n // Write updated compose\n writeComposeFile(composePath, compose);\n\n return { success: true, composePath, backupPath };\n}\n\n/**\n * Check whether a service exists in a docker-compose.yml file.\n */\nexport function isServiceInCompose(\n serviceId: string,\n composePath: string,\n): boolean {\n if (!existsSync(composePath)) {\n return false;\n }\n\n try {\n const compose = readComposeFile(composePath);\n return !!(compose.services && compose.services[serviceId]);\n } catch {\n return false;\n }\n}\n","/**\n * Log Aggregator — unified log reading from 4 sources.\n *\n * Reads CLI JSONL, Tunnel NDJSON, Traefik access log, and Docker container logs,\n * transforms each to UnifiedLogEntry, and supports querying with filters and pagination.\n *\n * @module utils/log-aggregator\n */\n\nimport { readFileSync, readdirSync, existsSync } from 'node:fs';\nimport { join, basename } from 'node:path';\nimport { homedir } from 'node:os';\nimport type {\n LogSource,\n UnifiedLogLevel,\n UnifiedLogEntry,\n LogQuery,\n LogQueryResult,\n LogStats,\n} from '@brewnet/shared';\nimport {\n LOG_QUERY_DEFAULT_LIMIT,\n LOG_QUERY_MAX_LIMIT,\n} from '@brewnet/shared';\nimport { runRotation } from './log-rotation.js';\n\n// ─── Duration Parsing ───────────────────────────────────────────────────────\n\n/**\n * Parse a human-friendly duration string (e.g. \"1h\", \"30m\", \"7d\") or ISO date\n * into an absolute ISO 8601 timestamp.\n *\n * Supported formats: Nh (hours), Nm (minutes), Nd (days), ISO 8601 date/datetime.\n */\nexport function parseDuration(input: string): string {\n const match = input.match(/^(\\d+)([hmd])$/);\n if (match) {\n const value = parseInt(match[1], 10);\n const unit = match[2];\n const now = Date.now();\n let ms = 0;\n switch (unit) {\n case 'h':\n ms = value * 60 * 60 * 1000;\n break;\n case 'm':\n ms = value * 60 * 1000;\n break;\n case 'd':\n ms = value * 24 * 60 * 60 * 1000;\n break;\n }\n return new Date(now - ms).toISOString();\n }\n\n // Try parsing as ISO date\n const date = new Date(input);\n if (!isNaN(date.getTime())) {\n return date.toISOString();\n }\n\n throw new Error(\n `Invalid time format: '${input}'. Use: 1h, 30m, 1d, or ISO date (2026-03-15)`,\n );\n}\n\n// ─── Source Readers ─────────────────────────────────────────────────────────\n\n/**\n * Read CLI JSONL log files (brewnet-YYYY-MM-DD.log) and transform to UnifiedLogEntry[].\n */\nexport function readCliLogs(logsDir: string, since?: string): UnifiedLogEntry[] {\n if (!existsSync(logsDir)) return [];\n\n const files = readdirSync(logsDir)\n .filter((f) => /^brewnet-\\d{4}-\\d{2}-\\d{2}\\.log$/.test(f))\n .sort();\n\n // If since is specified, skip files whose date is before the since date\n const sinceDate = since ? since.slice(0, 10) : undefined;\n\n const entries: UnifiedLogEntry[] = [];\n for (const file of files) {\n if (sinceDate) {\n const fileDate = file.replace('brewnet-', '').replace('.log', '');\n if (fileDate < sinceDate) continue;\n }\n\n const content = readFileSync(join(logsDir, file), 'utf-8');\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line) as {\n timestamp: string;\n level: string;\n command: string;\n message: string;\n metadata: Record<string, unknown>;\n };\n\n if (since && parsed.timestamp < since) continue;\n\n entries.push({\n timestamp: parsed.timestamp,\n source: 'cli',\n level: parsed.level as UnifiedLogLevel,\n message: parsed.message,\n metadata: { command: parsed.command, ...parsed.metadata },\n });\n } catch {\n // Skip malformed lines\n }\n }\n }\n return entries;\n}\n\n/**\n * Read Tunnel NDJSON log file (tunnel.log) and transform to UnifiedLogEntry[].\n */\nexport function readTunnelLogs(logsDir: string, since?: string): UnifiedLogEntry[] {\n const logFile = join(logsDir, 'tunnel.log');\n if (!existsSync(logFile)) return [];\n\n const content = readFileSync(logFile, 'utf-8');\n const entries: UnifiedLogEntry[] = [];\n\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line) as {\n timestamp: string;\n event: string;\n tunnelMode: string;\n tunnelId?: string;\n tunnelName?: string;\n domain?: string;\n detail: string;\n error?: string;\n };\n\n if (since && parsed.timestamp < since) continue;\n\n const metadata: Record<string, unknown> = {\n event: parsed.event,\n tunnelMode: parsed.tunnelMode,\n };\n if (parsed.tunnelId) metadata.tunnelId = parsed.tunnelId;\n if (parsed.tunnelName) metadata.tunnelName = parsed.tunnelName;\n\n entries.push({\n timestamp: parsed.timestamp,\n source: 'tunnel',\n level: parsed.error ? 'error' : 'info',\n service: parsed.domain,\n message: parsed.detail,\n metadata,\n });\n } catch {\n // Skip malformed lines\n }\n }\n return entries;\n}\n\n/**\n * Read Traefik JSON access log and transform to UnifiedLogEntry[].\n */\nexport function readAccessLogs(projectPath: string, since?: string): UnifiedLogEntry[] {\n const logFile = join(projectPath, 'logs', 'access.log');\n if (!existsSync(logFile)) return [];\n\n const content = readFileSync(logFile, 'utf-8');\n const entries: UnifiedLogEntry[] = [];\n\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line) as {\n StartUTC: string;\n OriginStatus: number;\n ServiceName?: string;\n RequestMethod: string;\n RequestPath: string;\n RouterName?: string;\n ClientAddr?: string;\n Duration?: number;\n RequestHost?: string;\n request_User_Agent?: string;\n 'request_User-Agent'?: string;\n };\n\n if (since && parsed.StartUTC < since) continue;\n\n let level: UnifiedLogLevel = 'info';\n if (parsed.OriginStatus >= 500) level = 'error';\n else if (parsed.OriginStatus >= 400) level = 'warn';\n\n const metadata: Record<string, unknown> = {};\n if (parsed.RouterName) metadata.routerName = parsed.RouterName;\n if (parsed.ClientAddr) metadata.clientAddr = parsed.ClientAddr;\n if (parsed.Duration !== undefined) metadata.duration = parsed.Duration;\n if (parsed.RequestHost) metadata.requestHost = parsed.RequestHost;\n const userAgent = parsed['request_User-Agent'] ?? parsed.request_User_Agent;\n if (userAgent) metadata.userAgent = userAgent;\n\n // Extract clean service name from Traefik's \"serviceName@provider\" format\n const serviceName = parsed.ServiceName?.split('@')[0];\n\n entries.push({\n timestamp: parsed.StartUTC,\n source: 'access',\n level,\n service: serviceName,\n message: `${parsed.RequestMethod} ${parsed.RequestPath} → ${parsed.OriginStatus}`,\n metadata,\n });\n } catch {\n // Skip malformed lines\n }\n }\n return entries;\n}\n\n/**\n * Read Docker container logs via dockerode.\n *\n * Uses 8-byte multiplexed stream format:\n * - Byte 0: stream type (1=stdout → info, 2=stderr → error)\n * - Bytes 4-7: payload size (big-endian uint32)\n * - Remaining: payload text\n */\nexport async function readServiceLogs(\n projectPath: string,\n opts?: { since?: string; tail?: number },\n): Promise<UnifiedLogEntry[]> {\n let Dockerode: typeof import('dockerode');\n try {\n Dockerode = (await import('dockerode')).default;\n } catch {\n return [];\n }\n\n const docker = new Dockerode();\n const entries: UnifiedLogEntry[] = [];\n\n try {\n const containers = await docker.listContainers({ all: false });\n // Filter containers belonging to this project by working directory label or name prefix\n const projectName = basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '');\n\n for (const containerInfo of containers) {\n const containerName =\n containerInfo.Names?.[0]?.replace(/^\\//, '') ?? containerInfo.Id.slice(0, 12);\n\n // Match containers that belong to this project (docker compose prefixes with project name)\n if (!containerName.toLowerCase().startsWith(projectName)) continue;\n\n const container = docker.getContainer(containerInfo.Id);\n const logOpts: Record<string, unknown> = {\n stdout: true,\n stderr: true,\n timestamps: true,\n };\n if (opts?.tail) logOpts.tail = opts.tail;\n if (opts?.since) {\n logOpts.since = Math.floor(new Date(opts.since).getTime() / 1000);\n }\n\n try {\n const logBuffer = (await container.logs(logOpts)) as Buffer;\n\n // Demultiplex Docker's 8-byte header format:\n // [stream_type(1), 0, 0, 0, size(4 big-endian)] + payload\n const frames: { stream: number; text: string }[] = [];\n if (Buffer.isBuffer(logBuffer)) {\n let pos = 0;\n while (pos + 8 <= logBuffer.length) {\n const streamType = logBuffer[pos];\n const payloadSize = logBuffer.readUInt32BE(pos + 4);\n pos += 8;\n if (pos + payloadSize > logBuffer.length) break;\n const payload = logBuffer.subarray(pos, pos + payloadSize).toString('utf-8');\n frames.push({ stream: streamType, text: payload });\n pos += payloadSize;\n }\n } else {\n // Fallback if logs() returns a plain string (no multiplexing)\n frames.push({ stream: 1, text: String(logBuffer) });\n }\n\n for (const frame of frames) {\n const level: UnifiedLogLevel = frame.stream === 2 ? 'error' : 'info';\n for (const line of frame.text.split('\\n')) {\n if (!line.trim()) continue;\n\n // Docker timestamp format at the start of each line\n const tsMatch = line.match(\n /(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?)\\s+(.*)/,\n );\n if (!tsMatch) continue;\n\n const timestamp = tsMatch[1].endsWith('Z') ? tsMatch[1] : tsMatch[1] + 'Z';\n const message = tsMatch[2];\n\n if (opts?.since && timestamp < opts.since) continue;\n\n // Strip project prefix from container name for clean service name\n const serviceName = containerName\n .replace(new RegExp(`^${projectName}[-_]`), '')\n .replace(/-\\d+$/, '');\n\n entries.push({\n timestamp,\n source: 'service',\n level,\n service: serviceName,\n message,\n metadata: { containerId: containerInfo.Id.slice(0, 12) },\n });\n }\n }\n } catch {\n // Container may have stopped between list and logs call\n }\n }\n } catch {\n // Docker not available or not running\n }\n\n return entries;\n}\n\n// ─── Query Engine ───────────────────────────────────────────────────────────\n\n/**\n * Query logs from all sources with filtering, sorting, and pagination.\n */\nexport async function queryLogs(\n query: LogQuery,\n projectPath: string,\n): Promise<LogQueryResult> {\n const logsDir = join(homedir(), '.brewnet', 'logs');\n const since = query.since;\n\n // Determine which sources to read\n const sourcesToRead: LogSource[] = query.sources ?? ['cli', 'tunnel', 'access', 'service'];\n\n // Read sources in parallel\n const readers: Promise<UnifiedLogEntry[]>[] = [];\n\n if (sourcesToRead.includes('cli')) {\n readers.push(Promise.resolve(readCliLogs(logsDir, since)));\n }\n if (sourcesToRead.includes('tunnel')) {\n readers.push(Promise.resolve(readTunnelLogs(logsDir, since)));\n }\n if (sourcesToRead.includes('access')) {\n readers.push(Promise.resolve(readAccessLogs(projectPath, since)));\n }\n if (sourcesToRead.includes('service')) {\n readers.push(readServiceLogs(projectPath, { since }));\n }\n\n const results = await Promise.all(readers);\n let entries = results.flat();\n\n // Run log rotation after reading (best-effort, never blocks queries)\n try {\n runRotation(logsDir, projectPath);\n } catch {\n // Rotation failure should never block log queries\n }\n\n // Apply filters\n if (query.levels?.length) {\n entries = entries.filter((e) => query.levels!.includes(e.level));\n }\n if (query.services?.length) {\n entries = entries.filter((e) => e.service && query.services!.includes(e.service));\n }\n if (query.until) {\n entries = entries.filter((e) => e.timestamp <= query.until!);\n }\n if (query.search) {\n const searchLower = query.search.toLowerCase();\n entries = entries.filter((e) => e.message.toLowerCase().includes(searchLower));\n }\n\n // Sort by timestamp descending\n entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n\n const total = entries.length;\n const limit = Math.min(query.limit ?? LOG_QUERY_DEFAULT_LIMIT, LOG_QUERY_MAX_LIMIT);\n const offset = query.offset ?? 0;\n const paged = entries.slice(offset, offset + limit);\n\n return {\n entries: paged,\n total,\n hasMore: offset + limit < total,\n };\n}\n\n/**\n * Compute aggregated statistics from all log sources.\n * Reads all entries (bypassing pagination) to produce accurate counts.\n */\nexport async function getLogStats(projectPath: string): Promise<LogStats> {\n const logsDir = join(homedir(), '.brewnet', 'logs');\n\n // Read all sources directly (no pagination) for accurate stats\n const [cliEntries, tunnelEntries, accessEntries, serviceEntries] = await Promise.all([\n Promise.resolve(readCliLogs(logsDir)),\n Promise.resolve(readTunnelLogs(logsDir)),\n Promise.resolve(readAccessLogs(projectPath)),\n readServiceLogs(projectPath, {}),\n ]);\n const allEntries = [...cliEntries, ...tunnelEntries, ...accessEntries, ...serviceEntries];\n\n const bySource: Record<LogSource, number> = { cli: 0, tunnel: 0, access: 0, service: 0 };\n const byLevel: Record<string, number> = { info: 0, warn: 0, error: 0, debug: 0 };\n\n for (const entry of allEntries) {\n bySource[entry.source]++;\n byLevel[entry.level] = (byLevel[entry.level] ?? 0) + 1;\n }\n\n // Sort descending to pick recent errors\n allEntries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n const recentErrors = allEntries.filter((e) => e.level === 'error').slice(0, 10);\n\n return {\n total: allEntries.length,\n bySource,\n byLevel,\n recentErrors,\n lastUpdated: new Date().toISOString(),\n };\n}\n","/**\n * Brewnet CLI — Backup Manager (T098)\n *\n * Provides backup and restore operations for Brewnet projects.\n * Archives project directories as .tar.gz files and supports\n * listing, restoring, and disk space validation.\n *\n * @module services/backup-manager\n */\n\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n statSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport { execSync } from 'node:child_process';\nimport crypto from 'node:crypto';\n\nimport { BrewnetError } from '../utils/errors.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A record describing a single backup snapshot. */\nexport interface BackupRecord {\n id: string;\n timestamp: number;\n path: string;\n size: number;\n projectName: string;\n}\n\n/** Result of a disk space check. */\nexport interface DiskSpaceResult {\n available: number; // bytes\n required: number; // bytes\n sufficient: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Utility helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a unique backup ID based on timestamp and random suffix.\n * Format: `backup-<timestamp>-<random6hex>`\n */\nexport function generateBackupId(): string {\n const ts = Date.now();\n const rand = crypto.randomBytes(3).toString('hex');\n return `backup-${ts}-${rand}`;\n}\n\n/**\n * Derive the project name from the project path.\n * Uses the last segment of the path as the project name.\n */\nexport function deriveProjectName(projectPath: string): string {\n const segments = projectPath.replace(/\\/+$/, '').split('/');\n return segments[segments.length - 1] || 'unknown';\n}\n\n/**\n * Build the archive filename for a backup.\n */\nexport function buildArchiveFilename(backupId: string): string {\n return `${backupId}.tar.gz`;\n}\n\n// ---------------------------------------------------------------------------\n// Core operations\n// ---------------------------------------------------------------------------\n\n/**\n * Create a backup of the project directory.\n *\n * Archives the contents of `projectPath` into a .tar.gz file stored\n * in `backupsDir`. Returns a BackupRecord describing the created backup.\n *\n * @param projectPath - The project directory to back up\n * @param backupsDir - The directory where backup archives are stored\n * @returns A BackupRecord with id, timestamp, path, size, projectName\n */\nexport function createBackup(projectPath: string, backupsDir: string): BackupRecord {\n mkdirSync(backupsDir, { recursive: true });\n\n const backupId = generateBackupId();\n const archiveFilename = buildArchiveFilename(backupId);\n const archivePath = join(backupsDir, archiveFilename);\n const timestamp = Date.now();\n\n // Create .tar.gz archive of the project directory contents.\n // Using -C to change into the parent dir and archiving the basename\n // so that extraction is relative.\n const parentDir = join(projectPath, '..');\n const baseName = deriveProjectName(projectPath);\n\n execSync(`tar -czf \"${archivePath}\" -C \"${parentDir}\" \"${baseName}\"`, {\n stdio: 'pipe',\n });\n\n const stats = statSync(archivePath);\n\n return {\n id: backupId,\n timestamp,\n path: archivePath,\n size: stats.size,\n projectName: baseName,\n };\n}\n\n/**\n * Restore a backup by extracting the archive to the project path.\n *\n * @param backupId - The ID of the backup to restore\n * @param backupsDir - The directory where backup archives are stored\n * @param projectPath - The target directory to restore into\n * @throws {BrewnetError} BN008 when the backup archive is not found\n */\nexport function restoreBackup(backupId: string, backupsDir: string, projectPath: string): void {\n const archiveFilename = buildArchiveFilename(backupId);\n const archivePath = join(backupsDir, archiveFilename);\n\n if (!existsSync(archivePath)) {\n throw BrewnetError.resourceNotFound(`backup:${backupId}`);\n }\n\n // Ensure the project directory exists\n mkdirSync(projectPath, { recursive: true });\n\n // Extract the archive — strip the top-level directory component so that\n // contents are placed directly into projectPath\n execSync(`tar -xzf \"${archivePath}\" -C \"${projectPath}\" --strip-components=1`, {\n stdio: 'pipe',\n });\n}\n\n/**\n * List all backups in the backups directory, sorted by timestamp (newest first).\n *\n * @param backupsDir - The directory where backup archives are stored\n * @returns Array of BackupRecord, newest first\n */\nexport function listBackups(backupsDir: string): BackupRecord[] {\n if (!existsSync(backupsDir)) {\n return [];\n }\n\n const files = readdirSync(backupsDir).filter((f) => f.endsWith('.tar.gz'));\n const records: BackupRecord[] = [];\n\n for (const file of files) {\n const filePath = join(backupsDir, file);\n const stats = statSync(filePath);\n\n // Parse the backup ID from the filename (remove .tar.gz)\n const backupId = file.replace(/\\.tar\\.gz$/, '');\n\n // Extract timestamp from backup ID format: backup-<timestamp>-<random>\n const parts = backupId.split('-');\n const timestamp = parts.length >= 2 ? parseInt(parts[1], 10) : stats.mtimeMs;\n\n // Derive project name by inspecting the archive listing.\n // The first entry is usually the top-level directory like \"my-project/\"\n let projectName = 'unknown';\n try {\n const listing = execSync(`tar -tzf \"${filePath}\" | head -1`, {\n stdio: 'pipe',\n encoding: 'utf-8',\n }).trim();\n projectName = listing.replace(/\\/$/, '').split('/')[0] || 'unknown';\n } catch {\n // Fall back to unknown\n }\n\n records.push({\n id: backupId,\n timestamp,\n path: filePath,\n size: stats.size,\n projectName,\n });\n }\n\n // Sort by timestamp, newest first\n records.sort((a, b) => b.timestamp - a.timestamp);\n\n return records;\n}\n\n/**\n * Check available disk space at the given path.\n *\n * @param path - The filesystem path to check\n * @param requiredBytes - The number of bytes required (optional, defaults to 0)\n * @returns DiskSpaceResult with available, required, and sufficient fields\n */\nexport function checkDiskSpace(path: string, requiredBytes: number = 0): DiskSpaceResult {\n // Use `df` to get available space in 1K blocks, then convert to bytes\n let available: number;\n try {\n // macOS and Linux both support `df -k`\n const output = execSync(`df -k \"${path}\" | tail -1`, {\n stdio: 'pipe',\n encoding: 'utf-8',\n }).trim();\n\n // df output columns: Filesystem 1K-blocks Used Available Use% Mounted-on\n const columns = output.split(/\\s+/);\n const availableKB = parseInt(columns[3], 10);\n available = availableKB * 1024;\n } catch {\n // If df fails, return 0 available\n available = 0;\n }\n\n return {\n available,\n required: requiredBytes,\n sufficient: available >= requiredBytes,\n };\n}\n","/**\n * Brewnet CLI — Structured Error System (T014)\n *\n * All Brewnet errors carry a machine-readable code (BN001-BN010),\n * an HTTP-equivalent status, and a human-readable remediation hint.\n *\n * @module utils/errors\n */\n\nexport type BrewnetErrorCode =\n | 'BN001'\n | 'BN002'\n | 'BN003'\n | 'BN004'\n | 'BN005'\n | 'BN006'\n | 'BN007'\n | 'BN008'\n | 'BN009'\n | 'BN010';\n\nexport class BrewnetError extends Error {\n readonly code: BrewnetErrorCode;\n readonly httpStatus: number;\n readonly remediation: string;\n\n constructor(\n code: BrewnetErrorCode,\n message: string,\n httpStatus: number,\n remediation: string,\n ) {\n super(message);\n this.name = 'BrewnetError';\n this.code = code;\n this.httpStatus = httpStatus;\n this.remediation = remediation;\n\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Format the error for terminal display.\n *\n * Example output:\n *\n * Error [BN001]: Docker daemon is not running\n *\n * Docker is required to manage services. Please start Docker and try again.\n *\n * Fix:\n * macOS: Open Docker Desktop\n * Linux: sudo systemctl start docker\n */\n format(): string {\n const lines: string[] = [\n `Error [${this.code}]: ${this.message}`,\n '',\n ` ${this.remediation}`,\n ];\n return lines.join('\\n');\n }\n\n /**\n * Serialize the error for structured logging (JSONL).\n */\n toJSON(): Record<string, unknown> {\n return {\n code: this.code,\n message: this.message,\n httpStatus: this.httpStatus,\n remediation: this.remediation,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Factory methods for every Brewnet error code\n // ---------------------------------------------------------------------------\n\n /**\n * BN001 — Docker daemon is not running (503 Service Unavailable).\n */\n static dockerNotRunning(): BrewnetError {\n return new BrewnetError(\n 'BN001',\n 'Docker daemon is not running',\n 503,\n [\n 'Docker is required to manage services. Please start Docker and try again.',\n '',\n ' Fix:',\n ' macOS: Open Docker Desktop',\n ' Linux: sudo systemctl start docker',\n ].join('\\n'),\n );\n }\n\n /**\n * BN002 — Project directory already exists (409 Conflict).\n */\n static directoryConflict(name: string): BrewnetError {\n return new BrewnetError(\n 'BN002',\n `Directory \"${name}\" already exists`,\n 409,\n [\n 'The project directory already exists. Choose a different project name or remove it first.',\n '',\n ' Fix:',\n ` rm -rf ${name} # remove existing directory`,\n ` brewnet create-app ${name}-v2 # use a different name`,\n ].join('\\n'),\n );\n }\n\n /**\n * BN002 — Port already in use (409 Conflict).\n */\n static portConflict(port: number, processInfo?: string): BrewnetError {\n const detail = processInfo\n ? ` (in use by ${processInfo})`\n : '';\n return new BrewnetError(\n 'BN002',\n `Port ${port} is already in use${detail}`,\n 409,\n [\n `Port ${port} is required by one of your selected services.`,\n '',\n ' Fix:',\n ` 1. Find the process: lsof -i :${port}`,\n ` 2. Stop it: kill <PID>`,\n ' 3. Or choose a different port in your configuration.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN003 — SSL certificate issuance failed (500 Internal Server Error).\n */\n static sslFailed(domain: string): BrewnetError {\n return new BrewnetError(\n 'BN003',\n `SSL certificate issuance failed for ${domain}`,\n 500,\n [\n 'Let\\'s Encrypt could not issue a certificate. Common causes:',\n '',\n ' - DNS records not yet propagated (wait a few minutes and retry)',\n ' - Domain does not resolve to this server\\'s public IP',\n ' - Rate limit reached (max 5 duplicates per week)',\n '',\n ' Fix:',\n ` 1. Verify DNS: dig +short ${domain}`,\n ' 2. Retry: brewnet domain ssl ' + domain,\n ' 3. Use Cloudflare Tunnel for automatic SSL instead.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN004 — Invalid license key (401 Unauthorized).\n */\n static invalidLicense(): BrewnetError {\n return new BrewnetError(\n 'BN004',\n 'Invalid or expired license key',\n 401,\n [\n 'Your Brewnet Pro/Team license key is invalid or has expired.',\n '',\n ' Fix:',\n ' 1. Check your key at https://brewnet.dev/account',\n ' 2. Update it: brewnet config set license <KEY>',\n ' 3. Contact support if the issue persists.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN005 — Rate limit exceeded (429 Too Many Requests).\n */\n static rateLimited(): BrewnetError {\n return new BrewnetError(\n 'BN005',\n 'Rate limit exceeded',\n 429,\n [\n 'Too many requests in a short period. Please wait and try again.',\n '',\n ' Fix:',\n ' Wait a few minutes before retrying.',\n ' If using CI, consider adding a delay between requests.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN006 — Boilerplate clone failed (500 Internal Server Error).\n */\n static cloneFailed(stackId: string): BrewnetError {\n return new BrewnetError(\n 'BN006',\n `Failed to clone boilerplate stack \"${stackId}\"`,\n 500,\n [\n 'Could not download the boilerplate from GitHub. Common causes:',\n '',\n ' - No internet connection',\n ' - GitHub is temporarily unavailable',\n ' - The stack branch does not exist on the remote',\n '',\n ' Fix:',\n ' 1. Check your internet connection',\n ' 2. Verify connectivity: curl -I https://github.com',\n ' 3. Retry: brewnet create-app <name> --stack ' + stackId,\n ].join('\\n'),\n );\n }\n\n /**\n * BN006 — Health check timed out after scaffolding (500 Internal Server Error).\n */\n static healthCheckTimeout(timeoutSec: number): BrewnetError {\n return new BrewnetError(\n 'BN006',\n `Application health check timed out after ${timeoutSec}s`,\n 500,\n [\n 'The application container started but did not respond to health checks in time.',\n '',\n ' Containers are still running. To diagnose:',\n ' docker compose logs backend # check for startup errors',\n ' docker compose logs # check all services',\n '',\n ' Fix:',\n ' 1. Check logs for errors (missing env vars, port conflicts, build errors)',\n ' 2. Run \"make down\" to stop containers',\n ' 3. Retry: brewnet create-app <name> --stack <STACK_ID>',\n ].join('\\n'),\n );\n }\n\n /**\n * BN006 — Build failed (500 Internal Server Error).\n */\n static buildFailed(logs?: string): BrewnetError {\n const logSnippet = logs\n ? `\\n\\n Build output (last lines):\\n ${logs.split('\\n').slice(-5).join('\\n ')}`\n : '';\n return new BrewnetError(\n 'BN006',\n 'Application build failed',\n 500,\n [\n 'The build process exited with a non-zero status.',\n '',\n ' Fix:',\n ' 1. Check your build command in brewnet.yml',\n ' 2. Run the build locally to reproduce the error',\n ' 3. Review logs: brewnet logs --build',\n logSnippet,\n ].join('\\n'),\n );\n }\n\n /**\n * BN007 — Invalid Git repository (400 Bad Request).\n */\n static invalidGitRepo(path: string): BrewnetError {\n return new BrewnetError(\n 'BN007',\n `Not a valid Git repository: ${path}`,\n 400,\n [\n 'The specified path is not a Git repository or is inaccessible.',\n '',\n ' Fix:',\n ` 1. Verify the path exists: ls -la ${path}`,\n ` 2. Initialize if needed: git init ${path}`,\n ' 3. Or clone an existing repo: git clone <url>',\n ].join('\\n'),\n );\n }\n\n /**\n * BN008 — Resource not found (404 Not Found).\n */\n static resourceNotFound(resource: string): BrewnetError {\n return new BrewnetError(\n 'BN008',\n `Resource not found: ${resource}`,\n 404,\n [\n `The requested resource \"${resource}\" could not be found.`,\n '',\n ' Fix:',\n ' 1. Check the name or ID for typos',\n ' 2. List available resources: brewnet status',\n ' 3. The resource may have been removed or renamed.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN009 — Database error (500 Internal Server Error).\n */\n static databaseError(detail: string): BrewnetError {\n return new BrewnetError(\n 'BN009',\n `Database error: ${detail}`,\n 500,\n [\n 'An internal database operation failed.',\n '',\n ' Fix:',\n ' 1. Check disk space: df -h',\n ' 2. Verify DB file permissions: ls -la ~/.brewnet/db/',\n ' 3. Try resetting the local DB: brewnet config reset-db',\n ' 4. If the issue persists, file a bug report.',\n ].join('\\n'),\n );\n }\n\n /**\n * BN010 — Feature requires Pro plan (403 Forbidden).\n */\n static proRequired(feature: string): BrewnetError {\n return new BrewnetError(\n 'BN010',\n `\"${feature}\" requires a Brewnet Pro subscription`,\n 403,\n [\n 'This feature is available on the Pro plan ($9/mo) or Team plan ($29/mo/server).',\n '',\n ' Upgrade:',\n ' https://brewnet.dev/pricing',\n '',\n ' Activate:',\n ' brewnet config set license <KEY>',\n ].join('\\n'),\n );\n }\n}\n\n/**\n * Type-guard to check if an unknown value is a BrewnetError.\n */\nexport function isBrewnetError(err: unknown): err is BrewnetError {\n return err instanceof BrewnetError;\n}\n","/**\n * DomainManager — Core lifecycle service for domain external access.\n *\n * Orchestrates connect / disconnect / list / status operations for mapping\n * local Brewnet services to external domains via Cloudflare Tunnel.\n *\n * Shared by both CLI commands and Admin Server REST API.\n *\n * @module services/domain-manager\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { execa } from 'execa';\nimport type { WizardState, DomainConnection, DomainScenario } from '@brewnet/shared';\nimport {\n getDnsRecords,\n deleteDnsRecord,\n createDnsRecord,\n configureTunnelIngress,\n getTunnelHealth,\n getActiveServiceRoutes,\n type ServiceRoute,\n} from './cloudflare-client.js';\nimport { addExternalLabels, removeExternalLabels } from './compose-generator.js';\nimport { loadState, saveState } from '../wizard/state.js';\nimport { readApps } from './app-registry.js';\nimport { getStackById } from '../config/stacks.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ConnectOptions {\n /** Overwrite existing CNAME record if conflict detected */\n force?: boolean;\n /** Scenario override (auto-detected if omitted) */\n scenario?: DomainScenario;\n /** Optional line-by-line progress logger (e.g. to push into SSE stream) */\n onLog?: (line: string) => void;\n}\n\nexport interface StepResult {\n step: string;\n status: 'completed' | 'failed' | 'skipped';\n durationMs?: number;\n error?: string;\n}\n\nexport interface ConnectResult {\n success: boolean;\n hostname: string;\n externalUrl: string;\n steps: StepResult[];\n error?: string;\n}\n\nexport interface DisconnectResult {\n success: boolean;\n appName: string;\n removedHostname: string;\n steps: StepResult[];\n error?: string;\n}\n\nexport interface AppInfo {\n name: string;\n containerName: string;\n port: number;\n running: boolean;\n alreadyConnected: boolean;\n hostname?: string;\n}\n\nexport interface DomainStatusInfo {\n appName: string;\n local: { url: string; healthy: boolean };\n external: { url: string; dnsResolved: boolean; httpsReachable: boolean };\n tunnel: { status: 'healthy' | 'degraded' | 'inactive'; connectorCount: number };\n dns: { type: string; name: string; content: string; proxied: boolean } | null;\n}\n\n// ---------------------------------------------------------------------------\n// DomainManager\n// ---------------------------------------------------------------------------\n\nexport class DomainManager {\n private projectName: string;\n private state: WizardState;\n\n constructor(projectName: string) {\n this.projectName = projectName;\n const loaded = loadState(projectName);\n if (!loaded) {\n throw new Error(`Project \"${projectName}\" not found. Run \\`brewnet init\\` first.`);\n }\n this.state = loaded;\n }\n\n /** Reload state from disk */\n reload(): void {\n const loaded = loadState(this.projectName);\n if (loaded) this.state = loaded;\n }\n\n /** Get a copy of the current state */\n getState(): WizardState {\n return structuredClone(this.state);\n }\n\n // ── connect ──────────────────────────────────────────────────────────────\n\n /**\n * Connect a local app to an external domain via Cloudflare Tunnel.\n *\n * Steps: health check → ingress update → DNS create → Traefik labels → persist → poll DNS\n * Rolls back on failure.\n */\n async connect(\n appName: string,\n subdomain: string,\n domain: string,\n options: ConnectOptions = {},\n ): Promise<ConnectResult> {\n const hostname = `${subdomain}.${domain}`;\n const steps: StepResult[] = [];\n const cf = this.state.domain.cloudflare;\n const log = (msg: string) => options.onLog?.(`[domain-connect] ${msg}`);\n\n log(`start: app=${appName} subdomain=${subdomain} domain=${domain}`);\n log(`cf state: tunnelId=${cf.tunnelId || '(empty)'} apiToken=${cf.apiToken ? '***' : '(empty)'} accountId=${cf.accountId || '(empty)'} zoneId=${cf.zoneId || '(empty)'}`);\n\n if (!cf.tunnelId || !cf.apiToken) {\n const err = 'Cloudflare credentials not configured. Set API token and tunnel ID first.';\n log(`FAIL: ${err}`);\n return {\n success: false,\n hostname,\n externalUrl: `https://${hostname}`,\n steps,\n error: err,\n };\n }\n\n // Determine container port for the app\n const containerPort = this.resolveContainerPort(appName);\n log(`resolveContainerPort(${appName}) → ${containerPort ?? 'null'}`);\n if (!containerPort) {\n const err = `Cannot determine container port for app \"${appName}\".`;\n log(`FAIL: ${err}`);\n return {\n success: false,\n hostname,\n externalUrl: `https://${hostname}`,\n steps,\n error: err,\n };\n }\n\n // Determine scenario\n const scenario = options.scenario ?? this.detectScenario();\n log(`scenario: ${scenario}`);\n\n // Step 1: Health check (local)\n log(`step 1/6: health check → http://127.0.0.1:${containerPort}/`);\n const healthStart = Date.now();\n try {\n const healthy = await this.checkLocalHealth(appName, containerPort);\n if (!healthy) {\n const err = `App \"${appName}\" not responding on port ${containerPort}`;\n log(`FAIL step 1: ${err}`);\n steps.push({ step: 'health_check', status: 'failed', error: err });\n return { success: false, hostname, externalUrl: `https://${hostname}`, steps, error: `APP_NOT_RUNNING: Local health check failed for ${appName} on port ${containerPort}` };\n }\n steps.push({ step: 'health_check', status: 'completed', durationMs: Date.now() - healthStart });\n log(`step 1 OK (${Date.now() - healthStart}ms)`);\n } catch (err) {\n log(`FAIL step 1 exception: ${err}`);\n steps.push({ step: 'health_check', status: 'failed', error: String(err) });\n return { success: false, hostname, externalUrl: `https://${hostname}`, steps, error: `Health check failed: ${err}` };\n }\n\n // Step 2: Update tunnel ingress\n log(`step 2/6: configure tunnel ingress (accountId=${cf.accountId || '(empty)'}, tunnelId=${cf.tunnelId})`);\n const ingressStart = Date.now();\n let previousIngress: ServiceRoute[] | null = null;\n try {\n previousIngress = getActiveServiceRoutes(this.state);\n const projectDomain = this.state.domain.zoneName;\n const builtinRoutes = previousIngress.map((r) => ({ ...r, domain: projectDomain }));\n const existingExtRoutes = (this.state.domainConnections ?? [])\n .filter((c) => c.appName !== appName)\n .map((c) => ({ subdomain: c.subdomain, containerName: this.resolveContainerName(c.appName), port: c.containerPort, domain: c.domain }));\n const newRoute: ServiceRoute = { subdomain, containerName: this.resolveContainerName(appName), port: containerPort, domain };\n const allRoutes = [...builtinRoutes, ...existingExtRoutes, newRoute];\n log(`ingress routes: ${JSON.stringify(allRoutes.map((r) => `${r.subdomain} → ${r.containerName}:${r.port}`))}`);\n await configureTunnelIngress(cf.apiToken, cf.accountId, cf.tunnelId, domain, allRoutes);\n steps.push({ step: 'ingress_update', status: 'completed', durationMs: Date.now() - ingressStart });\n log(`step 2 OK (${Date.now() - ingressStart}ms)`);\n } catch (err) {\n log(`FAIL step 2: ${err}`);\n steps.push({ step: 'ingress_update', status: 'failed', error: String(err) });\n return { success: false, hostname, externalUrl: `https://${hostname}`, steps, error: `Ingress update failed: ${err}` };\n }\n\n // Step 3: Create DNS CNAME record\n log(`step 3/6: DNS CNAME check/create for ${hostname} (zoneId=${cf.zoneId || '(empty)'})`);\n const dnsStart = Date.now();\n let cnameRecordId = '';\n try {\n // Check for existing CNAME\n const existing = await getDnsRecords(cf.apiToken, cf.zoneId, hostname);\n log(`existing DNS records for ${hostname}: ${existing.length}`);\n if (existing.length > 0 && !options.force) {\n // Rollback ingress\n await this.rollbackIngress(cf, previousIngress, domain);\n const err = `CNAME_CONFLICT: A CNAME record already exists for ${hostname}`;\n log(`FAIL step 3: ${err}`);\n steps.push({ step: 'dns_creation', status: 'failed', error: err });\n return { success: false, hostname, externalUrl: `https://${hostname}`, steps, error: `CNAME_CONFLICT` };\n }\n if (existing.length > 0 && options.force) {\n // Delete existing before creating\n for (const rec of existing) {\n await deleteDnsRecord(cf.apiToken, cf.zoneId, rec.id);\n }\n log(`deleted ${existing.length} existing CNAME record(s)`);\n }\n await createDnsRecord(cf.apiToken, cf.zoneId, cf.tunnelId, subdomain, domain);\n // Fetch the record ID for future deletion\n const created = await getDnsRecords(cf.apiToken, cf.zoneId, hostname);\n cnameRecordId = created[0]?.id ?? '';\n steps.push({ step: 'dns_creation', status: 'completed', durationMs: Date.now() - dnsStart });\n log(`step 3 OK — cnameRecordId=${cnameRecordId} (${Date.now() - dnsStart}ms)`);\n } catch (err) {\n // Rollback ingress\n await this.rollbackIngress(cf, previousIngress, domain);\n log(`FAIL step 3: ${err}`);\n steps.push({ step: 'dns_creation', status: 'failed', error: String(err) });\n return { success: false, hostname, externalUrl: `https://${hostname}`, steps, error: `DNS creation failed: ${err}` };\n }\n\n // Step 4: Add Traefik external labels\n log(`step 4/6: Traefik labels for ${appName}`);\n try {\n const composePath = this.getComposePath();\n if (fs.existsSync(composePath)) {\n addExternalLabels(composePath, appName, hostname, containerPort);\n log(`step 4 OK — labels added to ${composePath}`);\n } else {\n log(`step 4 SKIP — compose file not found at ${composePath}`);\n }\n steps.push({ step: 'traefik_labels', status: 'completed' });\n } catch (err) {\n // Non-fatal — labels can be added manually\n log(`step 4 WARN (non-fatal): ${err}`);\n steps.push({ step: 'traefik_labels', status: 'failed', error: String(err) });\n }\n\n // Step 5: Persist connection to state\n log(`step 5/6: persist connection to state`);\n const connection: DomainConnection = {\n appName,\n subdomain,\n domain,\n hostname,\n tunnelId: cf.tunnelId,\n cnameRecordId,\n containerPort,\n connectedAt: new Date().toISOString(),\n scenario,\n };\n\n if (!this.state.domainConnections) {\n this.state.domainConnections = [];\n }\n // Remove existing connection for same app if any\n this.state.domainConnections = this.state.domainConnections.filter((c) => c.appName !== appName);\n this.state.domainConnections.push(connection);\n saveState(this.state);\n log(`step 5 OK`);\n\n // Step 6: Poll DNS propagation\n log(`step 6/6: poll DNS propagation for ${hostname} (timeout 30s)`);\n const pollStart = Date.now();\n try {\n await this.pollDnsPropagation(hostname, 30_000);\n steps.push({ step: 'dns_propagation', status: 'completed', durationMs: Date.now() - pollStart });\n log(`step 6 OK (${Date.now() - pollStart}ms)`);\n } catch {\n steps.push({ step: 'dns_propagation', status: 'skipped', durationMs: Date.now() - pollStart });\n log(`step 6 SKIP — DNS not yet propagated (${Date.now() - pollStart}ms)`);\n }\n\n log(`SUCCESS: https://${hostname}`);\n return {\n success: true,\n hostname,\n externalUrl: `https://${hostname}`,\n steps,\n };\n }\n\n // ── disconnect ───────────────────────────────────────────────────────────\n\n /**\n * Disconnect an app from its external domain.\n *\n * Steps: remove ingress → delete DNS → remove Traefik labels → update state\n * Atomic rollback on failure.\n */\n async disconnect(appName: string): Promise<DisconnectResult> {\n const steps: StepResult[] = [];\n const connections = this.state.domainConnections ?? [];\n const conn = connections.find((c) => c.appName === appName);\n\n if (!conn) {\n return {\n success: false,\n appName,\n removedHostname: '',\n steps,\n error: `NOT_CONNECTED: No external domain connection found for app: ${appName}`,\n };\n }\n\n const cf = this.state.domain.cloudflare;\n\n // Step 1: Remove ingress rule\n try {\n const remainingRoutes = getActiveServiceRoutes(this.state);\n const projectDomain = this.state.domain.zoneName;\n const builtinRoutes = remainingRoutes\n .filter((r) => r.subdomain !== conn.subdomain)\n .map((r) => ({ ...r, domain: projectDomain }));\n const remainingExtRoutes = (this.state.domainConnections ?? [])\n .filter((c) => c.appName !== appName)\n .map((c) => ({ subdomain: c.subdomain, containerName: this.resolveContainerName(c.appName), port: c.containerPort, domain: c.domain }));\n const filteredRoutes = [...builtinRoutes, ...remainingExtRoutes];\n if (cf.apiToken && cf.accountId && cf.tunnelId) {\n await configureTunnelIngress(cf.apiToken, cf.accountId, cf.tunnelId, conn.domain, filteredRoutes);\n }\n steps.push({ step: 'ingress_removal', status: 'completed' });\n } catch (err) {\n steps.push({ step: 'ingress_removal', status: 'failed', error: String(err) });\n return { success: false, appName, removedHostname: conn.hostname, steps, error: `Ingress removal failed: ${err}` };\n }\n\n // Step 2: Delete DNS CNAME record\n try {\n if (cf.apiToken && cf.zoneId) {\n if (conn.cnameRecordId) {\n await deleteDnsRecord(cf.apiToken, cf.zoneId, conn.cnameRecordId);\n } else {\n // Fallback: lookup by hostname\n const records = await getDnsRecords(cf.apiToken, cf.zoneId, conn.hostname);\n for (const rec of records) {\n await deleteDnsRecord(cf.apiToken, cf.zoneId, rec.id);\n }\n }\n }\n steps.push({ step: 'dns_deletion', status: 'completed' });\n } catch (err) {\n // Rollback: re-add ingress rule (restore all routes including the one being disconnected)\n try {\n const routes = getActiveServiceRoutes(this.state);\n const projectDomain = this.state.domain.zoneName;\n const allBuiltin = routes.map((r) => ({ ...r, domain: projectDomain }));\n const allExt = (this.state.domainConnections ?? [])\n .map((c) => ({ subdomain: c.subdomain, containerName: this.resolveContainerName(c.appName), port: c.containerPort, domain: c.domain }));\n const allRoutes = [...allBuiltin, ...allExt];\n if (cf.apiToken && cf.accountId && cf.tunnelId) {\n await configureTunnelIngress(cf.apiToken, cf.accountId, cf.tunnelId, conn.domain, allRoutes);\n }\n } catch { /* best-effort rollback */ }\n steps.push({ step: 'dns_deletion', status: 'failed', error: String(err) });\n return { success: false, appName, removedHostname: conn.hostname, steps, error: `DNS deletion failed: ${err}` };\n }\n\n // Step 3: Remove Traefik external labels\n try {\n const composePath = this.getComposePath();\n if (fs.existsSync(composePath)) {\n removeExternalLabels(composePath, appName);\n }\n steps.push({ step: 'traefik_cleanup', status: 'completed' });\n } catch (err) {\n steps.push({ step: 'traefik_cleanup', status: 'failed', error: String(err) });\n }\n\n // Step 4: Update state\n this.state.domainConnections = connections.filter((c) => c.appName !== appName);\n saveState(this.state);\n\n return {\n success: true,\n appName,\n removedHostname: conn.hostname,\n steps,\n };\n }\n\n // ── list ─────────────────────────────────────────────────────────────────\n\n /** Returns all active domain connections. */\n list(): DomainConnection[] {\n return this.state.domainConnections ?? [];\n }\n\n // ── status ───────────────────────────────────────────────────────────────\n\n /**\n * Get detailed status for a specific app's domain connection,\n * or all connections if appName is omitted.\n */\n async status(appName?: string): Promise<DomainStatusInfo[]> {\n const connections = this.state.domainConnections ?? [];\n const targets = appName\n ? connections.filter((c) => c.appName === appName)\n : connections;\n\n const cf = this.state.domain.cloudflare;\n const results: DomainStatusInfo[] = [];\n\n for (const conn of targets) {\n const info: DomainStatusInfo = {\n appName: conn.appName,\n local: { url: `http://localhost:${conn.containerPort}`, healthy: false },\n external: { url: `https://${conn.hostname}`, dnsResolved: false, httpsReachable: false },\n tunnel: { status: 'inactive', connectorCount: 0 },\n dns: null,\n };\n\n // Local health\n try {\n info.local.healthy = await this.checkLocalHealth(conn.appName, conn.containerPort);\n } catch { /* leave false */ }\n\n // Tunnel health\n if (cf.apiToken && cf.accountId && cf.tunnelId) {\n try {\n const health = await getTunnelHealth(cf.apiToken, cf.accountId, cf.tunnelId);\n info.tunnel = health;\n } catch { /* leave inactive */ }\n }\n\n // DNS verification\n if (cf.apiToken && cf.zoneId) {\n try {\n const records = await getDnsRecords(cf.apiToken, cf.zoneId, conn.hostname);\n if (records.length > 0) {\n info.dns = {\n type: 'CNAME',\n name: records[0].name,\n content: records[0].content,\n proxied: records[0].proxied,\n };\n info.external.dnsResolved = true;\n }\n } catch { /* leave null */ }\n }\n\n // External reachability (use dig as fallback)\n try {\n const resolved = await this.checkDnsResolution(conn.hostname);\n info.external.dnsResolved = info.external.dnsResolved || resolved;\n } catch { /* leave false */ }\n\n // HTTPS reachability\n try {\n info.external.httpsReachable = await this.checkHttpsReachable(conn.hostname);\n } catch { /* leave false */ }\n\n results.push(info);\n }\n\n return results;\n }\n\n // ── getConnectableApps ───────────────────────────────────────────────────\n\n /** Returns apps that can be connected to domains (running services not yet connected). */\n getConnectableApps(): AppInfo[] {\n const routes = getActiveServiceRoutes(this.state);\n const connections = this.state.domainConnections ?? [];\n\n return routes.map((route) => {\n const existing = connections.find((c) => c.subdomain === route.subdomain);\n return {\n name: route.subdomain,\n containerName: route.containerName,\n port: route.port,\n running: true, // We assume routes represent running services\n alreadyConnected: !!existing,\n hostname: existing?.hostname,\n };\n });\n }\n\n // ── Private helpers ──────────────────────────────────────────────────────\n\n private detectScenario(): DomainScenario {\n const cf = this.state.domain.cloudflare;\n // Scenario A: Zone is managed by Cloudflare (zoneId is set and active)\n // Scenario B: Zone transferred to CF NS\n // Scenario C: CNAME-only (no NS delegation)\n // For now, default to 'A' — can be refined with zone status check\n if (cf.zoneId) return 'A';\n return 'C';\n }\n\n private resolveContainerPort(appName: string): number | null {\n const routes = getActiveServiceRoutes(this.state);\n const route = routes.find((r) => r.subdomain === appName || r.containerName === appName);\n if (route) return route.port;\n\n // Fallback: look up custom app created via `brewnet create-app`\n const appsJsonPath = path.join(os.homedir(), '.brewnet', 'apps.json');\n const apps = readApps(appsJsonPath);\n const found = apps.find((a) => a.name === appName);\n if (!found) return null;\n\n // Non-unified (split-stack) apps: connect to frontend port, not backend.\n // Frontend port is stored in appDir/.env as FRONTEND_PORT (default 3000).\n const stackEntry = found.stackId ? getStackById(found.stackId) : null;\n if (stackEntry?.isUnified === false && found.appDir) {\n let frontendPort = 3000;\n const envPath = path.join(found.appDir, '.env');\n try {\n const envContent = fs.readFileSync(envPath, 'utf-8');\n const match = envContent.match(/^FRONTEND_PORT=(\\d+)/m);\n if (match) frontendPort = parseInt(match[1]!, 10);\n } catch { /* use default 3000 */ }\n return frontendPort;\n }\n\n return found.port;\n }\n\n private resolveContainerName(appName: string): string {\n const routes = getActiveServiceRoutes(this.state);\n const route = routes.find((r) => r.subdomain === appName || r.containerName === appName);\n if (route) return route.containerName;\n\n // Custom create-app apps run in their own docker-compose network.\n // cloudflared reaches them via the host-mapped port using host.docker.internal.\n const appsJsonPath = path.join(os.homedir(), '.brewnet', 'apps.json');\n const apps = readApps(appsJsonPath);\n const found = apps.find((a) => a.name === appName);\n if (found) return 'host.docker.internal';\n\n return appName;\n }\n\n private getComposePath(): string {\n const projectPath = this.state.projectPath.startsWith('~')\n ? path.join(os.homedir(), this.state.projectPath.slice(1))\n : this.state.projectPath;\n return path.join(projectPath, 'docker-compose.yml');\n }\n\n private async checkLocalHealth(_appName: string, port: number): Promise<boolean> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5_000);\n const resp = await fetch(`http://127.0.0.1:${port}/`, { signal: controller.signal });\n clearTimeout(timeout);\n return resp.ok || resp.status < 500;\n } catch {\n return false;\n }\n }\n\n private async checkDnsResolution(hostname: string): Promise<boolean> {\n try {\n const result = await execa('dig', ['+short', 'CNAME', hostname, '@1.1.1.1'], { timeout: 10_000 });\n return result.stdout.trim().length > 0;\n } catch {\n return false;\n }\n }\n\n private async checkHttpsReachable(hostname: string): Promise<boolean> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 10_000);\n const resp = await fetch(`https://${hostname}/`, {\n method: 'HEAD',\n signal: controller.signal,\n });\n clearTimeout(timeout);\n return resp.ok || resp.status < 500;\n } catch {\n return false;\n }\n }\n\n private async rollbackIngress(\n cf: WizardState['domain']['cloudflare'],\n previousRoutes: ServiceRoute[] | null,\n domain: string,\n ): Promise<void> {\n if (!previousRoutes || !cf.apiToken || !cf.accountId || !cf.tunnelId) return;\n try {\n await configureTunnelIngress(cf.apiToken, cf.accountId, cf.tunnelId, domain, previousRoutes);\n } catch { /* best-effort rollback */ }\n }\n\n private async pollDnsPropagation(hostname: string, timeoutMs: number): Promise<void> {\n const start = Date.now();\n while (Date.now() - start < timeoutMs) {\n const resolved = await this.checkDnsResolution(hostname);\n if (resolved) return;\n await new Promise((r) => setTimeout(r, 2_000));\n }\n throw new Error('DNS propagation timeout');\n }\n}\n","// packages/cli/src/services/app-registry.ts\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { AppEntry, DeployHistoryEntry } from '../types/app-entry.js';\n\nexport function readApps(appsJsonPath: string): AppEntry[] {\n if (!existsSync(appsJsonPath)) return [];\n try {\n return JSON.parse(readFileSync(appsJsonPath, 'utf-8')) as AppEntry[];\n } catch {\n return [];\n }\n}\n\nexport function writeApps(appsJsonPath: string, apps: AppEntry[]): void {\n mkdirSync(dirname(appsJsonPath), { recursive: true });\n writeFileSync(appsJsonPath, JSON.stringify(apps, null, 2), 'utf-8');\n}\n\nexport function addApp(appsJsonPath: string, entry: AppEntry): void {\n const apps = readApps(appsJsonPath);\n if (apps.some((a) => a.name === entry.name)) {\n throw new Error(`App \"${entry.name}\" already exists`);\n }\n writeApps(appsJsonPath, [...apps, entry]);\n}\n\nexport function updateApp(appsJsonPath: string, name: string, patch: Partial<AppEntry>): void {\n const apps = readApps(appsJsonPath);\n const idx = apps.findIndex((a) => a.name === name);\n if (idx === -1) throw new Error(`App \"${name}\" not found`);\n apps[idx] = { ...apps[idx]!, ...patch };\n writeApps(appsJsonPath, apps);\n}\n\nexport function removeApp(appsJsonPath: string, name: string): void {\n const apps = readApps(appsJsonPath).filter((a) => a.name !== name);\n writeApps(appsJsonPath, apps);\n}\n\nexport function readDeployHistory(historyJsonPath: string): DeployHistoryEntry[] {\n if (!existsSync(historyJsonPath)) return [];\n try {\n return JSON.parse(readFileSync(historyJsonPath, 'utf-8')) as DeployHistoryEntry[];\n } catch {\n return [];\n }\n}\n\nexport function appendDeployHistory(historyJsonPath: string, entry: DeployHistoryEntry): void {\n const entries = readDeployHistory(historyJsonPath);\n mkdirSync(dirname(historyJsonPath), { recursive: true });\n writeFileSync(historyJsonPath, JSON.stringify([...entries, entry], null, 2), 'utf-8');\n}\n"],"mappings":"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","names":["readFileSync","readdirSync","existsSync","join","basename","existsSync","readdirSync","readFileSync","join","basename","existsSync","readdirSync","join","path","join","existsSync","readdirSync","path","existsSync","readFileSync","writeFileSync","mkdirSync"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/services/cloudflare-client.ts"],"sourcesContent":["/**\n * Cloudflare API client for Brewnet tunnel automation.\n *\n * Covers the full tunnel setup flow:\n * 1. Token verification\n * 2. Account listing / auto-selection\n * 3. Zone (domain) listing / selection\n * 4. Tunnel creation\n * 5. Ingress rule configuration\n * 6. DNS CNAME record creation\n *\n * API reference:\n * https://developers.cloudflare.com/api/\n *\n * @module services/cloudflare-client\n */\n\nimport crypto from 'node:crypto';\nimport type { WizardState } from '@brewnet/shared';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ServiceRoute {\n subdomain: string;\n containerName: string;\n port: number;\n /** Per-route domain override. When set, takes precedence over the shared `domain` param in configureTunnelIngress. */\n domain?: string;\n}\n\nexport interface RetryConfig {\n /** Maximum number of retry attempts (default: 3) */\n maxRetries?: number;\n /** Base delay in ms before first retry (default: 1000) */\n baseDelayMs?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst CF_BASE = 'https://api.cloudflare.com/client/v4';\n\nfunction cfHeaders(apiToken: string): Record<string, string> {\n return {\n 'Authorization': `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n };\n}\n\n// ---------------------------------------------------------------------------\n// fetchWithRetry\n// ---------------------------------------------------------------------------\n\n/**\n * Wrapper around `fetch` with exponential backoff retry logic.\n *\n * Retry conditions:\n * - Network errors (fetch throws)\n * - HTTP 5xx responses\n * - HTTP 429 (rate limit)\n *\n * No retry conditions:\n * - HTTP 400 / 401 / 403 (client errors — retrying won't help)\n * - Other 4xx responses\n *\n * Backoff: baseDelay * 2^attempt ± 10% jitter (default: 1s → 2s → 4s)\n */\nexport async function fetchWithRetry(\n url: string,\n init?: RequestInit,\n config: RetryConfig = {},\n): Promise<Response> {\n const maxRetries = config.maxRetries ?? 3;\n const baseDelayMs = config.baseDelayMs ?? 1000;\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, init);\n\n // No retry for auth/client errors\n if (response.status === 400 || response.status === 401 || response.status === 403) {\n return response;\n }\n\n // Retry on 5xx or 429\n if (response.status >= 500 || response.status === 429) {\n if (attempt < maxRetries) {\n await retryDelay(attempt, baseDelayMs);\n continue;\n }\n return response;\n }\n\n return response;\n } catch (err) {\n // Network error — retry\n lastError = err;\n if (attempt < maxRetries) {\n await retryDelay(attempt, baseDelayMs);\n continue;\n }\n }\n }\n\n throw lastError ?? new Error('fetchWithRetry: exhausted retries');\n}\n\nfunction retryDelay(attempt: number, baseDelayMs: number): Promise<void> {\n const jitterFactor = 0.9 + Math.random() * 0.2; // ±10%\n const delay = baseDelayMs * Math.pow(2, attempt) * jitterFactor;\n return new Promise((resolve) => setTimeout(resolve, delay));\n}\n\n// ---------------------------------------------------------------------------\n// deleteTunnel\n// ---------------------------------------------------------------------------\n\n/**\n * Delete a Cloudflare Tunnel via the API.\n *\n * DELETE /client/v4/accounts/{accountId}/cfd_tunnel/{tunnelId}\n *\n * Throws with a descriptive error if the tunnel has active connections\n * (HTTP 400) or if the request fails for any other reason.\n *\n * Used during auto-rollback when tunnel setup fails partway through.\n */\nexport async function deleteTunnel(\n apiToken: string,\n accountId: string,\n tunnelId: string,\n): Promise<void> {\n const url = `${CF_BASE}/accounts/${accountId}/cfd_tunnel/${tunnelId}`;\n\n const response = await fetchWithRetry(url, {\n method: 'DELETE',\n headers: cfHeaders(apiToken),\n });\n\n if (response.status === 404) {\n // Already deleted — treat as success\n return;\n }\n\n const data = (await response.json()) as {\n success: boolean;\n errors?: Array<{ message: string; code?: number }>;\n };\n\n if (!response.ok || !data.success) {\n const errMsg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n if (response.status === 400 && errMsg.toLowerCase().includes('active connection')) {\n throw new Error(\n `터널 삭제 실패: 활성 연결이 있습니다. cloudflared 컨테이너를 먼저 중지하세요. (${errMsg})`,\n );\n }\n throw new Error(`터널 삭제 실패: ${errMsg}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// verifyToken\n// ---------------------------------------------------------------------------\n\n/**\n * Verify a Cloudflare API token and retrieve the associated account email.\n *\n * GET /client/v4/user/tokens/verify\n * GET /client/v4/user (to get email)\n */\nexport async function verifyToken(\n apiToken: string,\n): Promise<{ valid: boolean; email?: string }> {\n const verifyResp = await fetch(`${CF_BASE}/user/tokens/verify`, {\n headers: cfHeaders(apiToken),\n });\n\n const verifyData = (await verifyResp.json()) as {\n success: boolean;\n result?: { status: string };\n };\n\n if (!verifyResp.ok || !verifyData.success || verifyData.result?.status !== 'active') {\n return { valid: false };\n }\n\n // Fetch email from /user endpoint\n const userResp = await fetch(`${CF_BASE}/user`, {\n headers: cfHeaders(apiToken),\n });\n const userData = (await userResp.json()) as {\n success: boolean;\n result?: { email: string };\n };\n\n const email = userData.success ? userData.result?.email : undefined;\n return { valid: true, email };\n}\n\n// ---------------------------------------------------------------------------\n// getAccounts\n// ---------------------------------------------------------------------------\n\n/**\n * List all Cloudflare accounts accessible with the given token.\n *\n * GET /client/v4/accounts\n */\nexport async function getAccounts(\n apiToken: string,\n): Promise<Array<{ id: string; name: string }>> {\n const response = await fetch(`${CF_BASE}/accounts`, {\n headers: cfHeaders(apiToken),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n result?: Array<{ id: string; name: string }>;\n };\n\n if (!response.ok || !data.success) return [];\n return data.result ?? [];\n}\n\n// ---------------------------------------------------------------------------\n// getZones\n// ---------------------------------------------------------------------------\n\n/**\n * List all DNS zones (domains) accessible with the given token.\n *\n * GET /client/v4/zones\n */\nexport async function getZones(\n apiToken: string,\n): Promise<Array<{ id: string; name: string; status: string; accountId?: string }>> {\n const response = await fetch(`${CF_BASE}/zones`, {\n headers: cfHeaders(apiToken),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n result?: Array<{ id: string; name: string; status: string; account?: { id: string } }>;\n };\n\n if (!response.ok || !data.success) return [];\n return (data.result ?? []).map((z) => ({\n id: z.id,\n name: z.name,\n status: z.status,\n accountId: z.account?.id,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// createTunnel\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Cloudflare Tunnel via the API.\n *\n * POST /client/v4/accounts/{accountId}/cfd_tunnel\n */\nexport async function createTunnel(\n apiToken: string,\n accountId: string,\n name: string,\n): Promise<{ tunnelId: string; tunnelToken: string }> {\n const tunnelSecret = crypto.randomBytes(32).toString('base64');\n const url = `${CF_BASE}/accounts/${accountId}/cfd_tunnel`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: cfHeaders(apiToken),\n body: JSON.stringify({\n name,\n config_src: 'cloudflare',\n tunnel_secret: tunnelSecret,\n }),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n result?: { id: string; token: string };\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n throw new Error(msg);\n }\n\n if (!data.result?.id || !data.result?.token) {\n throw new Error('Cloudflare API returned unexpected response (missing id or token)');\n }\n\n return {\n tunnelId: data.result.id,\n tunnelToken: data.result.token,\n };\n}\n\n// ---------------------------------------------------------------------------\n// configureTunnelIngress\n// ---------------------------------------------------------------------------\n\n/**\n * Configure ingress rules for a Cloudflare Tunnel.\n *\n * PUT /client/v4/accounts/{accountId}/cfd_tunnel/{tunnelId}/configurations\n */\nexport async function configureTunnelIngress(\n apiToken: string,\n accountId: string,\n tunnelId: string,\n domain: string,\n routes: ServiceRoute[],\n): Promise<void> {\n const url = `${CF_BASE}/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`;\n\n const ingress = [\n ...routes.map((r) => ({\n hostname: `${r.subdomain}.${r.domain ?? domain}`,\n service: `http://${r.containerName}:${r.port}`,\n })),\n { service: 'http_status:404' },\n ];\n\n const response = await fetch(url, {\n method: 'PUT',\n headers: cfHeaders(apiToken),\n body: JSON.stringify({ config: { ingress } }),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n throw new Error(`Failed to configure ingress: ${msg}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// createDnsRecord\n// ---------------------------------------------------------------------------\n\n/**\n * Create a CNAME DNS record pointing to the Cloudflare Tunnel.\n *\n * POST /client/v4/zones/{zoneId}/dns_records\n * Record: {subdomain}.{domain} → {tunnelId}.cfargotunnel.com (proxied)\n */\nexport async function createDnsRecord(\n apiToken: string,\n zoneId: string,\n tunnelId: string,\n subdomain: string,\n domain: string,\n): Promise<void> {\n const url = `${CF_BASE}/zones/${zoneId}/dns_records`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: cfHeaders(apiToken),\n body: JSON.stringify({\n type: 'CNAME',\n name: `${subdomain}.${domain}`,\n content: `${tunnelId}.cfargotunnel.com`,\n proxied: true,\n }),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n // Non-fatal if record already exists\n if (!msg.toLowerCase().includes('already exists')) {\n throw new Error(`DNS record creation failed: ${msg}`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// getDnsRecords\n// ---------------------------------------------------------------------------\n\n/**\n * Query CNAME DNS records for a specific hostname in a zone.\n *\n * GET /client/v4/zones/{zoneId}/dns_records?type=CNAME&name={hostname}\n */\nexport async function getDnsRecords(\n apiToken: string,\n zoneId: string,\n hostname: string,\n): Promise<Array<{ id: string; name: string; content: string; proxied: boolean }>> {\n const url = `${CF_BASE}/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(hostname)}`;\n\n const response = await fetchWithRetry(url, {\n headers: cfHeaders(apiToken),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n result?: Array<{ id: string; name: string; content: string; proxied: boolean }>;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n throw new Error(`Failed to query DNS records: ${msg}`);\n }\n\n return data.result ?? [];\n}\n\n// ---------------------------------------------------------------------------\n// deleteDnsRecord\n// ---------------------------------------------------------------------------\n\n/**\n * Delete a DNS record by ID.\n *\n * DELETE /client/v4/zones/{zoneId}/dns_records/{recordId}\n */\nexport async function deleteDnsRecord(\n apiToken: string,\n zoneId: string,\n recordId: string,\n): Promise<void> {\n const url = `${CF_BASE}/zones/${zoneId}/dns_records/${recordId}`;\n\n const response = await fetchWithRetry(url, {\n method: 'DELETE',\n headers: cfHeaders(apiToken),\n });\n\n if (response.status === 404) {\n // Already deleted — treat as success\n return;\n }\n\n const data = (await response.json()) as {\n success: boolean;\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n throw new Error(`Failed to delete DNS record: ${msg}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// getTunnelHealth\n// ---------------------------------------------------------------------------\n\n/**\n * Query the health of a Cloudflare Tunnel.\n *\n * GET /client/v4/accounts/{accountId}/cfd_tunnel/{tunnelId}\n *\n * Returns the tunnel status and number of active connectors.\n * Used for health verification after tunnel creation and for `brewnet domain tunnel status`.\n */\nexport async function getTunnelHealth(\n apiToken: string,\n accountId: string,\n tunnelId: string,\n): Promise<{ status: 'healthy' | 'degraded' | 'inactive'; connectorCount: number }> {\n const url = `${CF_BASE}/accounts/${accountId}/cfd_tunnel/${tunnelId}`;\n\n const response = await fetchWithRetry(url, {\n headers: cfHeaders(apiToken),\n });\n\n const data = (await response.json()) as {\n success: boolean;\n result?: {\n status: string;\n connections?: Array<unknown>;\n };\n errors?: Array<{ message: string }>;\n };\n\n if (!response.ok || !data.success) {\n const msg = data.errors?.[0]?.message ?? `HTTP ${response.status}`;\n throw new Error(`Failed to get tunnel health: ${msg}`);\n }\n\n const rawStatus = data.result?.status ?? 'inactive';\n const connectorCount = data.result?.connections?.length ?? 0;\n\n const status: 'healthy' | 'degraded' | 'inactive' =\n rawStatus === 'healthy' ? 'healthy'\n : rawStatus === 'degraded' ? 'degraded'\n : 'inactive';\n\n return { status, connectorCount };\n}\n\n// ---------------------------------------------------------------------------\n// buildTokenCreationUrl\n// ---------------------------------------------------------------------------\n\n/**\n * Build a pre-filled Cloudflare API Token creation URL.\n *\n * Sets permissions: Cloudflare Tunnel (Edit) + DNS (Edit)\n * Sets name: brewnet-{projectName}\n */\nexport function buildTokenCreationUrl(projectName: string): string {\n const perms = encodeURIComponent(\n JSON.stringify([\n { key: 'cloudflare_tunnel', type: 'edit' },\n { key: 'dns', type: 'edit' },\n ]),\n );\n return `https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=${perms}&name=brewnet-${projectName}`;\n}\n\n// ---------------------------------------------------------------------------\n// getActiveServiceRoutes\n// ---------------------------------------------------------------------------\n\n/**\n * Build the list of service routes to expose through the Cloudflare Tunnel.\n * Called after server component selection to determine which ingress rules to create.\n */\nexport function getActiveServiceRoutes(state: WizardState): ServiceRoute[] {\n const routes: ServiceRoute[] = [];\n\n // Git server (always enabled)\n routes.push({ subdomain: 'git', containerName: 'gitea', port: 3000 });\n\n // File server\n if (state.servers.fileServer?.enabled) {\n if (state.servers.fileServer.service === 'nextcloud') {\n routes.push({ subdomain: 'cloud', containerName: 'nextcloud', port: 80 });\n } else if (state.servers.fileServer.service === 'minio') {\n routes.push({ subdomain: 'minio', containerName: 'minio', port: 9001 });\n }\n }\n\n // Media\n if (state.servers.media?.enabled && state.servers.media.services?.includes('jellyfin')) {\n routes.push({ subdomain: 'media', containerName: 'jellyfin', port: 8096 });\n }\n\n // Database admin UI (pgAdmin for PostgreSQL)\n if (\n state.servers.dbServer?.enabled &&\n state.servers.dbServer.adminUI &&\n state.servers.dbServer.primary === 'postgresql'\n ) {\n routes.push({ subdomain: 'pgadmin', containerName: 'pgadmin', port: 80 });\n }\n\n // FileBrowser\n if (state.servers.fileBrowser?.enabled) {\n routes.push({ subdomain: 'files', containerName: 'filebrowser', port: 80 });\n }\n\n return routes;\n}\n"],"mappings":"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","names":[]}