import { config as dotenvConfig } from "dotenv"; import { execSync } from "node:child_process"; import fs from "node:fs"; import { z } from "zod"; import { REQUIRED_KEYS } from "./constants.js"; import { logger } from "./logger.js"; const envSchema = z.object({ DEEPGRAM_API_KEY: z.string().min(1), GOOGLE_GENERATIVE_AI_API_KEY: z.string().min(1), PORT: z .string() .default("3001") .transform(Number) .pipe(z.number().int().min(1).max(65535)), }); export type Env = z.infer; export class EnvError extends Error { constructor(message: string) { super(message); this.name = "EnvError"; } } // プロセスライフタイムで1回だけ初期化される設定値(録音セッション状態とは無関係) let _env: Env | null = null; export function getEnv(): Env { if (!_env) _env = loadEnv(); return _env; } function loadEnv(): Env { dotenvConfig(); const first = envSchema.safeParse(process.env); if (first.success) return first.data; if (!fs.existsSync(".env") && tryOpInject()) { dotenvConfig({ override: true }); const retry = envSchema.safeParse(process.env); if (retry.success) { logger.info("1Password CLI で .env を自動生成しました"); return retry.data; } } const { fieldErrors } = z.flattenError(first.error); throw new EnvError(formatDiagnostics(fieldErrors)); } function tryOpInject(): boolean { try { execSync("op --version", { stdio: "ignore", timeout: 3_000 }); } catch { return false; } try { logger.info("1Password CLI で .env を生成中..."); execSync("op inject -i .env.example -o .env", { stdio: "inherit", timeout: 30_000, }); return true; } catch { logger.warn("1Password CLI での .env 生成に失敗しました"); return false; } } function hasOpCli(): boolean { try { execSync("op --version", { stdio: "ignore", timeout: 3_000 }); return true; } catch { return false; } } function formatDiagnostics( fieldErrors: Record, ): string { const issues = Object.keys(fieldErrors) .map((key) => { const val = process.env[key]; const status = val === undefined ? "未設定" : val === "" ? "空です" : "値が不正です"; return ` ${key}: ${status}`; }) .join("\n"); const opAvailable = hasOpCli(); const lines = [ "", "============================================================", " 環境変数が設定されていません", "============================================================", "", " 不足している変数:", issues, "", " -- 解決方法 -----------------------------------------------", "", ]; if (opAvailable) { lines.push( " [方法1] 1Password CLI で自動生成(推奨)", " $ op inject -i .env.example -o .env", "", ); } lines.push( opAvailable ? " [方法2] 手動で .env ファイルを作成" : " .env ファイルを手動で作成してください", "", ...REQUIRED_KEYS.map((key) => ` ${key}=your-key-here`), "", " API キーの取得先:", " Deepgram : https://console.deepgram.com/", " Google AI : https://aistudio.google.com/apikey", "", "============================================================", "", ); return lines.join("\n"); }