@elsikora/commitizen-plugin-commitlint-ai
Version:
AI-powered Commitizen adapter with Commitlint integration
182 lines (178 loc) • 7.58 kB
JavaScript
;
var node_fs = require('node:fs');
var node_path = require('node:path');
var chalk = require('chalk');
// Store config in project directory
const CONFIG_DIR = "./.elsikora";
const CONFIG_FILE = node_path.join(CONFIG_DIR, "commitlint-ai.config.js");
// In-memory cache
let llmConfig = null;
// Track if we've already shown mode error
// eslint-disable-next-line @elsikora-typescript/naming-convention
let modeErrorShown = false;
// Check for API keys in environment variables
const getApiKeyFromEnvironment = (provider) => {
try {
if (typeof process === "undefined" || !process?.env) {
return null;
}
if (provider === "openai") {
return process.env.OPENAI_API_KEY ?? null;
}
else if (provider === "anthropic") {
return process.env.ANTHROPIC_API_KEY ?? null;
}
}
catch (error) {
console.warn("Error accessing environment variables:", error);
}
return null;
};
// Try to load config from file
const loadConfigFromFile = () => {
try {
if (node_fs.existsSync(CONFIG_FILE)) {
// Check if there's an old JSON file and migrate it
const oldJsonFile = node_path.join(CONFIG_DIR, "commitlint-ai.json");
if (node_fs.existsSync(oldJsonFile)) {
try {
const oldConfigString = node_fs.readFileSync(oldJsonFile, "utf8");
const oldConfig = JSON.parse(oldConfigString);
// Save to the new JS format
saveConfigToFile({
...oldConfig,
apiKey: "",
});
return oldConfig;
}
catch {
// Ignore errors with old file
}
}
// Parse the ESM module format
const configContent = node_fs.readFileSync(CONFIG_FILE, "utf8");
try {
// Use a safer approach than regex + JSON.parse
// Execute the file as a JavaScript module using Node's module system
// This is a workaround since we can't directly import a dynamic path in ESM
// Simple approach: parse the JS object manually
const objectPattern = /export\s+default\s+(\{[\s\S]*?\});/;
const match = objectPattern.exec(configContent);
if (match?.[1]) {
// Extract the object text
const objectText = match[1];
// Extract property assignments with a more robust approach
const properties = {};
// Match each property in the format: key: value,
// eslint-disable-next-line @elsikora-sonar/slow-regex
const propertyRegex = /\s*(\w+)\s*:\s*["']?([^,"'}\s]+)["']?\s*,?/g;
// eslint-disable-next-line @elsikora-typescript/typedef
let propertyMatch;
// eslint-disable-next-line @elsikora-typescript/no-unsafe-argument
while ((propertyMatch = propertyRegex.exec(objectText)) !== null) {
const [, key, value] = propertyMatch;
// Remove quotes if present
// eslint-disable-next-line @elsikora-sonar/anchor-precedence,@elsikora-typescript/no-unsafe-assignment,@elsikora-typescript/no-unsafe-member-access,@elsikora-typescript/no-unsafe-call
const cleanValue = value.replaceAll(/^["']|["']$/g, "");
// eslint-disable-next-line @elsikora-typescript/no-unsafe-assignment,@elsikora-typescript/no-unsafe-member-access
properties[key] = cleanValue;
}
// Validate mode if present (but only show the error once)
if (properties.mode && properties.mode !== "auto" && properties.mode !== "manual") {
if (!modeErrorShown) {
// eslint-disable-next-line @elsikora-typescript/restrict-template-expressions
console.log(chalk.yellow(`Invalid mode "${properties.mode}" in config. Valid values are "auto" or "manual". Using default mode.`));
modeErrorShown = true;
}
properties.mode = "auto";
}
return properties;
}
return null;
}
catch (parseError) {
console.warn("Error parsing config file:", parseError);
return null;
}
}
}
catch (error) {
console.warn("Error loading LLM config from file:", error);
}
return null;
};
// Save config to file (without API key)
const saveConfigToFile = (config) => {
try {
if (!node_fs.existsSync(CONFIG_DIR)) {
// eslint-disable-next-line @elsikora-typescript/naming-convention
node_fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
// Only store provider, model, and mode (not the API key)
const storageConfig = {
mode: config.mode,
model: config.model,
provider: config.provider,
};
// Format as an ESM module with proper JS object format (no quotes around keys)
// Always include the mode field, using 'auto' as default if not specified
const jsContent = `export default {
provider: ${JSON.stringify(storageConfig.provider)},
model: ${JSON.stringify(storageConfig.model)},
mode: ${JSON.stringify(storageConfig.mode ?? "auto")}
};`;
node_fs.writeFileSync(CONFIG_FILE, jsContent, "utf8");
// Remove old JSON file if it exists
const oldJsonFile = node_path.join(CONFIG_DIR, "commitlint-ai.json");
if (node_fs.existsSync(oldJsonFile)) {
try {
// Use fs.unlink to delete the file - but we'll use writeFileSync with empty content instead
// to avoid needing to import fs.unlink
node_fs.writeFileSync(oldJsonFile, "", "utf8");
}
catch {
// Ignore errors with old file deletion
}
}
}
catch (error) {
console.warn("Error saving LLM config to file:", error);
}
};
const setLLMConfig = (config) => {
llmConfig = config;
if (config) {
// For debugging
console.log("Saving config:", JSON.stringify({ ...config, apiKey: "[REDACTED]" }));
saveConfigToFile(config);
}
};
const getLLMConfig = () => {
// If we already have a config in memory, return it
if (llmConfig) {
return llmConfig;
}
// Otherwise try to load from file
const fileConfig = loadConfigFromFile();
if (fileConfig) {
// Check if we have API key in environment
const apiKey = getApiKeyFromEnvironment(fileConfig.provider);
// We have both the saved config and an API key
if (apiKey) {
llmConfig = {
...fileConfig,
apiKey,
};
return llmConfig;
}
// Return the partial config (without API key) so we can ask for it
return {
...fileConfig,
apiKey: "", // Empty string signals that we need to ask for the key
};
}
return null;
};
exports.getLLMConfig = getLLMConfig;
exports.setLLMConfig = setLLMConfig;
//# sourceMappingURL=config.js.map