(Init): Added shit
This commit is contained in:
5
crates/bot/deno.json
Normal file
5
crates/bot/deno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@elly/bot",
|
||||
"version": "0.1.0",
|
||||
"exports": "./src/main.ts"
|
||||
}
|
||||
144
crates/bot/src/main.ts
Normal file
144
crates/bot/src/main.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user