134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
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");
|
||
}
|