(Init): Added shit

This commit is contained in:
2026-05-28 23:46:40 +00:00
parent a5250706cb
commit 8410600c63
46 changed files with 3898 additions and 228 deletions

144
crates/bot/src/main.ts Normal file
View File

@@ -0,0 +1,144 @@
/**
* @elly/bot entrypoint — Phase 1 boot.
*
* Phase 1 responsibilities:
* 1. Load and validate `config.toml` against the shared Zod schema.
* 2. Load and validate bot environment variables (DISCORD_TOKEN, IPC_TOKEN).
* 3. Construct the root structured logger and emit a boot summary.
* 4. Wire SIGINT/SIGTERM handlers for graceful shutdown.
*
* Phase 3 will add: Discord.js client, command/interaction router, and
* the typed IPC client that talks to `@elly/core`.
*/
import {
BotEnvSchema,
ConfigError,
ConfigValidationError,
createLogger,
EnvValidationError,
loadConfig,
loadEnv,
type BotEnv,
type Config,
type Logger,
} from "@elly/shared";
const CONFIG_PATH = Deno.env.get("CONFIG_PATH") ?? "./config.toml";
const LOGGER_NAME = "@elly/bot";
async function main(): Promise<void> {
const config = await loadConfigOrExit();
const env = loadEnvOrExit();
const logger = buildLogger(config, env);
logger.info("phase 1 boot starting", {
crate: "bot",
nodeEnv: env.NODE_ENV,
configPath: CONFIG_PATH,
});
logBootSummary(logger, config, env);
installSignalHandlers(logger);
logger.info("phase 1 boot complete", {
pid: Deno.pid,
deno: Deno.version.deno,
typescript: Deno.version.typescript,
});
// Phase 1 has no long-running work; the Discord client arrives in Phase 3.
await logger.flush();
}
// =====================================================================
// Boot helpers
// =====================================================================
async function loadConfigOrExit(): Promise<Config> {
try {
return await loadConfig(CONFIG_PATH);
} catch (err) {
bootFail(err, "config");
}
}
function loadEnvOrExit(): BotEnv {
try {
return loadEnv(BotEnvSchema);
} catch (err) {
bootFail(err, "env");
}
}
function buildLogger(config: Config, env: BotEnv): Logger {
const level = env.LOG_LEVEL ?? config.logging.level;
const isProd = env.NODE_ENV === "production";
return createLogger({
name: LOGGER_NAME,
level,
format: isProd ? "json" : config.logging.format,
file: config.logging.file
? {
path: config.logging.file,
maxBytes: config.logging.file_max_bytes,
maxBackups: config.logging.file_max_backups,
}
: undefined,
});
}
function logBootSummary(logger: Logger, config: Config, env: BotEnv): void {
logger.info("config validated", {
bot: config.bot.name,
guild: config.guild.name,
coreIpc: `${config.ipc.host}:${config.ipc.port}`,
features: countEnabled(config.features),
});
logger.debug("environment validated", {
nodeEnv: env.NODE_ENV,
logLevel: env.LOG_LEVEL ?? "<from config>",
discordTokenPresent: Boolean(env.DISCORD_TOKEN),
ipcTokenPresent: Boolean(env.IPC_TOKEN),
});
}
function countEnabled(features: Record<string, boolean>): string {
const enabled = Object.entries(features)
.filter(([, v]) => v)
.map(([k]) => k);
return `${enabled.length}/${Object.keys(features).length} enabled`;
}
function installSignalHandlers(logger: Logger): void {
const shutdown = (signal: string) => {
logger.info("shutdown signal received", { signal });
logger.flush().finally(() => Deno.exit(0));
};
Deno.addSignalListener("SIGINT", () => shutdown("SIGINT"));
Deno.addSignalListener("SIGTERM", () => shutdown("SIGTERM"));
}
function bootFail(err: unknown, stage: "config" | "env"): never {
if (err instanceof ConfigValidationError || err instanceof EnvValidationError) {
console.error(`[@elly/bot] ${stage} validation failed:`);
for (const issue of err.issues) {
console.error(` - ${issue.path}: ${issue.message}`);
}
Deno.exit(1);
}
if (err instanceof ConfigError) {
console.error(`[@elly/bot] ${stage} error: ${err.message}`);
Deno.exit(1);
}
console.error(`[@elly/bot] unexpected ${stage} error:`, err);
Deno.exit(1);
}
if (import.meta.main) {
main().catch((err) => {
console.error("[@elly/bot] fatal boot error:", err);
Deno.exit(1);
});
}