vibe-codex
Version:
CLI tool to install development rules and git hooks with interactive configuration
336 lines (292 loc) • 8.68 kB
JavaScript
/**
* Doctor command - Diagnose vibe-codex installation issues
*/
const chalk = require("chalk");
const ora = require("ora");
const fs = require("fs-extra");
const path = require("path");
const { exec } = require("child_process");
const { promisify } = require("util");
const {
validateSetup,
detectPackageManager,
checkNpxAvailability,
getInstallInstructions,
} = require("../utils/package-manager");
const execAsync = promisify(exec);
module.exports = async function doctor(options) {
console.log(chalk.blue("\n🩺 Running vibe-codex diagnostics...\n"));
const checks = [];
const issues = [];
const suggestions = [];
// Check Node.js version
const nodeCheck = await checkNodeVersion();
checks.push(nodeCheck);
if (!nodeCheck.passed) issues.push(nodeCheck);
// Check npm/npx availability
const npxCheck = await checkNpx();
checks.push(npxCheck);
if (!npxCheck.passed) issues.push(npxCheck);
// Check package manager
const pmCheck = await checkPackageManager();
checks.push(pmCheck);
if (!pmCheck.passed) issues.push(pmCheck);
// Check Git installation
const gitCheck = await checkGit();
checks.push(gitCheck);
if (!gitCheck.passed) issues.push(gitCheck);
// Check vibe-codex installation
const vibeCheck = await checkVibeCodex();
checks.push(vibeCheck);
if (!vibeCheck.passed) issues.push(vibeCheck);
// Check configuration
const configCheck = await checkConfiguration();
checks.push(configCheck);
if (!configCheck.passed) issues.push(configCheck);
// Check Git hooks
const hooksCheck = await checkGitHooks();
checks.push(hooksCheck);
if (!hooksCheck.passed) issues.push(hooksCheck);
// Display results
console.log(chalk.bold("\n📋 Diagnostic Results:\n"));
checks.forEach((check) => {
const icon = check.passed ? chalk.green("✓") : chalk.red("✗");
const status = check.passed ? chalk.green("PASSED") : chalk.red("FAILED");
console.log(`${icon} ${check.name}: ${status}`);
if (!check.passed && check.details) {
console.log(chalk.gray(` ${check.details}`));
}
});
// Display issues and fixes
if (issues.length > 0) {
console.log(chalk.red(`\n❌ Found ${issues.length} issue(s):\n`));
issues.forEach((issue, index) => {
console.log(chalk.red(`${index + 1}. ${issue.name}`));
console.log(chalk.gray(` ${issue.details}`));
if (issue.fix) {
console.log(chalk.yellow(` Fix: ${issue.fix}`));
}
console.log();
});
if (options.fix) {
console.log(chalk.blue("\n🔧 Attempting automatic fixes...\n"));
await attemptFixes(issues);
} else {
console.log(
chalk.yellow("💡 Run with --fix flag to attempt automatic fixes"),
);
}
} else {
console.log(
chalk.green(
"\n✨ All checks passed! vibe-codex is properly configured.\n",
),
);
}
// Show additional suggestions
if (suggestions.length > 0) {
console.log(chalk.blue("\n💡 Suggestions:\n"));
suggestions.forEach((suggestion) => {
console.log(`- ${suggestion}`);
});
}
};
async function checkNodeVersion() {
try {
const { stdout } = await execAsync("node --version");
const version = stdout.trim();
const major = parseInt(version.slice(1).split(".")[0]);
if (major < 14) {
return {
name: "Node.js version",
passed: false,
details: `Version ${version} is too old (minimum: v14.0.0)`,
fix: "Update Node.js to v14 or higher",
};
}
return {
name: "Node.js version",
passed: true,
details: version,
};
} catch (error) {
return {
name: "Node.js version",
passed: false,
details: "Node.js not found",
fix: "Install Node.js from https://nodejs.org",
};
}
}
async function checkNpx() {
const npxInfo = await checkNpxAvailability();
if (!npxInfo.available) {
return {
name: "npx availability",
passed: false,
details: `npm ${npxInfo.npmVersion || "unknown"} - npx not available`,
fix: "Update npm: npm install -g npm@latest",
};
}
return {
name: "npx availability",
passed: true,
details: `npm ${npxInfo.npmVersion}`,
};
}
async function checkPackageManager() {
try {
const pm = await detectPackageManager();
const { stdout } = await execAsync(`${pm} --version`);
return {
name: "Package manager",
passed: true,
details: `${pm} ${stdout.trim()}`,
};
} catch (error) {
return {
name: "Package manager",
passed: false,
details: "Package manager not detected",
fix: "Ensure npm, yarn, or pnpm is installed",
};
}
}
async function checkGit() {
try {
const { stdout } = await execAsync("git --version");
const version = stdout.trim();
// Check if in a git repository
try {
await execAsync("git rev-parse --is-inside-work-tree");
return {
name: "Git",
passed: true,
details: `${version} (in repository)`,
};
} catch {
return {
name: "Git",
passed: false,
details: `${version} (not in a git repository)`,
fix: "Run: git init",
};
}
} catch (error) {
return {
name: "Git",
passed: false,
details: "Git not found",
fix: "Install Git from https://git-scm.com",
};
}
}
async function checkVibeCodex() {
// Check if vibe-codex is accessible
const checks = [
{ method: "npx", command: "npx --no-install vibe-codex --version" },
{ method: "local", command: "./node_modules/.bin/vibe-codex --version" },
{ method: "global", command: "vibe-codex --version" },
];
for (const check of checks) {
try {
const { stdout } = await execAsync(check.command);
return {
name: "vibe-codex installation",
passed: true,
details: `Found via ${check.method}: ${stdout.trim()}`,
};
} catch {
// Continue to next method
}
}
const pm = await detectPackageManager();
const instructions = getInstallInstructions(pm);
return {
name: "vibe-codex installation",
passed: false,
details: "vibe-codex not found in PATH",
fix: `Install with: ${instructions.local}`,
};
}
async function checkConfiguration() {
const configPath = path.join(process.cwd(), ".vibe-codex.json");
try {
const config = await fs.readJSON(configPath);
// Validate configuration structure
if (!config.version || !config.modules) {
return {
name: "Configuration file",
passed: false,
details: "Invalid configuration structure",
fix: "Run: npx vibe-codex init --force",
};
}
const moduleCount = Object.keys(config.modules).filter(
(m) => config.modules[m].enabled,
).length;
return {
name: "Configuration file",
passed: true,
details: `Valid (${moduleCount} modules enabled)`,
};
} catch (error) {
return {
name: "Configuration file",
passed: false,
details: "Configuration file not found",
fix: "Run: npx vibe-codex init",
};
}
}
async function checkGitHooks() {
const hooksDir = path.join(process.cwd(), ".git", "hooks");
const requiredHooks = ["pre-commit", "commit-msg"];
const foundHooks = [];
const missingHooks = [];
for (const hook of requiredHooks) {
const hookPath = path.join(hooksDir, hook);
try {
const content = await fs.readFile(hookPath, "utf8");
if (content.includes("vibe-codex")) {
foundHooks.push(hook);
} else {
missingHooks.push(hook);
}
} catch {
missingHooks.push(hook);
}
}
if (missingHooks.length > 0) {
return {
name: "Git hooks",
passed: false,
details: `Missing hooks: ${missingHooks.join(", ")}`,
fix: "Run: npx vibe-codex update --hooks",
};
}
return {
name: "Git hooks",
passed: true,
details: `All hooks installed (${foundHooks.join(", ")})`,
};
}
async function attemptFixes(issues) {
const spinner = ora();
for (const issue of issues) {
if (issue.fix && issue.fix.startsWith("Run:")) {
const command = issue.fix.replace("Run: ", "");
spinner.start(`Fixing: ${issue.name}`);
try {
await execAsync(command);
spinner.succeed(`Fixed: ${issue.name}`);
} catch (error) {
spinner.fail(`Failed to fix: ${issue.name}`);
console.error(chalk.gray(` ${error.message}`));
}
} else {
console.log(chalk.yellow(`⚠️ Manual fix required for: ${issue.name}`));
console.log(chalk.gray(` ${issue.fix}`));
}
}
}