@glyphtek/scriptit
Version:
A cross-runtime CLI and library for running scripts with environment management, TUI, and support for lambda functions. Optimized for Bun with compatibility for Node.js and Deno.
145 lines • 6.54 kB
JavaScript
// src/common/utils/utils.ts
import { existsSync as pathExistsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import dotenv from "dotenv";
import { minimatch } from "minimatch";
import { logger } from "../logger/index.js";
export const DEFAULT_SCRIPTS_DIR = "scripts";
export const DEFAULT_TMP_DIR = "tmp";
export const DEFAULT_CONFIG_FILE = "runner.config.js"; // or .ts if you transpile
export function interpolateEnvVars(value, env) {
if (typeof value === "string") {
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => env[varName] || "");
}
if (Array.isArray(value)) {
return value.map((item) => interpolateEnvVars(item, env));
}
if (typeof value === "object" && value !== null) {
const result = {};
for (const key in value) {
result[key] = interpolateEnvVars(value[key], env);
}
return result;
}
return value;
}
export async function loadConfig(configPath) {
const resolvedConfigPath = path.resolve(configPath || DEFAULT_CONFIG_FILE);
logger.debug(`loadConfig: Attempting to load config from: ${resolvedConfigPath}`);
let userConfig = {};
if (pathExistsSync(resolvedConfigPath)) {
logger.debug(`loadConfig: Config file found: ${resolvedConfigPath}`);
try {
const importedConfig = await import(`${resolvedConfigPath}?v=${Date.now()}`);
userConfig = importedConfig.default || importedConfig;
logger.debug("loadConfig: User config loaded:", userConfig);
// Log exclude patterns if present
if (userConfig.excludePatterns) {
logger.debug("loadConfig: Exclude patterns found:", userConfig.excludePatterns);
}
else {
logger.debug("loadConfig: No exclude patterns defined in config");
}
}
catch (error) {
logger.warn(`loadConfig: Could not load config file ${resolvedConfigPath}:`, error);
}
}
else {
logger.debug(`loadConfig: Config file NOT found: ${resolvedConfigPath}. Using defaults.`);
}
const defaultConfig = {
scriptsDir: DEFAULT_SCRIPTS_DIR,
tmpDir: DEFAULT_TMP_DIR,
envFiles: [".env"],
defaultParams: {},
};
const mergedConfig = { ...defaultConfig, ...userConfig };
// Resolve directories to absolute paths AFTER merging
logger.debug(`loadConfig: Merged scriptsDir before resolve: ${mergedConfig.scriptsDir}`);
mergedConfig.scriptsDir = path.resolve(mergedConfig.scriptsDir);
logger.debug(`loadConfig: Merged tmpDir before resolve: ${mergedConfig.tmpDir}`);
mergedConfig.tmpDir = path.resolve(mergedConfig.tmpDir);
logger.debug(`loadConfig: Final effective scriptsDir: ${mergedConfig.scriptsDir}`);
logger.debug(`loadConfig: Final effective tmpDir: ${mergedConfig.tmpDir}`);
return mergedConfig;
}
export function loadEnvironmentVariables(envFiles) {
const loadedEnv = {};
// Load from specified .env files
for (const file of envFiles) {
if (pathExistsSync(path.resolve(file))) {
const result = dotenv.config({
path: path.resolve(file),
override: true,
}); // override ensures later files take precedence
if (result.parsed) {
Object.assign(loadedEnv, result.parsed);
}
}
}
// System environment variables take highest precedence over .env files
return { ...loadedEnv, ...process.env };
}
/**
* Recursively find all .js and .ts files in a directory.
* @param dirPath The directory to search.
* @param baseDir The original base directory for scripts (to create relative paths for display).
* @param currentRelativePath The current relative path from the baseDir (used in recursion).
* @param excludePatterns Optional array of glob patterns to exclude.
* @returns A promise that resolves to an array of full file paths.
*/
async function findScriptFilesRecursive(dirPath, baseDir, currentRelativePath = "", excludePatterns = []) {
let scriptFiles = [];
try {
if (!pathExistsSync(dirPath)) {
logger.error(`findScriptFilesRecursive: Directory DOES NOT EXIST: ${dirPath}`);
return [];
}
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullEntryPath = path.join(dirPath, entry.name);
const entryRelativePath = path.join(currentRelativePath, entry.name);
// Check if this entry should be excluded
const relativePathFromBase = path.relative(baseDir, fullEntryPath);
const shouldExclude = excludePatterns.some((pattern) => {
const matches = minimatch(relativePathFromBase, pattern, {
matchBase: true,
dot: true,
});
if (matches) {
logger.debug(`Excluded by pattern "${pattern}": ${relativePathFromBase}`);
}
return matches;
});
if (shouldExclude) {
logger.debug(`Excluded file/directory: ${relativePathFromBase} (matched exclude pattern)`);
continue; // Skip this entry
}
if (entry.isDirectory()) {
// Recursively search in subdirectories
scriptFiles = scriptFiles.concat(await findScriptFilesRecursive(fullEntryPath, baseDir, entryRelativePath, excludePatterns));
}
else if (entry.isFile() &&
(entry.name.endsWith(".js") || entry.name.endsWith(".ts"))) {
// Add script file (store its full absolute path)
scriptFiles.push(fullEntryPath);
}
}
}
catch (error) {
logger.warn(`Error reading directory ${dirPath}:`, error);
}
return scriptFiles;
}
export async function getScriptFiles(scriptsBaseDir, excludePatterns = []) {
logger.debug(`getScriptFiles: Called with scriptsBaseDir: "${scriptsBaseDir}"`);
if (excludePatterns.length > 0) {
logger.debug(`getScriptFiles: Using exclude patterns: ${JSON.stringify(excludePatterns)}`);
}
const allScriptFullPaths = await findScriptFilesRecursive(scriptsBaseDir, scriptsBaseDir, "", excludePatterns);
logger.debug("getScriptFiles: Found script full paths (recursive):", allScriptFullPaths);
return allScriptFullPaths;
}
//# sourceMappingURL=utils.js.map