UNPKG

wordlift-cli

Version:

WordLift CLI - Your AI SEO Assistant powered by Google Gemini. Agentic SEO workflows with Agent Skills support, WordLift MCP integration, knowledge graphs, and intelligent content optimization for modern content creators.

234 lines (204 loc) 7.53 kB
#!/usr/bin/env node /** * WordLift CLI - A customized Gemini CLI for WordLift SEO workflows * @license Apache-2.0 */ const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); function resolveGeminiBinary(rootDir) { const candidates = [ path.join(rootDir, 'node_modules', '@google', 'gemini-cli', 'bundle', 'gemini.js'), path.join(rootDir, 'node_modules', '@google', 'gemini-cli', 'dist', 'index.js') ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } // Fallback path for error messaging when dependency is missing. return candidates[0]; } function getRuntimePaths(cwd = process.cwd(), rootDir = path.dirname(__dirname)) { return { userWorkingDir: cwd, packageDir: rootDir, userGeminiDir: path.join(cwd, '.gemini'), geminiBinary: resolveGeminiBinary(rootDir) }; } /** * Helper to load .env manually if process.env.GEMINI_API_KEY is missing. * This handles the strict validation in gemini-cli nightly. */ function loadApiKeyFromEnv(options = {}) { const { userWorkingDir = process.cwd(), env = process.env, fsModule = fs, homeDir = os.homedir() } = options; if (env.GEMINI_API_KEY) return; const envFiles = [ path.join(userWorkingDir, '.env'), path.join(userWorkingDir, '.gemini', '.env'), path.join(homeDir, '.gemini', '.env') ]; for (const envPath of envFiles) { if (fsModule.existsSync(envPath)) { try { const envContent = fsModule.readFileSync(envPath, 'utf-8'); const match = envContent.match(/^GEMINI_API_KEY=(.*)$/m); if (match && match[1]) { env.GEMINI_API_KEY = match[1].trim(); return; } } catch (e) { // Continue to next path } } } } /** * Ensures the project has the necessary WordLift context and settings. */ function ensureWordLiftContext(options = {}) { const { userWorkingDir = process.cwd(), packageDir = path.dirname(__dirname), userGeminiDir = path.join(userWorkingDir, '.gemini'), fsModule = fs, env = process.env, log = console.log } = options; const packageWordLiftFile = path.join(packageDir, 'WORDLIFT.md'); const userWordLiftFile = path.join(userWorkingDir, 'WORDLIFT.md'); const userSettingsFile = path.join(userGeminiDir, 'settings.json'); loadApiKeyFromEnv({ userWorkingDir, env, fsModule }); // 1. Deploy WORDLIFT.md context file if (fsModule.existsSync(packageWordLiftFile) && !fsModule.existsSync(userWordLiftFile)) { try { fsModule.copyFileSync(packageWordLiftFile, userWordLiftFile); log('\x1b[2m📄 WordLift SEO context added to project\x1b[0m'); } catch (e) {} } // 2. Ensure .gemini directory exists if (!fsModule.existsSync(userGeminiDir)) { try { fsModule.mkdirSync(userGeminiDir, { recursive: true }); } catch (e) {} } // 3. Ensure Skills infrastructure const userSkillsDir = path.join(userGeminiDir, 'skills'); if (!fsModule.existsSync(userSkillsDir)) { try { fsModule.mkdirSync(userSkillsDir, { recursive: true }); } catch (e) {} } // 4. Configure settings.json const defaultSettings = { ui: { hideBanner: true }, experimental: { skills: true }, mcpServers: { wordlift: { command: "npx", args: [ "mcp-remote@latest", "https://mcp.wordlift.io/sse" ], timeout: 180000 } }, tools: { allowed: ["wordlift"], autoAccept: true }, skillsDir: "./.gemini/skills", extensions: { allowRemoteInstall: true }, contextFileName: "WORDLIFT.md" }; try { let settings = defaultSettings; if (fsModule.existsSync(userSettingsFile)) { const existingStr = fsModule.readFileSync(userSettingsFile, 'utf8'); if (existingStr.trim()) { const existing = JSON.parse(existingStr); settings = { ...existing, ...defaultSettings }; // Deep merge for nested objects if they exist settings.ui = { ...(existing.ui || {}), ...defaultSettings.ui }; settings.experimental = { ...(existing.experimental || {}), ...defaultSettings.experimental }; settings.extensions = { ...(existing.extensions || {}), ...defaultSettings.extensions }; settings.mcpServers = { ...(existing.mcpServers || {}), ...defaultSettings.mcpServers }; settings.tools = { ...(existing.tools || {}), ...defaultSettings.tools }; } } fsModule.writeFileSync(userSettingsFile, JSON.stringify(settings, null, 2)); } catch (e) { // If JSON parsing fails or other error, just overwrite with defaults try { fsModule.writeFileSync(userSettingsFile, JSON.stringify(defaultSettings, null, 2)); } catch (err) {} } } /** * Prints the WordLift branding header. */ function showBranding() { console.log(` \x1b[36m██ ██ ██████ ██████ ██████ ██ ██ ███████ ████████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██ ██████ ██ ██ ██ ██ █████ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███ ██████ ██ ██ ██████ ███████ ██ ██ ██\x1b[0m 🚀 \x1b[1mWordLift CLI - Universal SEO Assistant\x1b[0m ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); } // Main execution flow function main() { const { userWorkingDir, geminiBinary, packageDir, userGeminiDir } = getRuntimePaths(); const args = process.argv.slice(2); const isHelp = args.includes('--help') || args.includes('-h'); const isVersion = args.includes('--version') || args.includes('-v'); const isInteractive = args.length === 0; // Setup context for everything except version checks if (!isVersion) { ensureWordLiftContext({ userWorkingDir, geminiBinary, packageDir, userGeminiDir }); } // Show branding for interactive mode or help if (isInteractive || isHelp) { showBranding(); if (isInteractive) { console.log('\x1b[2m🤖 Agent WordLift - SEO Specialist Mode Activated\x1b[0m'); console.log(`\x1b[2m📁 Project: ${userWorkingDir}\x1b[0m\n`); } } // Prepare child process arguments const childArgs = [geminiBinary, ...args]; const childEnv = { ...process.env, WORDLIFT_CLI: 'true', GEMINI_CONFIG_DIR: userGeminiDir }; const child = spawn('node', childArgs, { stdio: 'inherit', cwd: userWorkingDir, env: childEnv }); child.on('error', (err) => { console.error(`\x1b[31m❌ Error starting WordLift CLI: ${err.message}\x1b[0m`); process.exit(1); }); child.on('close', (code) => { if (code === 41) { console.log('\n\x1b[33m💡 Quick Fix: Set your API Key to get started:\x1b[0m'); console.log(' export GEMINI_API_KEY=your_key_here'); console.log(' (or add it to a .env file in this directory)\n'); } process.exit(code); }); } if (require.main === module) { main(); } module.exports = { getRuntimePaths, loadApiKeyFromEnv, ensureWordLiftContext, showBranding, main };