zephyr-agent
Version:
Zephyr plugin agent
174 lines (173 loc) • 6.98 kB
JavaScript
import "node:module";
import { existsSync, realpathSync, statSync } from "node:fs";
import { createRequire } from "node:module";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { createJiti } from "jiti";
import { ZeErrors, ZephyrError } from "../errors/index.mjs";
import { redactString } from "../security/redaction.mjs";
const CONFIG_FILE_NAMES = [
'zephyr.config.ts',
'zephyr.config.mts',
'zephyr.config.cts',
'zephyr.config.js',
'zephyr.config.mjs',
'zephyr.config.cjs'
];
const CONFIG_FIELDS = new Set([
'org',
'project',
'appName',
'remoteDependencies',
'dependencyUrlMode'
]);
const CONFIG_ERROR_MAX_LENGTH = 1000;
const configCache = new Map();
function defineConfig(config) {
return config;
}
function resolveZephyrContextDirectory(context) {
const startingPath = context?.trim() || process.cwd();
const absolutePath = isAbsolute(startingPath) ? startingPath : resolve(process.cwd(), startingPath);
try {
return statSync(absolutePath).isFile() ? dirname(absolutePath) : absolutePath;
} catch {
return absolutePath;
}
}
function getZephyrConfig(context) {
const configPath = findConfigPath(resolveZephyrContextDirectory(context));
if (!configPath) return Object.freeze({});
const canonicalPath = getCanonicalConfigPath(configPath);
const fingerprint = getConfigFingerprint(canonicalPath);
const cached = configCache.get(canonicalPath);
if (cached?.fingerprint === fingerprint) return cached.config;
const config = loadConfigModule(canonicalPath);
configCache.set(canonicalPath, {
fingerprint,
config
});
return config;
}
function mergeRemoteDependencies(packageDependencies, config) {
if (!packageDependencies && !config.remoteDependencies) return;
return {
...packageDependencies,
...config.remoteDependencies
};
}
function findConfigPath(startingDirectory) {
let directory = startingDirectory;
while(true){
const matches = CONFIG_FILE_NAMES.map((fileName)=>join(directory, fileName)).filter(isFile);
if (matches.length > 1) throwConfigError(directory, `multiple config files found: ${matches.map((file)=>basename(file)).join(', ')}`);
if (matches[0]) return matches[0];
const parent = dirname(directory);
if (parent === directory) return;
directory = parent;
}
}
function isFile(filePath) {
try {
return existsSync(filePath) && statSync(filePath).isFile();
} catch {
return false;
}
}
function getCanonicalConfigPath(configPath) {
try {
return realpathSync(configPath);
} catch (error) {
throwConfigError(configPath, `failed to resolve config path: ${safeConfigErrorMessage(error)}`);
}
}
function getConfigFingerprint(configPath) {
try {
const stats = statSync(configPath);
return `${stats.mtimeMs}:${stats.size}`;
} catch (error) {
throwConfigError(configPath, `failed to inspect config: ${safeConfigErrorMessage(error)}`);
}
}
function loadConfigModule(configPath) {
try {
const requireFromConfig = createRequire(configPath);
delete requireFromConfig.cache[configPath];
const jiti = createJiti(configPath, {
interopDefault: true,
moduleCache: false
});
const loaded = jiti(configPath);
const config = isObject(loaded) && 'default' in loaded ? loaded['default'] : loaded;
return parseConfig(config, configPath);
} catch (error) {
if (ZephyrError.is(error, ZeErrors.ERR_ZEPHYR_CONFIG_NOT_VALID)) throw error;
throwConfigError(configPath, `failed to load config: ${safeConfigErrorMessage(error)}`);
}
}
function parseConfig(value, configPath) {
if (!isObject(value)) throwConfigError(configPath, 'default export must be an object');
const unknownFields = Object.keys(value).filter((field)=>!CONFIG_FIELDS.has(field));
if (unknownFields.length > 0) throwConfigError(configPath, `unknown field${1 === unknownFields.length ? '' : 's'}: ${unknownFields.join(', ')}`);
const org = readOptionalString(value['org'], 'org', configPath);
const project = readOptionalString(value['project'], 'project', configPath);
const appName = readOptionalString(value['appName'], 'appName', configPath);
const remoteDependencies = readRemoteDependencies(value['remoteDependencies'], configPath);
const dependencyUrlMode = readDependencyUrlMode(value['dependencyUrlMode'], configPath);
return Object.freeze({
...org ? {
org
} : {},
...project ? {
project
} : {},
...appName ? {
appName
} : {},
...remoteDependencies ? {
remoteDependencies
} : {},
...dependencyUrlMode ? {
dependencyUrlMode
} : {}
});
}
function readDependencyUrlMode(value, configPath) {
if (void 0 === value) return;
if ('selector' !== value && 'version' !== value) throwConfigError(configPath, 'dependencyUrlMode must be either "selector" or "version"');
return value;
}
function readOptionalString(value, field, configPath) {
if (void 0 === value) return;
if ('string' != typeof value) throwConfigError(configPath, `${field} must be a string`);
const normalized = value.trim();
if (!normalized) throwConfigError(configPath, `${field} must not be empty`);
return normalized;
}
function readRemoteDependencies(value, configPath) {
if (void 0 === value) return;
if (!isObject(value)) throwConfigError(configPath, 'remoteDependencies must be an object with string values');
const dependencies = {};
for (const [name, reference] of Object.entries(value)){
if (!name.trim()) throwConfigError(configPath, 'remoteDependencies keys must not be empty');
if ('string' != typeof reference) throwConfigError(configPath, `remoteDependencies.${name} must be a string`);
if (!reference.trim()) throwConfigError(configPath, `remoteDependencies.${name} must not be empty`);
dependencies[name] = reference.trim();
}
return Object.freeze(dependencies);
}
function isObject(value) {
return null !== value && 'object' == typeof value && !Array.isArray(value);
}
function safeConfigErrorMessage(error) {
const rawMessage = error instanceof Error ? error.message : String(error);
const redacted = redactString(rawMessage);
if (redacted.length <= CONFIG_ERROR_MAX_LENGTH) return redacted;
return `${redacted.slice(0, CONFIG_ERROR_MAX_LENGTH)}…[truncated]`;
}
function throwConfigError(configPath, message) {
throw new ZephyrError(ZeErrors.ERR_ZEPHYR_CONFIG_NOT_VALID, {
message: `at ${configPath}: ${safeConfigErrorMessage(message)}`
});
}
export { defineConfig, getZephyrConfig, mergeRemoteDependencies, resolveZephyrContextDirectory };
//# sourceMappingURL=zephyr-config.mjs.map