prodobit
Version:
Open-core business application development platform
231 lines (196 loc) • 6.24 kB
text/typescript
import type { PlatformAdapter } from "../platform/adapters.js";
import { supportsFileSystem } from "../platform/runtime-detection.js";
export interface EnvironmentInfo {
environment: "development" | "production" | "test";
nodeEnv: string;
isDevelopment: boolean;
isProduction: boolean;
isTest: boolean;
workingDirectory: string;
configDirectory: string;
homeDirectory?: string | undefined;
}
export async function detectEnvironment(adapter: PlatformAdapter): Promise<EnvironmentInfo> {
const nodeEnv = adapter.getEnvironmentVariable("NODE_ENV") || "development";
const prodobitEnv = (adapter.getEnvironmentVariable("PRODOBIT_ENV") ||
nodeEnv) as "development" | "production" | "test";
const environment = ["development", "production", "test"].includes(
prodobitEnv
)
? prodobitEnv
: "development";
const workingDirectory = adapter.getWorkingDirectory();
const configDir = adapter.getEnvironmentVariable("PRODOBIT_CONFIG_DIR");
const configDirectory =
configDir && adapter.resolvePath
? await adapter.resolvePath(configDir)
: workingDirectory;
const homeDirectory =
adapter.getEnvironmentVariable("HOME") ||
adapter.getEnvironmentVariable("USERPROFILE");
return {
environment,
nodeEnv,
isDevelopment: environment === "development",
isProduction: environment === "production",
isTest: environment === "test",
workingDirectory,
configDirectory,
homeDirectory,
};
}
export async function getConfigSearchPaths(
env: EnvironmentInfo,
adapter: PlatformAdapter
): Promise<string[]> {
const paths: string[] = [];
// Current directory
paths.push(env.workingDirectory);
// Config directory if different from working directory
if (env.configDirectory !== env.workingDirectory) {
paths.push(env.configDirectory);
}
// Home directory config (only if file system is supported)
if (env.homeDirectory && adapter.resolvePath) {
paths.push(await adapter.resolvePath(env.homeDirectory, ".prodobit"));
paths.push(await adapter.resolvePath(env.homeDirectory, ".config", "prodobit"));
}
// System-wide config (Unix-like systems, only for Node.js)
if (adapter.name === "node") {
const platformInfo = adapter.getPlatformInfo();
if (platformInfo.platform !== "win32") {
paths.push("/etc/prodobit");
paths.push("/usr/local/etc/prodobit");
}
}
return paths;
}
export async function findConfigFile(
searchPaths: string[],
filenames: string[],
adapter: PlatformAdapter
): Promise<string | null> {
if (!supportsFileSystem() || !adapter.fileExists || !adapter.resolvePath) {
return null;
}
for (const searchPath of searchPaths) {
for (const filename of filenames) {
const fullPath = await adapter.resolvePath(searchPath, filename);
try {
const exists = await adapter.fileExists(fullPath);
if (exists) {
return fullPath;
}
} catch {
// File doesn't exist or not readable, continue searching
continue;
}
}
}
return null;
}
export function expandEnvironmentVariables(
value: string,
adapter: PlatformAdapter
): string {
return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
const envValue = adapter.getEnvironmentVariable(varName);
if (envValue === undefined) {
throw new Error(`Environment variable ${varName} is not defined`);
}
return envValue;
});
}
export function parseEnvironmentValue(value: string): unknown {
// Handle empty values
if (value === "") {
return "";
}
// Handle boolean values
if (value === "true" || value === "false") {
return value === "true";
}
// Handle null and undefined
if (value === "null") {
return null;
}
if (value === "undefined") {
return undefined;
}
// Handle JSON values
if (
(value.startsWith("{") && value.endsWith("}")) ||
(value.startsWith("[") && value.endsWith("]"))
) {
try {
return JSON.parse(value);
} catch {
// Not valid JSON, treat as string
return value;
}
}
// Handle numbers
if (/^-?\d+$/.test(value)) {
const intValue = parseInt(value, 10);
if (!isNaN(intValue)) {
return intValue;
}
}
if (/^-?\d*\.\d+$/.test(value)) {
const floatValue = parseFloat(value);
if (!isNaN(floatValue)) {
return floatValue;
}
}
// Handle comma-separated arrays
if (value.includes(",") && !value.includes(" ")) {
return value.split(",").map((item) => item.trim());
}
// Note: Environment variable expansion requires adapter context
// This should be handled by the caller with expandEnvironmentVariables(value, adapter)
// Default to string
return value;
}
export function normalizeConfigPath(path: string): string {
// Convert environment variable style to dot notation
// Example: DATABASE_HOST -> database.host
return path
.toLowerCase()
.replace(/_/g, ".")
.replace(/^prodobit\./, ""); // Remove prodobit prefix if present
}
export function getEnvironmentPrefix(adapter: PlatformAdapter): string {
return adapter.getEnvironmentVariable("PRODOBIT_ENV_PREFIX") || "PRODOBIT_";
}
export interface ProcessSignalHandler {
signal: NodeJS.Signals | "all";
handler: () => Promise<void> | void;
}
export function setupGracefulShutdown(
handlers: ProcessSignalHandler[],
adapter: PlatformAdapter
): void {
// Only setup signal handlers in Node.js environment
if (adapter.name !== "node") {
console.warn("Graceful shutdown signals not supported in this runtime");
return;
}
const signals: NodeJS.Signals[] = ["SIGTERM", "SIGINT", "SIGUSR2"];
for (const signal of signals) {
process.on(signal, async () => {
console.log(`Received ${signal}, starting graceful shutdown...`);
try {
for (const { signal: handlerSignal, handler } of handlers) {
if (handlerSignal === signal || (handlerSignal as string) === "all") {
await handler();
}
}
console.log("Graceful shutdown completed");
process.exit(0);
} catch (error) {
console.error("Error during graceful shutdown:", error);
process.exit(1);
}
});
}
}