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
JavaScript
#!/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
};