#!/usr/bin/env node import fs from 'fs/promises' import path from 'path' // --- Helpers --- async function fileExists(filePath) { try { await fs.access(filePath) return true } catch { return false } } function getAppDir(appName) { // Try apps/appName, then root const candidates = [path.join(process.cwd(), 'apps', appName), path.join(process.cwd(), appName)] return candidates.find((dir) => fs .stat(dir) .then((s) => s.isDirectory()) .catch(() => false) ) } async function getMonorepoPackages() { const pkgsDir = path.join(process.cwd(), 'packages') const entries = await fs.readdir(pkgsDir) return entries.filter((e) => !e.startsWith('.')) } async function getAppDependencies(appDir) { const pkgJsonPath = path.join(appDir, 'package.json') const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8')) const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies } return Object.keys(allDeps || {}).filter((dep) => dep.startsWith('@scandic-hotels/')) } async function findEnvFiles(baseDir) { const envDir = path.join(baseDir, 'env') const files = [] for (const f of ['server.ts', 'client.ts']) { const full = path.join(envDir, f) if (await fileExists(full)) files.push(full) } return files } async function extractEnvVarsFromFile(filePath) { const content = await fs.readFile(filePath, 'utf8') const regex = /process\.env\.([A-Z0-9_]+)/g const vars = new Set() let match while ((match = regex.exec(content))) { vars.add(match[1]) } return Array.from(vars) } async function readEnvLocalVars(envPath) { try { const content = await fs.readFile(envPath, 'utf8') const lines = content.split(/\r?\n/) const vars = new Set() for (const line of lines) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=/) if (m) vars.add(m[1]) } return vars } catch { return new Set() } } // --- New: Extract zod schema example values --- function extractZodExamplesFromContent(content) { // Heuristic: find zod schema definitions in the file // Only supports simple z.string(), z.enum([...]), z.coerce.number(), .default(), .refine() const result = {} // Match server: { ... } or client: { ... } const serverMatch = content.match(/(server|client):\s*{([\s\S]*?)}\s*,/) if (!serverMatch) return result const block = serverMatch[2] // Match lines like VAR: z.string().default("foo"), const lineRegex = /([A-Z0-9_]+):\s*z\.(string|enum|coerce\.number|coerce\.boolean|number|boolean)\(([^)]*)\)([^,]*)/g let match while ((match = lineRegex.exec(block))) { const [_, varName, zodType, zodArgs, zodChain] = match let example = 'example' if (zodType === 'string') { if (/refine\(.*s === \"true\" \|\| s === \"false\"/.test(zodChain)) { example = '"true/false"' } else if (/default\(["']([^"']+)["']\)/.test(zodChain)) { example = '"' + zodChain.match(/default\(["']([^"']+)["']\)/)[1] + '"' } else { example = '"example"' } } else if (zodType === 'enum') { // z.enum(["a", "b"]) => "a|b" const opts = zodArgs .replace(/\[|\]|\s|"|'/g, '') .split(',') .filter(Boolean) example = '"' + opts.join('|') + '"' } else if (zodType.includes('number')) { if (/default\((\d+(\.\d+)?)\)/.test(zodChain)) { example = '"' + zodChain.match(/default\((\d+(\.\d+)?)\)/)[1] + '"' } else { example = '"123"' } } else if (zodType.includes('boolean')) { example = '"true/false"' } result[varName] = example } return result } async function extractEnvExamplesFromFile(filePath) { const content = await fs.readFile(filePath, 'utf8') return extractZodExamplesFromContent(content) } async function main() { const args = process.argv.slice(2) const appName = args[0] const group = args.includes('--group') const missing = args.includes('--missing') if (!appName) { console.error('Usage: node show-env.mjs [--group] [--missing]') process.exit(1) } const appDir = getAppDir(appName) if (!appDir) { console.error(`App directory not found for: ${appName}`) process.exit(1) } const deps = await getAppDependencies(appDir) const monorepoPkgs = await getMonorepoPackages() const usedPkgs = deps .map((dep) => dep.replace('@scandic-hotels/', '')) .filter((pkg) => monorepoPkgs.includes(pkg)) // Collect env files const sources = [{ name: appName, dir: appDir }] for (const pkg of usedPkgs) { sources.push({ name: pkg, dir: path.join(process.cwd(), 'packages', pkg) }) } const envVarsBySource = {} const envExamplesBySource = {} for (const src of sources) { const files = await findEnvFiles(src.dir) const vars = new Set() const examples = {} for (const file of files) { for (const v of await extractEnvVarsFromFile(file)) vars.add(v) const ex = await extractEnvExamplesFromFile(file) Object.assign(examples, ex) } envVarsBySource[src.name] = Array.from(vars).sort() envExamplesBySource[src.name] = examples } function getExample(varName, source) { return envExamplesBySource[source]?.[varName] || '"example"' } // If --missing, read .env.local from appDir let definedVars = new Set() if (missing) { const envPath = path.join(appDir, '.env.local') definedVars = await readEnvLocalVars(envPath) } function filterMissing(vars) { if (!missing) return vars return vars.filter((v) => !definedVars.has(v)) } if (group) { // Print grouped by source for (const [src, vars] of Object.entries(envVarsBySource)) { const missingVars = filterMissing(vars) if (missingVars.length === 0) continue console.log(`\n[${src}]`) for (const v of missingVars) console.log(`${v}=${getExample(v, src)}`) } } else { // Print all unique env vars, sorted, with example const allVars = new Set() for (const vars of Object.values(envVarsBySource)) { for (const v of vars) allVars.add(v) } const sortedVars = Array.from(allVars).sort() const missingVars = filterMissing(sortedVars) for (const v of missingVars) { let example = '"example"' for (const src of Object.keys(envExamplesBySource)) { if (envExamplesBySource[src][v]) { example = envExamplesBySource[src][v] break } } console.log(`${v}=${example}`) } } } main().catch((err) => { console.error(err) process.exit(1) })