gather-ts
Version:
A powerful code analysis and packaging tool designed for creating AI-friendly code representations for javascript and typescript projects.
372 lines • 13.3 kB
JavaScript
;
// src/config/ConfigManager.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigManager = void 0;
const events_1 = require("events");
const errors_1 = require("@/errors");
const services_1 = require("@/types/services");
// Create a class that extends EventEmitter and implements needed methods
class EventEmitterBase extends events_1.EventEmitter {
on(event, listener) {
return super.on(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
removeAllListeners(event) {
return super.removeAllListeners(event);
}
}
const defaultConfig = {
maxDepth: 5,
topFilesCount: 5,
showTokenCount: true,
tokenizer: {
model: "gpt-4",
showWarning: true,
},
outputFormat: {
includeSummaryInFile: true,
includeGenerationTime: true,
includeUsageGuidelines: true,
},
debug: false,
cacheTokenCounts: true,
};
class ConfigManager extends services_1.BaseService {
constructor(projectRoot, deps, options = {}) {
super();
this.deps = deps;
this.metrics = {
loads: 0,
updates: 0,
validationErrors: 0,
lastUpdate: undefined,
};
this.eventEmitter = new EventEmitterBase();
if (!projectRoot || !this.deps.fileSystem.exists(projectRoot)) {
throw new errors_1.ConfigurationError("Project root directory does not exist", projectRoot);
}
this.projectRoot = projectRoot;
this.debug = options.debug || false;
this.watch = options.watch || false;
this.configPath =
options.configPath ||
this.deps.fileSystem.joinPath(projectRoot, "gather-ts.config.json");
this.config = { ...defaultConfig };
this.initializeMetrics();
this.logDebug(`ConfigManager created with root: ${projectRoot}`);
this.logDebug(`Using config path: ${this.configPath}`);
}
// Delegate EventEmitter methods
on(event, listener) {
this.eventEmitter.on(event, listener);
return this;
}
emit(event, ...args) {
return this.eventEmitter.emit(event, ...args);
}
removeAllListeners(event) {
this.eventEmitter.removeAllListeners(event);
return this;
}
async initialize() {
this.logDebug("Initializing ConfigManager");
try {
await super.initialize();
await this.loadConfig();
if (this.watch) {
this.startWatching();
}
this.logDebug("ConfigManager initialization complete");
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to initialize ConfigManager: ${error instanceof Error ? error.message : String(error)}`, this.configPath);
}
}
cleanup() {
this.logDebug("Cleaning up ConfigManager");
if (this.watchHandler) {
clearInterval(this.watchHandler);
this.watchHandler = undefined;
}
this.removeAllListeners();
super.cleanup();
}
initializeMetrics() {
this.metrics = {
loads: 0,
updates: 0,
validationErrors: 0,
lastUpdate: undefined,
};
}
logDebug(message) {
if (this.debug) {
this.deps.logger.debug(message);
}
}
getConfig() {
this.checkInitialized();
return { ...this.config };
}
startWatching() {
this.logDebug("Starting config file watch");
if (this.watchHandler) {
clearInterval(this.watchHandler);
}
this.watchHandler = setInterval(async () => {
try {
const stats = this.deps.fileSystem.statSync(this.configPath);
if (stats.mtime.getTime() !== this.metrics.lastUpdate) {
this.logDebug("Config file change detected");
await this.loadConfig({ isWatch: true });
}
}
catch (error) {
this.deps.logger.warn(`Error watching config file: ${error instanceof Error ? error.message : String(error)}`);
}
}, 1000);
}
async loadConfig(options = {}) {
this.logDebug(`Loading configuration${options.isWatch ? " (watch)" : ""}`);
let fileConfig = {};
try {
if (this.deps.fileSystem.exists(this.configPath)) {
const configContent = await this.deps.fileSystem.readFile(this.configPath);
fileConfig = JSON.parse(configContent);
this.logDebug("Loaded configuration from file");
}
else {
this.logDebug("No config file found, using defaults");
}
const newConfig = {
...defaultConfig,
...fileConfig,
};
const validation = this.validateConfig(newConfig);
if (!validation.isValid) {
throw new errors_1.ConfigurationError(`Configuration validation failed: ${validation.errors.join(", ")}`, this.configPath);
}
const oldConfig = this.config;
this.config = newConfig;
this.metrics.loads++;
this.metrics.lastUpdate = Date.now();
this.emit("configChange", {
type: "update",
oldConfig,
newConfig,
timestamp: Date.now(),
isWatch: options.isWatch,
});
}
catch (error) {
this.metrics.validationErrors++;
throw new errors_1.ConfigurationError(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`, this.configPath);
}
}
async updateConfig(updates) {
this.logDebug("Updating configuration");
try {
const newConfig = {
...this.config,
...updates,
};
const validation = this.validateConfig(newConfig);
if (!validation.isValid) {
throw new errors_1.ValidationError(`Invalid configuration updates: ${validation.errors.join(", ")}`);
}
const oldConfig = this.config;
this.config = newConfig;
this.metrics.updates++;
this.metrics.lastUpdate = Date.now();
await this.saveConfig();
this.emit("configChange", {
type: "update",
oldConfig,
newConfig,
timestamp: Date.now(),
});
return this.getConfig();
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to update configuration: ${error instanceof Error ? error.message : String(error)}`, this.configPath);
}
}
async saveConfig() {
this.logDebug("Saving configuration");
try {
const configString = JSON.stringify(this.config, null, 2);
await this.deps.fileSystem.writeFile(this.configPath, configString);
this.logDebug("Configuration saved successfully");
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`, this.configPath);
}
}
reset() {
this.logDebug("Resetting configuration to defaults");
const oldConfig = this.config;
this.config = { ...defaultConfig };
this.metrics.updates++;
this.metrics.lastUpdate = Date.now();
this.emit("configChange", {
type: "reset",
oldConfig,
newConfig: this.config,
timestamp: Date.now(),
});
}
getMetrics() {
return { ...this.metrics };
}
onConfigChange(callback) {
this.on("configChange", callback);
}
getProjectRoot() {
return this.projectRoot;
}
getTokenizerModel() {
return this.config.tokenizer.model;
}
getMaxDepth() {
return this.config.maxDepth;
}
shouldShowTokenCount() {
return this.config.showTokenCount;
}
getTopFilesCount() {
return this.config.topFilesCount;
}
isDebugEnabled() {
return this.debug;
}
getModelTokenLimit() {
const limits = {
"gpt-3.5-turbo": 16384,
"gpt-4": 8192,
"gpt-4o": 128000,
"gpt-4o-mini": 128000,
o1: 128000,
"o1-mini": 128000,
"o3-mini": 200000,
};
const limit = limits[this.config.tokenizer.model];
if (!limit) {
throw new errors_1.ConfigurationError(`Unsupported model: ${this.config.tokenizer.model}`, this.configPath);
}
return limit;
}
getTokenWarning(totalTokens) {
if (!this.config.tokenizer.showWarning)
return null;
const limit = this.getModelTokenLimit();
if (totalTokens > limit) {
return `Total tokens (${totalTokens.toLocaleString()}) exceed ${this.config.tokenizer.model}'s context limit of ${limit.toLocaleString()}`;
}
return null;
}
validateConfig(config) {
this.logDebug("Validating configuration");
const result = {
isValid: true,
errors: [],
warnings: [],
};
try {
// Validate with deps.validator
this.deps.validator.validate(config, "config", {
requiredFields: ["tokenizer", "outputFormat"],
});
// Validate tokenizer configuration
if (config.tokenizer) {
this.validateTokenizerConfig(config, result);
}
// Validate output format
if (config.outputFormat) {
this.validateOutputFormat(config, result);
}
// Validate max depth
if (config.maxDepth !== undefined) {
if (typeof config.maxDepth !== "number" || config.maxDepth < 0) {
result.errors.push("maxDepth must be a positive number or undefined");
result.isValid = false;
}
else if (config.maxDepth > 10) {
result.warnings.push("High maxDepth value may impact performance");
}
}
// Validate top files count
if (config.topFilesCount !== undefined) {
if (typeof config.topFilesCount !== "number" ||
config.topFilesCount < 1) {
result.errors.push("topFilesCount must be a positive number");
result.isValid = false;
}
else if (config.topFilesCount > 20) {
result.warnings.push("Large topFilesCount value may impact readability");
}
}
}
catch (error) {
result.errors.push(error instanceof Error ? error.message : String(error));
result.isValid = false;
}
// Update metrics if validation failed
if (!result.isValid) {
this.metrics.validationErrors++;
}
return result;
}
validateTokenizerConfig(config, result) {
const validModels = [
"gpt-3.5-turbo",
"gpt-4",
"gpt-4o",
"gpt-4o-mini",
"o1",
"o1-mini",
"o3-mini",
];
if (!config.tokenizer) {
result.errors.push("Missing tokenizer configuration");
result.isValid = false;
return;
}
if (!validModels.includes(config.tokenizer.model)) {
result.errors.push(`Invalid tokenizer model. Valid models are: ${validModels.join(", ")}`);
result.isValid = false;
}
if (typeof config.tokenizer.showWarning !== "boolean") {
result.errors.push("tokenizer.showWarning must be a boolean");
result.isValid = false;
}
}
validateOutputFormat(config, result) {
if (!config.outputFormat) {
result.errors.push("Missing output format configuration");
result.isValid = false;
return;
}
const booleanFields = [
"includeSummaryInFile",
"includeGenerationTime",
"includeUsageGuidelines",
];
booleanFields.forEach((field) => {
if (config.outputFormat[field] !== undefined &&
typeof config.outputFormat[field] !== "boolean") {
result.errors.push(`outputFormat.${field} must be a boolean`);
result.isValid = false;
}
});
if (config.outputFormat.format !== undefined &&
!["json", "text", "markdown"].includes(config.outputFormat.format)) {
result.errors.push("outputFormat.format must be one of: json, text, markdown");
result.isValid = false;
}
}
}
exports.ConfigManager = ConfigManager;
Object.assign(ConfigManager.prototype, events_1.EventEmitter.prototype);
//# sourceMappingURL=ConfigManager.js.map