import fs from 'fs'; import path from 'path'; import url from 'url'; import dotenv from "dotenv"; class NodeHelpers { public constructor() { } public loadEnv(envPath?: string, tryFromHome = true) { if (tryFromHome) { // Load .env from home directory if not already set (do not override existing) const homeEnvPath = path.join(process.env.HOME || process.env.USERPROFILE || "", ".@livx.cc.ask.env"); if (fs.existsSync(homeEnvPath)) { dotenv.config({ path: homeEnvPath, override: false }); } else { // console.warn('No .@livx.cc.ask.env found in home directory'); } } // Load .env from current working directory (local override) dotenv.config({ path: path.resolve(envPath ?? process.cwd(), ".env"), override: true }); return process.env; } // Helper to get env or fallback public getEnvOrDefault(key: string, fallback?: T): T | undefined { return (process.env[key] as T) ?? fallback; } /** * Gets the current module's metadata in a format compatible with selfExecutableHanlder * @returns The module metadata object */ public getCurrentModuleMeta(): CjsModule | EsmImportMeta { if (typeof import.meta !== 'undefined' && import.meta.url) { return { url: import.meta.url }; } return module; } /** * Handles self-execution of a module with a callback * @param callback The function to execute if this module is being run directly * @example * ```typescript * // In your module: * handleSelfExecution(async () => { * // Your main execution code here * }); * ``` */ public handleSelfExecution(callback: () => Promise | void): void { // Get the caller's file path from the stack trace const getCallerFile = () => { const stack = new Error().stack; if (!stack) return null; // Find the first line that's not from helpers.ts and not "native" const lines = stack.split('\n'); for (const line of lines) { if (line.includes('handleSelfExecution')) continue; if (line.includes('helpers.ts')) continue; if (line.includes('helpers/node.ts')) continue; if (line.includes('native')) continue; const match = line.match(/\(([^)]+)\)/); if (match) { const filePath = match[1]; if (filePath.startsWith('file://')) { return { url: filePath }; } return { filename: filePath }; } } return null; }; let meta = getCallerFile(); // If stack trace method failed, try using process.argv[1] if (!meta && process.argv[1]) { const scriptPath = process.argv[1]; if (scriptPath.startsWith('file://')) { meta = { url: scriptPath }; } else { meta = { filename: scriptPath }; } } if (!meta) { console.warn('Could not determine caller module metadata'); return; } if (this.isExecutedDirectly(meta)) { Promise.resolve(callback()).catch(err => { console.error('Error in self-execution:', err); process.exit(1); }); } } /** * Detects if the *calling* module is the main script executed directly via CLI. * This function needs the context (module or import.meta) of the caller. * * @param moduleOrMeta - Pass the `module` object if calling from CommonJS. * Pass `import.meta` if calling from ES Module. * @returns {boolean} - True if the calling module is the main entry point, false otherwise. * @throws {Error} - If the provided context is invalid or necessary info is missing. */ public isExecutedDirectly(moduleOrMeta: CjsModule | EsmImportMeta): boolean { let callerPath: string | null = null; let mainPath: string | null = null; let isBun = typeof Bun !== 'undefined'; // --- Determine the path of the CALLER module --- if ('filename' in moduleOrMeta) { // Likely a CommonJS module object was passed callerPath = path.resolve(moduleOrMeta.filename); } else if ('url' in moduleOrMeta && moduleOrMeta.url.startsWith('file://')) { // Likely an ESM import.meta object was passed callerPath = path.resolve(url.fileURLToPath(moduleOrMeta.url)); } else { throw new Error("Helpers.isExecutedDirectly: Invalid input. Pass 'module' (CJS) or 'import.meta' (ESM)."); } // --- Determine the path of the MAIN entry point script --- if (isBun && Bun.main) { // Bun provides a direct way mainPath = path.resolve(Bun.main); } else if (typeof require !== 'undefined' && require.main) { // Standard Node.js CommonJS way mainPath = path.resolve(require.main.filename); } else if (process.argv[1]) { // Fallback for ESM in Node.js (or environments without require.main) // process.argv[1] usually contains the path to the main script // Be cautious: this might not be 100% reliable in all edge cases (e.g., symlinks, `node -e "..."`) mainPath = path.resolve(process.argv[1]); } // --- Normalize paths for comparison --- const normalizePath = (p: string) => { // Remove line numbers and column numbers from the path return p.replace(/:\d+(:\d+)?$/, ''); }; // --- Compare the paths --- if (!callerPath) { // Should not happen if input validation passed, but for safety: console.warn("Helpers.isExecutedDirectly: Could not determine caller's path."); return false; } if (!mainPath) { // This can happen if executed in a way without a clear main script (e.g., Node REPL, `node -e '...'`) console.warn("Helpers.isExecutedDirectly: Could not determine the main script path."); return false; } // Check if the normalized paths are identical return normalizePath(callerPath) === normalizePath(mainPath); } // Check if the script is running as the main module /*public isMain(meta: ImportMeta): boolean { // Get the current file's path const currentFile = process.argv[1]; if (!currentFile) return false; // Check if we're running directly via bun run or tsx const isDirectRun = currentFile.endsWith('.ts') && (currentFile.includes('body/parser.ts') || currentFile.includes('parser.ts')); if (isDirectRun) { return true; } // Fallback to the original check return meta?.main ?? import.meta.main; }*/ } // Define interfaces for the expected structures (optional but good practice) interface CjsModule { filename: string; // require is implicitly available in CJS scope, including require.main } interface EsmImportMeta { url: string; // e.g., 'file:///path/to/your/module.js' // process is globally available for process.argv } export const nodeHelpers = new NodeHelpers();