index.ts•1.97 kB
import fs from 'node:fs/promises';
import path from 'node:path';
import { AppError } from '../lib/errors.js';
export type Config = {
gitlab?: { url: string; token: string };
jira?: { url: string; token: string };
};
const expandHome = (p: string) => p.replace(/^~(?=\/|$)/, process.env.HOME || '');
export async function loadConfig(configPath?: string): Promise<Config> {
const envCfg: Config = {
gitlab: process.env.GITLAB_URL && process.env.GITLAB_TOKEN
? { url: String(process.env.GITLAB_URL), token: String(process.env.GITLAB_TOKEN) }
: undefined,
jira: process.env.JIRA_URL && process.env.JIRA_TOKEN
? { url: String(process.env.JIRA_URL), token: String(process.env.JIRA_TOKEN) }
: undefined,
};
const defaultPath = path.join(process.env.HOME || '', '.mcp-gitlab-jira.json');
const candidate = expandHome(configPath || defaultPath);
let fileCfg: Config = {};
try {
const raw = await fs.readFile(candidate, 'utf8');
fileCfg = JSON.parse(raw);
} catch (_) {
// optional config file
}
// Env wins over file values if both provided
const merged: Config = {
gitlab: envCfg.gitlab || fileCfg.gitlab,
jira: envCfg.jira || fileCfg.jira,
};
validateConfig(merged);
return merged;
}
export function validateConfig(cfg: Config) {
const hasGitLab = !!cfg.gitlab;
const hasJira = !!cfg.jira;
if (!hasGitLab && !hasJira) {
// Allow running without both, but typical usage expects at least one target
// Throwing here keeps failures explicit per PRD
throw new AppError(
'CONFIG_MISSING',
'No configuration provided. Set GitLab and/or Jira credentials via env or JSON file.'
);
}
const check = (name: 'gitlab' | 'jira') => {
const entry = cfg[name];
if (!entry) return;
if (!entry.url || !entry.token) {
throw new AppError('CONFIG_MISSING', `${name} configuration requires url and token`);
}
};
check('gitlab');
check('jira');
}