docs-v2/scripts/docs-cli/docs-edit.js

279 lines
7.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Documentation file opener
* Opens existing documentation pages in your default editor
*
* Usage:
* docs edit <url> # Non-blocking (default)
* docs edit <url> --wait # Wait for editor to close
* docs edit <url> --list # List files without opening
* docs edit <url> --editor vim # Use specific editor
*/
import { parseArgs } from 'node:util';
import process from 'node:process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import { parseDocumentationURL, urlToFilePaths } from '../lib/url-parser.js';
import { getSourceFromFrontmatter } from '../lib/content-utils.js';
import { resolveEditor } from './lib/editor-resolver.js';
import { spawnEditor, shouldWait } from './lib/process-manager.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Repository root
const REPO_ROOT = join(__dirname, '..', '..');
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m',
};
/**
* Print colored output
*/
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
/**
* Parse command line arguments
*/
function parseArguments() {
const { values, positionals } = parseArgs({
options: {
help: { type: 'boolean', default: false },
list: { type: 'boolean', default: false },
wait: { type: 'boolean', default: false },
editor: { type: 'string' },
},
allowPositionals: true,
});
// First positional argument is the URL
if (positionals.length > 0 && !values.url) {
values.url = positionals[0];
}
return values;
}
/**
* Print usage information
*/
function printUsage() {
console.log(`
${colors.bright}Documentation File Opener${colors.reset}
${colors.bright}Usage:${colors.reset}
docs edit <url> Open in editor (non-blocking)
docs edit <url> --wait Wait for editor to close
docs edit <url> --list List files without opening
docs edit <url> --editor <cmd> Use specific editor
${colors.bright}Arguments:${colors.reset}
<url> Documentation URL or path
${colors.bright}Options:${colors.reset}
--list List matching files without opening
--wait Block until editor closes (for interactive use)
--editor <cmd> Override default editor
--help Show this help message
${colors.bright}Examples:${colors.reset}
# Quick edit (CLI exits immediately, editor runs in background)
docs edit https://docs.influxdata.com/influxdb3/core/admin/databases/
docs edit /influxdb3/core/admin/databases/
# Interactive edit (CLI waits for you to close editor)
docs edit /influxdb3/core/admin/databases/ --wait
# Use specific editor
docs edit /influxdb3/core/admin/databases/ --editor nano
# List files only (useful for scripting)
docs edit /influxdb3/core/admin/databases/ --list
${colors.bright}Editor Configuration:${colors.reset}
The editor is selected in this order:
1. --editor flag
2. DOCS_EDITOR environment variable
3. VISUAL environment variable
4. EDITOR environment variable
5. System default (vim, nano, etc.)
Examples:
export EDITOR=vim
export EDITOR=nano
export DOCS_EDITOR="code --wait"
${colors.bright}Notes:${colors.reset}
- Default behavior is non-blocking (agent-friendly)
- Use --wait flag for interactive editing sessions
- Multiple files may open if content is shared across products
`);
}
/**
* Find matching files for a URL
*/
function findFiles(url) {
try {
// Parse URL
const parsed = parseDocumentationURL(url);
log(`\n🔍 Analyzing URL: ${url}`, 'bright');
log(` Product: ${parsed.namespace}/${parsed.product || 'N/A'}`, 'cyan');
log(` Section: ${parsed.section || 'N/A'}`, 'cyan');
// Get potential file paths
const potentialPaths = urlToFilePaths(parsed);
const foundFiles = [];
for (const relativePath of potentialPaths) {
const fullPath = join(REPO_ROOT, relativePath);
if (existsSync(fullPath)) {
foundFiles.push(relativePath);
}
}
return { parsed, foundFiles };
} catch (error) {
log(`\n✗ Error parsing URL: ${error.message}`, 'red');
process.exit(1);
}
}
/**
* Check if file uses shared content
* @param {string} filePath - Relative path from repo root
* @returns {string|null} Path to shared source file or null
*/
function checkSharedContent(filePath) {
const fullPath = join(REPO_ROOT, filePath);
return getSourceFromFrontmatter(fullPath);
}
/**
* Open files in editor
*/
function openInEditor(files, options = {}) {
try {
// Resolve editor command
const editor = resolveEditor({ editor: options.editor });
const wait = shouldWait(options.wait);
log(`\n📝 Opening ${files.length} file(s) in ${editor}...`, 'bright');
if (!wait) {
log(' Editor will open in background (CLI exits immediately)', 'cyan');
log(' Use --wait flag to block until editor closes', 'cyan');
}
// Convert to absolute paths
const absolutePaths = files.map((f) => join(REPO_ROOT, f));
// Spawn editor
spawnEditor(editor, absolutePaths, {
wait,
onError: (error) => {
log(`\n✗ Failed to open editor: ${error.message}`, 'red');
log('\nTroubleshooting:', 'yellow');
log(' 1. Set EDITOR environment variable:', 'cyan');
log(' export EDITOR=vim', 'cyan');
log(' 2. Or use --editor flag:', 'cyan');
log(' docs edit <url> --editor nano', 'cyan');
process.exit(1);
},
});
// In non-blocking mode, give process time to spawn before exit
if (!wait) {
setTimeout(() => {
log('✓ Editor launched\n', 'green');
process.exit(0);
}, 100);
}
} catch (error) {
log(`\n${error.message}`, 'red');
process.exit(1);
}
}
/**
* Main entry point
*/
async function main() {
const options = parseArguments();
// Show help
if (options.help || !options.url) {
printUsage();
process.exit(0);
}
// Find files
const { foundFiles } = findFiles(options.url);
if (foundFiles.length === 0) {
log('\n✗ No files found for this URL', 'red');
log('\nThe page may not exist yet. To create new content, use:', 'yellow');
log(' docs create <draft-path> --products <product>', 'cyan');
process.exit(1);
}
// Display found files
log('\n✓ Found files:', 'green');
const allFiles = new Set();
for (const file of foundFiles) {
allFiles.add(file);
log(`${file}`, 'cyan');
// Check for shared content
const sharedSource = checkSharedContent(file);
if (sharedSource) {
if (existsSync(join(REPO_ROOT, sharedSource))) {
allFiles.add(sharedSource);
log(
`${sharedSource} ${colors.yellow}(shared source)${colors.reset}`,
'cyan'
);
}
}
}
const filesToOpen = Array.from(allFiles);
// List only mode
if (options.list) {
log(`\n✓ Found ${filesToOpen.length} file(s)`, 'green');
process.exit(0);
}
// Open in editor
openInEditor(filesToOpen, {
wait: options.wait,
editor: options.editor,
});
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
log(`\nFatal error: ${error.message}`, 'red');
console.error(error.stack);
process.exit(1);
});
}
export { findFiles, openInEditor };