mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
Add config and composition root
This commit is contained in:
1 parent
09bec111d5
commit
c047fcbd5c
2 files changed
+149
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ConfigurationError } from "./domain/errors.js";
|
||||
import { err, ok, type Result } from "./domain/result.js";
|
||||
|
||||
const DEFAULT_HOPPER_LAUNCHER_PATH =
|
||||
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
||||
|
||||
export interface AppConfig {
|
||||
readonly hopperLauncherPath: string;
|
||||
readonly hopperTargetPath: string;
|
||||
readonly hopperTargetKind: "executable" | "database";
|
||||
readonly hopperLoaderArgs: readonly string[];
|
||||
}
|
||||
|
||||
const environmentSchema = z.object({
|
||||
HOPPER_LAUNCHER_PATH: z.string().min(1).optional(),
|
||||
HOPPER_TARGET_PATH: z.string().min(1),
|
||||
HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"),
|
||||
HOPPER_LOADER_ARGS_JSON: z.string().optional(),
|
||||
});
|
||||
|
||||
/** Parse Hopper launcher configuration once at the composition root. */
|
||||
export const parseConfig = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): Result<AppConfig, ConfigurationError> => {
|
||||
const parsedEnvironment = environmentSchema.safeParse(environment);
|
||||
if (!parsedEnvironment.success) {
|
||||
return err(
|
||||
new ConfigurationError("HOPPER_TARGET_PATH is required", {
|
||||
cause: parsedEnvironment.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
let loaderArgs: unknown = [];
|
||||
const encoded = parsedEnvironment.data.HOPPER_LOADER_ARGS_JSON;
|
||||
if (encoded !== undefined) {
|
||||
try {
|
||||
loaderArgs = JSON.parse(encoded);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new ConfigurationError("HOPPER_LOADER_ARGS_JSON must be valid JSON", {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
const parsedArgs = z.array(z.string()).safeParse(loaderArgs);
|
||||
if (!parsedArgs.success) {
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"HOPPER_LOADER_ARGS_JSON must encode an array of strings",
|
||||
{ cause: parsedArgs.error },
|
||||
),
|
||||
);
|
||||
}
|
||||
return ok({
|
||||
hopperLauncherPath:
|
||||
parsedEnvironment.data.HOPPER_LAUNCHER_PATH ??
|
||||
DEFAULT_HOPPER_LAUNCHER_PATH,
|
||||
hopperTargetPath: parsedEnvironment.data.HOPPER_TARGET_PATH,
|
||||
hopperTargetKind: parsedEnvironment.data.HOPPER_TARGET_KIND,
|
||||
hopperLoaderArgs: parsedArgs.data,
|
||||
});
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
serveStdio,
|
||||
type StdioServerHandle,
|
||||
} from "@modelcontextprotocol/server/stdio";
|
||||
|
||||
import { parseConfig } from "./config.js";
|
||||
import { HopperApplicationLauncher } from "./hopper/BridgeLauncher.js";
|
||||
import { HopperClient } from "./hopper/HopperClient.js";
|
||||
import { createServer } from "./server/createServer.js";
|
||||
|
||||
/** Start the production stdio server and install owned shutdown handlers. */
|
||||
export const run = async (): Promise<number> => {
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) {
|
||||
process.stderr.write(`${config.error._tag}: ${config.error.message}\n`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bridgeScriptPath = fileURLToPath(
|
||||
new URL("../bridge/hopper_bridge.py", import.meta.url),
|
||||
);
|
||||
const hopper = new HopperClient({
|
||||
launcher: new HopperApplicationLauncher({
|
||||
launcherPath: config.value.hopperLauncherPath,
|
||||
targetPath: config.value.hopperTargetPath,
|
||||
targetKind: config.value.hopperTargetKind,
|
||||
loaderArgs: config.value.hopperLoaderArgs,
|
||||
bridgeScriptPath,
|
||||
}),
|
||||
});
|
||||
let handle: StdioServerHandle;
|
||||
try {
|
||||
handle = serveStdio(() => createServer(hopper), {
|
||||
onerror: () => {
|
||||
process.stderr.write("MCP stdio transport error\n");
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
await hopper.close();
|
||||
process.stderr.write("Failed to start MCP stdio transport\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
let shutdownPromise: Promise<void> | undefined;
|
||||
const shutdown = (): Promise<void> => {
|
||||
shutdownPromise ??= (async () => {
|
||||
await handle.close();
|
||||
await hopper.close();
|
||||
})();
|
||||
return shutdownPromise;
|
||||
};
|
||||
const requestShutdown = (): void => {
|
||||
shutdown().catch(() => {
|
||||
process.exitCode = 1;
|
||||
process.stderr.write("Failed to shut down cleanly\n");
|
||||
});
|
||||
};
|
||||
|
||||
process.once("SIGINT", requestShutdown);
|
||||
process.once("SIGTERM", requestShutdown);
|
||||
process.stdin.once("end", requestShutdown);
|
||||
process.stdin.once("close", requestShutdown);
|
||||
return 0;
|
||||
};
|
||||
|
||||
const entryPath = process.argv[1];
|
||||
if (
|
||||
entryPath !== undefined &&
|
||||
pathToFileURL(entryPath).href === import.meta.url
|
||||
) {
|
||||
run()
|
||||
.then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
})
|
||||
.catch(() => {
|
||||
process.stderr.write("Unexpected server startup failure\n");
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user