realtime-minutes/server/env.ts
2026-04-17 16:11:31 +09:00

134 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<typeof envSchema>;
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, string[] | undefined>,
): 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");
}