/** * Build-time CLI: рендерит канонический серверный конфиг HY2XS тем же кодом, * который работает на target-сервере. * * Используется compatibility gate: получившийся YAML скармливается реальному * upstream-бинарнику Hysteria до того, как будет создан release package. * * bun run tools/render-canonical-config.ts \ * --package-dir . --obfs gecko --tls-mode file \ * --cert /tmp/x.crt --key /tmp/x.key --out /tmp/config.yaml */ import { parseRuntimeEnv } from "../src/config/env"; import { HYSTERIA_OBFS_TYPES } from "../src/config/profile"; import { readText } from "../src/lib/fs"; import { hysteriaConfigTemplatePath, renderHysteriaConfig } from "../src/steps/config"; import type { RuntimeContext } from "../src/types/context"; type Options = { packageDir: string; configPath: string; templatePath: string; obfs: string; tlsMode: string; certPath: string; keyPath: string; hysteriaPort: string; trafficStatsPort: string; outPath: string; }; function fail(message: string): never { console.error(`[hy2xs-build] ERROR: ${message}`); process.exit(1); } function parseArgs(argv: string[]): Options { const options: Options = { packageDir: "", configPath: "", templatePath: "", obfs: "gecko", tlsMode: "file", certPath: "", keyPath: "", hysteriaPort: "", trafficStatsPort: "", outPath: "" }; const flags: Record = { "--package-dir": "packageDir", "--config": "configPath", "--template": "templatePath", "--obfs": "obfs", "--tls-mode": "tlsMode", "--cert": "certPath", "--key": "keyPath", "--port": "hysteriaPort", "--traffic-stats-port": "trafficStatsPort", "--out": "outPath" }; for (let i = 0; i < argv.length; i += 1) { const key = flags[argv[i]]; if (!key) { fail(`unknown argument: ${argv[i]}`); } const value = argv[i + 1]; if (value === undefined || value.startsWith("--")) { fail(`missing value for ${argv[i]}`); } options[key] = value; i += 1; } if (!options.packageDir) { fail("missing --package-dir"); } if (!options.outPath) { fail("missing --out"); } if (!(HYSTERIA_OBFS_TYPES as readonly string[]).includes(options.obfs)) { fail(`unsupported --obfs value: ${options.obfs}`); } options.configPath ||= `${options.packageDir}/config/hy2xs.env`; options.templatePath ||= hysteriaConfigTemplatePath(options.packageDir); return options; } function overrideEnv(source: string, overrides: Record): string { const pending = new Map(Object.entries(overrides)); const lines = source.split(/\r?\n/).map((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { return line; } const key = trimmed.slice(0, trimmed.indexOf("=")); if (pending.has(key)) { const value = pending.get(key) as string; pending.delete(key); return `${key}=${value}`; } return line; }); for (const [key, value] of pending) { lines.push(`${key}=${value}`); } return lines.join("\n"); } async function main(): Promise { const options = parseArgs(Bun.argv.slice(2)); const overrides: Record = { HY2XS_HYSTERIA_OBFS_TYPE: options.obfs, HY2XS_TLS_MODE: options.tlsMode, // Compatibility gate работает офлайн и не должен зависеть от секретов пакета. HY2XS_HYSTERIA_OBFS_PASSWORD: "hy2xs-compat-gate-obfs-password", HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET: "hy2xs-compat-gate-traffic-secret", HY2XS_ADMIN_INITIAL_PASSWORD: "hy2xs-compat-gate-admin-password", HY2XS_ADMIN_CON_PASS: "hy2xs-compat-gate-con-password" }; if (options.certPath) { overrides.HY2XS_TLS_CERT_PATH = options.certPath; } if (options.keyPath) { overrides.HY2XS_TLS_KEY_PATH = options.keyPath; } if (options.hysteriaPort) { overrides.HY2XS_HYSTERIA_PORT = options.hysteriaPort; } if (options.trafficStatsPort) { overrides.HY2XS_HYSTERIA_TRAFFIC_STATS_PORT = options.trafficStatsPort; } const sourceEnv = await readText(options.configPath); const config = parseRuntimeEnv(overrideEnv(sourceEnv, overrides)); const context = { mode: "install", options: { packageDir: options.packageDir, sourceConfigPath: options.configPath, runtimeConfigPath: "/etc/hy2xs/hy2xs.env", nonInteractive: true, skipFirewall: true, skipServiceStart: true, skipSmoke: true }, config, packageVersion: "compat-gate", packageBuildId: "compat-gate", installDate: new Date().toISOString(), hysteriaVersion: "compat-gate", hysteriaResolution: "compat-gate" } satisfies RuntimeContext; const template = await readText(options.templatePath); await Bun.write(options.outPath, renderHysteriaConfig(context, template)); } try { await main(); } catch (error) { fail(error instanceof Error ? error.message : String(error)); }