git-contextor
Version:
A code context tool with vector search and real-time monitoring, with optional Git integration.
155 lines (138 loc) • 6.4 kB
JavaScript
const logger = require('./logger');
const ora = require('ora');
const inquirer = require('inquirer');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
/**
* Checks if the Qdrant service is available at the configured URL.
* @param {string} host
* @param {number} port
* @returns {Promise<boolean>}
*/
async function isQdrantReady(host, port) {
const url = `http://${host}:${port}`;
const isInteractive = process.stdout.isTTY;
const spinner = isInteractive ? ora(`Checking Qdrant connection at ${url}...`).start() : null;
try {
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
if (response.ok) {
if (spinner) {
spinner.succeed(`Qdrant connection successful at ${url}`);
} else {
logger.debug(`Qdrant connection successful at ${url}`);
}
return true;
}
const message = `Qdrant at ${url} responded with status ${response.status}`;
if (spinner) {
spinner.fail(message);
} else {
logger.warn(message);
}
return false;
} catch (error) {
let message;
if (error.name === 'AbortError' || error.code === 'ECONNREFUSED') {
message = `Could not connect to Qdrant at ${url}.`;
if (spinner) spinner.fail(message); else logger.warn(message);
} else {
message = `An unexpected error occurred while checking Qdrant: ${error.message}`;
if (spinner) spinner.fail(message); else logger.error(message);
}
return false;
}
}
/**
* Checks for a running Qdrant instance and provides interactive prompts if not found.
* @param {import('../../core/ConfigManager')} configManager - The application's config manager.
* @returns {Promise<void>}
*/
async function checkQdrant(configManager) {
const { provider, qdrant } = configManager.config.vectorStore;
let { host: qdrantHost, port: qdrantPort } = qdrant;
// Only check Qdrant if it's the configured provider or auto-selected with a host
if (provider === 'memory' || (provider === 'auto' && !qdrantHost)) {
return;
}
if (qdrantHost && await isQdrantReady(qdrantHost, qdrantPort)) {
return;
}
// Do not prompt if not in an interactive terminal
if (!process.stdout.isTTY) {
const errorMsg = 'Qdrant connection failed. Run in an interactive terminal to configure or start Qdrant.';
logger.error(errorMsg);
throw new Error(errorMsg);
}
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Qdrant connection failed. What would you like to do?',
choices: [
{ name: 'Enter a different host and port', value: 'enter_new' },
{ name: 'Attempt to start Qdrant via Docker (requires Docker & docker-compose)', value: 'start_docker' },
{ name: 'Abort', value: 'abort' },
],
},
]);
if (action === 'abort') {
logger.info('Aborting operation.');
throw new Error('User aborted Qdrant setup.');
}
if (action === 'enter_new') {
const { newHost, newPort } = await inquirer.prompt([
{ type: 'input', name: 'newHost', message: 'Enter Qdrant host:', default: qdrantHost || 'localhost' },
{ type: 'input', name: 'newPort', message: 'Enter Qdrant port:', default: qdrantPort, validate: input => !isNaN(parseInt(input, 10)) || 'Please enter a valid port number.' },
]);
qdrantHost = newHost;
qdrantPort = parseInt(newPort, 10);
if (await isQdrantReady(qdrantHost, qdrantPort)) {
// Explicitly set provider to 'qdrant' since user is providing details for it.
await configManager.updateConfig({ vectorStore: { provider: 'qdrant', qdrant: { host: qdrantHost, port: qdrantPort } } });
logger.success(`Configuration updated to use Qdrant at: ${qdrantHost}:${qdrantPort}`);
return;
} else {
const errorMsg = 'Still unable to connect with the new details. Aborting.';
logger.error(errorMsg);
throw new Error(errorMsg);
}
}
if (action === 'start_docker') {
const spinner = ora('Attempting to start Qdrant Docker container...').start();
try {
let packagePath;
try {
// This works when git-contextor is an installed dependency
packagePath = path.dirname(require.resolve('git-contextor/package.json'));
} catch (e) {
// This is a fallback for local development
packagePath = path.resolve(__dirname, '../../..');
}
const composeFile = path.join(packagePath, 'docker', 'docker-compose.yml');
if (!fs.existsSync(composeFile)) {
spinner.fail('Could not find docker-compose.yml file. Please start Qdrant manually.');
const errorMsg = 'You can usually start it by running: docker-compose -f /path/to/git-contextor/docker/docker-compose.yml up -d';
logger.info(errorMsg);
throw new Error('docker-compose.yml not found.');
}
execSync(`docker-compose -f "${composeFile}" up -d`, { stdio: 'ignore' });
spinner.succeed('Docker command executed. Waiting 5 seconds for Qdrant to start...');
await new Promise(resolve => setTimeout(resolve, 5000));
if (await isQdrantReady(qdrantHost, qdrantPort)) {
return;
} else {
spinner.fail('Qdrant container was started, but the service is still not responding.');
const errorMsg = 'Please check the Docker container logs and try again.';
logger.error(errorMsg);
throw new Error('Qdrant from Docker is not responding.');
}
} catch (error) {
spinner.fail('Failed to start Qdrant via Docker.');
logger.error(error.message);
logger.info('Please ensure Docker and docker-compose are installed and running.');
throw error; // re-throw original or new error
}
}
}
module.exports = { checkQdrant };