Skip to main content
Glama
repair.ts23.9 kB
/** * WP Navigator Init Repair Mode * * Provides idempotent repair functionality for wpnav init. * Detects existing configuration, validates files, and offers * to fix/regenerate broken or missing components. * * @package WP_Navigator_Pro * @since 2.4.5 */ import * as fs from 'fs'; import * as path from 'path'; import { confirmPrompt, selectPrompt } from '../tui/prompts.js'; import { success, error as errorMessage, warning, info, newline, keyValue, createSpinner, colorize, list, divider, symbols, } from '../tui/components.js'; import { generateClaudeMd, generateAgentsMd, generateGeminiMd, generateMcpJson, getDefaultClaudeMdContext, } from './generators.js'; import { generateGitignore, generateGitignoreAppend } from './gitignore.js'; import { loadWpnavConfig, toLegacyConfig } from '../../wpnav-config.js'; import { makeWpRequest } from '../../http.js'; // ============================================================================= // Types // ============================================================================= export type FileStatus = 'valid' | 'missing' | 'invalid' | 'outdated'; export interface FileCheckResult { path: string; name: string; status: FileStatus; message?: string; canRegenerate: boolean; } export interface CredentialCheckResult { exists: boolean; valid: boolean; message?: string; siteUrl?: string; username?: string; } export interface RepairState { hasExistingConfig: boolean; files: FileCheckResult[]; credentials: CredentialCheckResult; needsRepair: boolean; missingFiles: string[]; invalidFiles: string[]; } export interface RepairResult { success: boolean; filesRepaired: string[]; filesSkipped: string[]; errors: string[]; } // ============================================================================= // File Definitions // ============================================================================= /** * List of files managed by wpnav init with their validation functions */ const MANAGED_FILES = [ { name: 'wpnavigator.jsonc', path: 'wpnavigator.jsonc', required: true, canRegenerate: true, validate: validateManifest, }, { name: 'wpnav.config.json', path: 'wpnav.config.json', required: false, canRegenerate: true, validate: validateWpnavConfig, }, { name: '.wpnav.env', path: '.wpnav.env', required: false, canRegenerate: false, // Contains credentials - don't regenerate validate: validateEnvFile, }, { name: 'CLAUDE.md', path: 'CLAUDE.md', required: false, canRegenerate: true, validate: validateClaudeMd, }, { name: 'AGENTS.md', path: 'AGENTS.md', required: false, canRegenerate: true, validate: validateAgentsMd, }, { name: 'GEMINI.md', path: 'GEMINI.md', required: false, canRegenerate: true, validate: validateGeminiMd, }, { name: '.mcp.json', path: '.mcp.json', required: false, canRegenerate: true, validate: validateMcpJson, }, { name: '.gitignore', path: '.gitignore', required: false, canRegenerate: true, validate: validateGitignore, }, { name: 'docs/README.md', path: 'docs/README.md', required: false, canRegenerate: true, validate: validateDocsReadme, }, ] as const; /** * Required directories for project structure */ const REQUIRED_DIRS = ['snapshots', 'snapshots/pages', 'roles', 'docs', 'sample-prompts']; // ============================================================================= // Validation Functions // ============================================================================= function validateManifest(content: string): { valid: boolean; message?: string } { try { // Remove JSONC comments for parsing // Be careful not to remove // in URLs - only match // at start of line or after whitespace const jsonContent = content .replace(/^\s*\/\/.*$/gm, '') // Line comments at start of line .replace(/,\s*\/\/.*$/gm, ',') // Line comments after commas .replace(/\/\*[\s\S]*?\*\//g, ''); // Block comments const parsed = JSON.parse(jsonContent); if (!parsed.schema_version) { return { valid: false, message: 'Missing schema_version field' }; } if (!parsed.site) { return { valid: false, message: 'Missing site section' }; } return { valid: true }; } catch (err) { return { valid: false, message: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`, }; } } function validateWpnavConfig(content: string): { valid: boolean; message?: string } { try { const parsed = JSON.parse(content); if (!parsed.config_version) { return { valid: false, message: 'Missing config_version field' }; } if (!parsed.environments) { return { valid: false, message: 'Missing environments section' }; } return { valid: true }; } catch (err) { return { valid: false, message: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`, }; } } function validateEnvFile(content: string): { valid: boolean; message?: string } { const hasBaseUrl = content.includes('WP_BASE_URL='); const hasUser = content.includes('WP_APP_USER='); const hasPass = content.includes('WP_APP_PASS='); if (!hasBaseUrl) { return { valid: false, message: 'Missing WP_BASE_URL' }; } if (!hasUser) { return { valid: false, message: 'Missing WP_APP_USER' }; } if (!hasPass) { return { valid: false, message: 'Missing WP_APP_PASS' }; } // Check if credentials are actually filled in (not just empty) const baseUrlMatch = content.match(/WP_BASE_URL=(.+)/); if (baseUrlMatch && baseUrlMatch[1].trim() === '') { return { valid: false, message: 'WP_BASE_URL is empty' }; } return { valid: true }; } function validateClaudeMd(content: string): { valid: boolean; message?: string } { // Check for WP Navigator header if (!content.includes('WP Navigator')) { return { valid: false, message: 'Not a WP Navigator CLAUDE.md file' }; } // Check for essential sections if (!content.includes('Quick Reference') && !content.includes('Commands')) { return { valid: false, message: 'Missing essential sections' }; } return { valid: true }; } function validateAgentsMd(content: string): { valid: boolean; message?: string } { if (!content.includes('WP Navigator')) { return { valid: false, message: 'Not a WP Navigator AGENTS.md file' }; } return { valid: true }; } function validateGeminiMd(content: string): { valid: boolean; message?: string } { if (!content.includes('WP Navigator')) { return { valid: false, message: 'Not a WP Navigator GEMINI.md file' }; } return { valid: true }; } function validateMcpJson(content: string): { valid: boolean; message?: string } { try { const parsed = JSON.parse(content); if (!parsed.mcpServers?.wpnav) { return { valid: false, message: 'Missing wpnav server configuration' }; } return { valid: true }; } catch (err) { return { valid: false, message: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`, }; } } function validateGitignore(content: string): { valid: boolean; message?: string } { // Check for essential patterns const essentialPatterns = ['.wpnav.env', 'wp-config.json']; const missingPatterns = essentialPatterns.filter((p) => !content.includes(p)); if (missingPatterns.length > 0) { return { valid: false, message: `Missing patterns: ${missingPatterns.join(', ')}` }; } return { valid: true }; } function validateDocsReadme(content: string): { valid: boolean; message?: string } { if (!content.includes('WP Navigator')) { return { valid: false, message: 'Not a WP Navigator docs/README.md file' }; } return { valid: true }; } // ============================================================================= // Detection Functions // ============================================================================= /** * Detect if this directory has an existing WP Navigator configuration */ export function detectExistingConfig(cwd: string): boolean { const indicators = [ 'wpnavigator.jsonc', 'wpnav.config.json', '.wpnav.env', 'snapshots/site_index.json', ]; return indicators.some((file) => fs.existsSync(path.join(cwd, file))); } /** * Check all managed files and directories */ export function checkProjectFiles(cwd: string): FileCheckResult[] { const results: FileCheckResult[] = []; for (const fileDef of MANAGED_FILES) { const filePath = path.join(cwd, fileDef.path); const result: FileCheckResult = { path: fileDef.path, name: fileDef.name, status: 'missing', canRegenerate: fileDef.canRegenerate, }; if (fs.existsSync(filePath)) { try { const content = fs.readFileSync(filePath, 'utf8'); const validation = fileDef.validate(content); if (validation.valid) { result.status = 'valid'; } else { result.status = 'invalid'; result.message = validation.message; } } catch (err) { result.status = 'invalid'; result.message = `Cannot read file: ${err instanceof Error ? err.message : String(err)}`; } } results.push(result); } // Check directories for (const dir of REQUIRED_DIRS) { const dirPath = path.join(cwd, dir); results.push({ path: dir + '/', name: dir, status: fs.existsSync(dirPath) ? 'valid' : 'missing', canRegenerate: true, }); } return results; } /** * Check credential validity by testing connection */ export async function checkCredentials(cwd: string): Promise<CredentialCheckResult> { const envPath = path.join(cwd, '.wpnav.env'); if (!fs.existsSync(envPath)) { return { exists: false, valid: false, message: 'No credentials file (.wpnav.env)', }; } // Parse env file to extract credentials const content = fs.readFileSync(envPath, 'utf8'); const siteUrlMatch = content.match(/WP_BASE_URL=(.+)/); const usernameMatch = content.match(/WP_APP_USER=(.+)/); const passwordMatch = content.match(/WP_APP_PASS=(.+)/); const siteUrl = siteUrlMatch?.[1]?.trim(); const username = usernameMatch?.[1]?.trim(); const password = passwordMatch?.[1]?.trim(); if (!siteUrl || !username || !password) { return { exists: true, valid: false, message: 'Credentials file is incomplete', siteUrl, username, }; } // Try to test connection try { const result = loadWpnavConfig({ fallbackToEnv: true, }); if (result.success && result.config) { const legacyConfig = toLegacyConfig(result.config); const wpRequest = makeWpRequest(legacyConfig); // Test with introspect endpoint await wpRequest('wpnav/v1/introspect'); return { exists: true, valid: true, siteUrl, username, }; } } catch { // Connection failed, but credentials exist } return { exists: true, valid: false, message: 'Connection test failed (credentials may be invalid or site unreachable)', siteUrl, username, }; } /** * Build complete repair state for the project */ export async function buildRepairState(cwd: string): Promise<RepairState> { const hasExistingConfig = detectExistingConfig(cwd); const files = checkProjectFiles(cwd); const credentials = await checkCredentials(cwd); const missingFiles = files.filter((f) => f.status === 'missing').map((f) => f.name); const invalidFiles = files.filter((f) => f.status === 'invalid').map((f) => f.name); const needsRepair = missingFiles.length > 0 || invalidFiles.length > 0 || !credentials.valid; return { hasExistingConfig, files, credentials, needsRepair, missingFiles, invalidFiles, }; } // ============================================================================= // Display Functions // ============================================================================= /** * Display repair state to user */ export function displayRepairState(state: RepairState): void { newline(); info('Checking configuration...'); newline(); for (const file of state.files) { const statusSymbol = getStatusSymbol(file.status); const statusColor = getStatusColor(file.status); const statusText = colorize(statusSymbol, statusColor); let line = ` ${statusText} ${file.name}`; if (file.status === 'invalid' && file.message) { line += ` - ${colorize(file.message, 'dim')}`; } console.error(line); } // Credentials status newline(); if (state.credentials.exists) { if (state.credentials.valid) { console.error(` ${colorize(symbols.success, 'green')} Credentials - working`); } else { console.error( ` ${colorize(symbols.error, 'red')} Credentials - ${state.credentials.message || 'invalid'}` ); } } else { console.error(` ${colorize(symbols.error, 'red')} Credentials - not configured`); } newline(); } function getStatusSymbol(status: FileStatus): string { switch (status) { case 'valid': return symbols.success; case 'missing': return symbols.error; case 'invalid': return symbols.error; case 'outdated': return symbols.warning; default: return symbols.dash; } } function getStatusColor(status: FileStatus): 'green' | 'red' | 'yellow' | 'dim' { switch (status) { case 'valid': return 'green'; case 'missing': case 'invalid': return 'red'; case 'outdated': return 'yellow'; default: return 'dim'; } } // ============================================================================= // Repair Functions // ============================================================================= /** * Regenerate a specific file */ function regenerateFile(cwd: string, fileName: string): boolean { const context = getDefaultClaudeMdContext(); try { switch (fileName) { case 'CLAUDE.md': { const content = generateClaudeMd(context); fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), content, 'utf8'); return true; } case 'AGENTS.md': { const content = generateAgentsMd(context); fs.writeFileSync(path.join(cwd, 'AGENTS.md'), content, 'utf8'); return true; } case 'GEMINI.md': { const content = generateGeminiMd(context); fs.writeFileSync(path.join(cwd, 'GEMINI.md'), content, 'utf8'); return true; } case '.mcp.json': { const content = generateMcpJson({ enableWrites: false }); fs.writeFileSync(path.join(cwd, '.mcp.json'), content, 'utf8'); return true; } case '.gitignore': { const gitignorePath = path.join(cwd, '.gitignore'); if (fs.existsSync(gitignorePath)) { // Append missing patterns const existing = fs.readFileSync(gitignorePath, 'utf8'); const appendContent = generateGitignoreAppend(existing); if (appendContent) { fs.appendFileSync(gitignorePath, appendContent); } } else { fs.writeFileSync(gitignorePath, generateGitignore(), 'utf8'); } return true; } case 'docs/README.md': { const docsDir = path.join(cwd, 'docs'); if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }); } fs.writeFileSync(path.join(cwd, 'docs', 'README.md'), generateDocsReadme(), 'utf8'); return true; } case 'wpnavigator.jsonc': { fs.writeFileSync(path.join(cwd, 'wpnavigator.jsonc'), generateManifestTemplate(), 'utf8'); return true; } case 'wpnav.config.json': { fs.writeFileSync(path.join(cwd, 'wpnav.config.json'), generateConfigTemplate(), 'utf8'); return true; } default: // Handle directories if (fileName.endsWith('/') || REQUIRED_DIRS.includes(fileName)) { const dirName = fileName.replace(/\/$/, ''); const dirPath = path.join(cwd, dirName); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } return true; } return false; } } catch { return false; } } /** * Generate minimal wpnavigator.jsonc template */ function generateManifestTemplate(): string { return `{ // WP Navigator Site Manifest // This file defines what you WANT your site to look like. // AI assistants read this file to understand your intent. // // Schema version: 1 "$schema": "https://wpnav.ai/schemas/wpnavigator.schema.json", "schema_version": 1, // Site identity "site": { "name": "", "tagline": "", "url": "" }, // Brand guidelines (optional - for Full Setup) "brand": { // "primary_color": "#0073aa", // "secondary_color": "#23282d", // "heading_font": "System UI", // "body_font": "System UI" }, // Pages to manage "pages": [ // Example: // { // "slug": "home", // "title": "Home", // "template": "front-page" // } ], // Plugins configuration "plugins": { // "woocommerce": { "status": "active" } } } `; } /** * Generate minimal wpnav.config.json template */ function generateConfigTemplate(): string { return JSON.stringify( { config_version: '1.0', default_environment: 'local', environments: { local: { site: '', user: '', password: '$WP_APP_PASS', }, }, }, null, 2 ); } /** * Generate docs/README.md content */ function generateDocsReadme(): string { return `# WP Navigator Project This folder contains your WP Navigator project files. ## How WP Navigator Works 1. **Your WordPress site** – Where your real content and pages live. 2. **This project folder** – Stores: - \`snapshots/\` – What your site looks like now (read from WordPress) - \`wpnavigator.jsonc\` – What you WANT your site to look like (your intent) 3. **Your AI assistant** (Claude, Codex, etc.) – Reads these files, helps you plan changes, and WP Navigator applies them safely. The AI never talks directly to your live site – it only edits files in this folder. You stay in control. ## Quick Start 1. Open this folder in Claude Code or Codex Cloud 2. Show it: - \`wpnavigator.jsonc\` - \`snapshots/site_index.json\` - \`snapshots/pages/home.json\` 3. Ask: "Help me review my WP Navigator setup" 4. When ready, run: \`npx wpnav sync\` ## Commands \`\`\`bash npx wpnav status # Check WordPress connection npx wpnav snapshot site # Update site snapshot npx wpnav diff # Compare manifest vs WordPress npx wpnav sync --dry-run # Preview changes before applying npx wpnav sync # Apply changes to WordPress \`\`\` ## Links - Demo: https://wpnav.ai/start/demo - Help: https://wpnav.ai/help - Docs: https://wpnav.ai/docs `; } /** * Execute repair based on user selection */ export async function executeRepair( cwd: string, state: RepairState, options: { all?: boolean; files?: string[] } ): Promise<RepairResult> { const result: RepairResult = { success: true, filesRepaired: [], filesSkipped: [], errors: [], }; // Determine which files to repair const filesToRepair: string[] = options.all ? state.files .filter((f) => (f.status === 'missing' || f.status === 'invalid') && f.canRegenerate) .map((f) => f.name) : options.files || []; for (const fileName of filesToRepair) { const fileInfo = state.files.find((f) => f.name === fileName); if (!fileInfo) { result.filesSkipped.push(fileName); continue; } if (!fileInfo.canRegenerate) { result.filesSkipped.push(fileName); continue; } const success = regenerateFile(cwd, fileName); if (success) { result.filesRepaired.push(fileName); } else { result.errors.push(`Failed to regenerate ${fileName}`); result.success = false; } } return result; } // ============================================================================= // Main Repair Handler // ============================================================================= /** * Handle repair mode for wpnav init * @returns Exit code: 0 for success, 1 for errors */ export async function handleRepairMode(cwd: string): Promise<number> { newline(); info('Existing WP Navigator configuration detected.'); newline(); const spinner = createSpinner({ text: 'Checking configuration...' }); const state = await buildRepairState(cwd); spinner.stop(); displayRepairState(state); if (!state.needsRepair) { success('Configuration is complete and valid!'); newline(); info('Your WP Navigator setup is ready to use.'); newline(); list([ 'Run "npx wpnav status" to check connection', 'Run "npx wpnav snapshot site" to update snapshots', 'Run "npx wpnav diff" to compare manifest vs WordPress', ]); return 0; } // Ask user what to do const repairableFiles = state.files .filter((f) => (f.status === 'missing' || f.status === 'invalid') && f.canRegenerate) .map((f) => f.name); if (repairableFiles.length === 0) { warning('No files can be automatically repaired.'); if (!state.credentials.valid) { newline(); info('To fix credentials, run: npx wpnav configure'); } return 0; } const action = await selectPrompt({ message: 'How would you like to proceed?', choices: [ { label: `Regenerate all missing/broken files (${repairableFiles.length})`, value: 'all', recommended: true, }, { label: 'Choose which files to regenerate', value: 'select', }, { label: 'Skip repair (exit)', value: 'skip', }, ], }); if (action === 'skip') { info('Repair skipped. Run "npx wpnav init --repair" anytime to try again.'); return 0; } let filesToRepair: string[] = []; if (action === 'all') { filesToRepair = repairableFiles; } else { // Let user select specific files for (const file of repairableFiles) { const repair = await confirmPrompt({ message: `Regenerate ${file}?`, defaultValue: true, }); if (repair) { filesToRepair.push(file); } } } if (filesToRepair.length === 0) { info('No files selected for repair.'); return 0; } // Execute repair newline(); const repairSpinner = createSpinner({ text: 'Repairing configuration...' }); const repairResult = await executeRepair(cwd, state, { files: filesToRepair }); repairSpinner.stop(); // Display results if (repairResult.filesRepaired.length > 0) { newline(); success('Repaired:'); for (const file of repairResult.filesRepaired) { console.error(` ${colorize(symbols.success, 'green')} ${file}`); } } if (repairResult.errors.length > 0) { newline(); errorMessage('Errors:'); for (const err of repairResult.errors) { console.error(` ${colorize(symbols.error, 'red')} ${err}`); } } newline(); if (repairResult.success) { success('Configuration repaired!'); } else { warning('Some files could not be repaired.'); } // Remind about credentials if needed if (!state.credentials.valid) { newline(); info('Note: Credentials need attention. Run "npx wpnav configure" to set up.'); } return repairResult.success ? 0 : 1; } /** * Check if repair mode should be offered (for auto-detection) */ export function shouldOfferRepair(cwd: string): boolean { return detectExistingConfig(cwd); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server