UNPKG

@debugg-ai/debugg-ai-mcp

Version:

Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.

114 lines (113 loc) 4.21 kB
/** * Project Context Service * At startup: detect repo → resolve project → fetch environments + credentials. * Exposes the result so tool descriptions can be enriched dynamically. */ import { config } from '../config/index.js'; import { DebuggAIServerClient } from './index.js'; import { detectRepoName } from '../utils/gitContext.js'; import { Logger } from '../utils/logger.js'; const logger = new Logger({ module: 'projectContext' }); let cached = null; let inFlight = null; /** * Resolve the current project context: repo → project → environments → credentials. * * Caches the first successful result. Concurrent calls share a single in-flight * promise. Failures are NOT cached — the next call will retry — so a transient * network error on the first tool call doesn't permanently disable the service. */ const STARTUP_TIMEOUT_MS = 10_000; export async function resolveProjectContext() { if (cached) return cached; if (inFlight) return inFlight; inFlight = (async () => { const repoName = detectRepoName(); if (!repoName) { logger.info('No git repo detected — skipping project context'); return null; } let timer = null; try { const result = await Promise.race([ resolveProjectContextInner(repoName), new Promise((resolve) => { timer = setTimeout(() => { logger.warn('Project context resolution timed out'); resolve(null); }, STARTUP_TIMEOUT_MS); }), ]); if (result) cached = result; return result; } catch (err) { logger.warn(`Failed to resolve project context: ${err}`); return null; } finally { if (timer) clearTimeout(timer); } })().finally(() => { inFlight = null; }); return inFlight; } async function resolveProjectContextInner(repoName) { try { const client = new DebuggAIServerClient(config.api.key); await client.init(); const project = await client.findProjectByRepoName(repoName); if (!project) { logger.info(`No project found for repo "${repoName}"`); return null; } logger.info(`Resolved project: ${project.name} (${project.uuid})`); // Fetch environments for this project const envResponse = await client.tx.get(`api/v1/projects/${project.uuid}/environments/`); const rawEnvs = envResponse?.results ?? []; // Fetch credentials for each environment in parallel const environments = await Promise.all(rawEnvs .filter((e) => e.isActive) .map(async (env) => { let credentials = []; try { const credResponse = await client.tx.get(`api/v1/projects/${project.uuid}/environments/${env.uuid}/credentials/`); credentials = (credResponse?.results ?? []) .filter((c) => c.isActive) .map((c) => ({ uuid: c.uuid, label: c.label || c.username, username: c.username, role: c.role, environmentName: env.name, environmentUuid: env.uuid, })); } catch { // Some environments may not support credentials } return { uuid: env.uuid, name: env.name, url: env.url || env.activeUrl || '', credentials, }; })); cached = { repoName, project, environments }; const totalCreds = environments.reduce((n, e) => n + e.credentials.length, 0); logger.info(`Project context ready: ${environments.length} environments, ${totalCreds} credentials`); return cached; } catch (err) { logger.warn(`Failed to resolve project context: ${err}`); return null; } } export function getProjectContext() { return cached; }