diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 01c79d95..4014578f 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -5,7 +5,7 @@ on: types: [opened, edited, synchronize, reopened] jobs: - lint: + pr-title-check: runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v5 diff --git a/misc/config.yml b/misc/config.yml index 3f50d224..f3a417bc 100644 --- a/misc/config.yml +++ b/misc/config.yml @@ -15,12 +15,12 @@ env: # Explicitly set the number of columns # or use `auto` to take the current # number of columns of your shell -cols: auto +cols: 135 # Explicitly set the number of rows # or use `auto` to take the current # number of rows of your shell -rows: auto +rows: 34 # Amount of times to repeat GIF # If value is -1, play once diff --git a/misc/rli-demo-search.gif b/misc/rli-demo-search.gif new file mode 100644 index 00000000..1146854d Binary files /dev/null and b/misc/rli-demo-search.gif differ diff --git a/misc/rli-ssh-demo.gif b/misc/rli-ssh-demo.gif new file mode 100644 index 00000000..d0aed613 Binary files /dev/null and b/misc/rli-ssh-demo.gif differ diff --git a/scripts/generate-command-docs.js b/scripts/generate-command-docs.js index edb8f536..e0e114ba 100644 --- a/scripts/generate-command-docs.js +++ b/scripts/generate-command-docs.js @@ -2,7 +2,7 @@ import { fileURLToPath } from "url"; import { dirname, join } from "path"; -import { readFileSync, writeFileSync } from "fs"; +import { readFileSync, writeFileSync, existsSync } from "fs"; import { createProgram } from "../dist/utils/commands.js"; const __filename = fileURLToPath(import.meta.url); @@ -10,6 +10,9 @@ const __dirname = dirname(__filename); const rootDir = join(__dirname, ".."); const readmePath = join(rootDir, "README.md"); +// Default docs path - can be overridden via DOCS_PATH env var or --docs-path argument +const defaultDocsPath = join(rootDir, "..", "docs", "docs", "tools", "cli.mdx"); + /** * Generates markdown documentation for the command structure from Commander * in the format: code blocks with grouped examples @@ -116,6 +119,139 @@ function generateCommandStructure(program) { return lines.join("\n"); } +/** + * Generates a detailed command reference for external docs (cli.mdx) + * Uses Mintlify components for better presentation + */ +function generateDetailedCommandDocs(program) { + const sections = []; + + // Get all top-level commands (excluding hidden ones) + const commands = program.commands.filter((cmd) => !cmd._hidden); + + for (const command of commands) { + const commandName = command.name(); + const commandAlias = command.aliases()[0] || null; + const sectionTitleBase = commandName.charAt(0).toUpperCase() + commandName.slice(1); + const aliasNote = commandAlias ? ` (alias: \`${commandAlias}\`)` : ""; + + const lines = []; + lines.push(`### ${sectionTitleBase} Commands${aliasNote}`); + lines.push(""); + lines.push(""); + + // Get subcommands + const subcommands = command.commands.filter((cmd) => !cmd._hidden); + + for (const subcmd of subcommands) { + // Build command signature + const args = subcmd._args + .map((arg) => { + const isVariadic = arg.variadic || false; + if (arg.required) { + return `<${arg.name()}${isVariadic ? "..." : ""}>`; + } + return `[${arg.name()}${isVariadic ? "..." : ""}]`; + }) + .join(" "); + + const cmdName = args ? `${subcmd.name()} ${args}` : subcmd.name(); + const fullCmd = `rli ${commandName} ${cmdName}`; + + // Get icon based on command type + const icon = getCommandIcon(subcmd.name()); + + lines.push(` `); + lines.push(` ${subcmd.description()}`); + lines.push(""); + lines.push(` \`\`\`bash`); + lines.push(` ${fullCmd}`); + lines.push(` \`\`\``); + + // Add options/parameters if any (excluding common output option) + const options = subcmd.options.filter(opt => !opt.flags.includes("--output")); + if (options.length > 0) { + lines.push(""); + for (const opt of options) { + const { name, type, isRequired } = parseOption(opt); + const defaultAttr = opt.defaultValue !== undefined ? ` default="${opt.defaultValue}"` : ""; + const requiredAttr = isRequired || opt.mandatory ? " required" : ""; + lines.push(` `); + lines.push(` ${opt.description}`); + lines.push(` `); + } + } + + lines.push(" "); + } + + lines.push(""); + lines.push(""); + + sections.push(lines.join("\n")); + } + + return sections.join("\n"); +} + +/** + * Parse option flags to extract name and type + */ +function parseOption(opt) { + const flags = opt.flags; + // Extract the long flag name (e.g., "--name " -> "name") + const longMatch = flags.match(/--([a-z-]+)/i); + const name = longMatch ? longMatch[1] : flags; + + // Determine type from the flag pattern + let type = "boolean"; + if (flags.includes("<") && flags.includes(">")) { + // Has a value placeholder + if (flags.includes("...>")) { + type = "string[]"; + } else { + type = "string"; + } + } + + const isRequired = opt.mandatory || false; + + return { name, type, isRequired }; +} + +/** + * Get an appropriate icon for a command based on its name + */ +function getCommandIcon(cmdName) { + const iconMap = { + create: "plus", + list: "list", + get: "eye", + delete: "trash", + exec: "terminal", + "exec-async": "terminal", + "get-async": "clock", + "send-stdin": "keyboard", + upload: "upload", + download: "download", + read: "file", + write: "pencil", + suspend: "pause", + resume: "play", + shutdown: "power-off", + ssh: "terminal", + scp: "copy", + rsync: "rotate", + tunnel: "network-wired", + logs: "scroll", + status: "info", + start: "play", + install: "download", + }; + + return iconMap[cmdName] || "code"; +} + /** * Updates the README.md file with the generated command structure */ @@ -149,11 +285,288 @@ function updateReadme(newCommandStructure) { console.log("✅ Updated README.md with generated command structure"); } +/** + * Generates the full cli.mdx content + */ +function generateCliMdx(program) { + const commandDocs = generateDetailedCommandDocs(program); + + return `--- +title: 'Runloop CLI' +description: 'Explore, experiment with, and test the Runloop API using the Runloop CLI.' +icon: 'terminal' +--- + +The Runloop CLI (\`rli\`) provides both an interactive terminal UI and traditional CLI commands for managing your Runloop resources. + + + + @runloop/rl-cli + + + runloopai/rl-cli + + + +## Installation + + + +\`\`\`bash npm +npm install -g @runloop/rl-cli +\`\`\` + +\`\`\`bash yarn +yarn global add @runloop/rl-cli +\`\`\` + +\`\`\`bash pnpm +pnpm add -g @runloop/rl-cli +\`\`\` + + + +## Setup + +Configure your API key: + +\`\`\`bash +export RUNLOOP_API_KEY=your_api_key_here +\`\`\` + + + Get your API key from [runloop.ai/settings](https://runloop.ai/settings) + + +## Quick Start + + + + Launch the interactive UI with a beautiful terminal interface: + + \`\`\`bash + rli + \`\`\` + + Navigate with arrow keys, select with Enter, and manage all your resources visually. + + + Runloop CLI Interactive Mode + + + ### SSH to Devbox + + From the interactive menu, select a devbox and choose **SSH** to open a secure shell session directly into your devbox. The CLI handles all the SSH key setup and connection details automatically. + + + SSH into a Devbox + + + + Use traditional commands for scripting and automation: + + \`\`\`bash + # List all devboxes + rli devbox list --output json + \`\`\` + + \`\`\`json Example Output + [ + { + "id": "dbx_1234567890", + "name": "my-devbox", + "status": "running", + "blueprint_id": "bpt_abcdef" + } + ] + \`\`\` + + \`\`\`bash + # Create a new devbox + rli devbox create --name my-devbox --blueprint my-blueprint + \`\`\` + + \`\`\`text Example Output + dbx_1234567890 + \`\`\` + + \`\`\`bash + # Execute a command in a devbox + rli devbox exec dbx_1234567890 echo "Hello World" + \`\`\` + + \`\`\`text Example Output + Hello World + \`\`\` + + \`\`\`bash + # SSH into a devbox + rli devbox ssh dbx_1234567890 + \`\`\` + + + +## Command Groups + + + + Create, manage, and interact with devboxes + + + Create and manage devbox snapshots + + + Manage reusable devbox templates + + + Upload and manage file objects + + + +## Command Reference + +${commandDocs} + +## MCP Server (AI Integration) + + + The CLI includes a Model Context Protocol (MCP) server that allows AI assistants like Claude to interact with your devboxes. + + +### Quick Setup for Claude Desktop + +\`\`\`bash +# Install MCP configuration +rli mcp install +\`\`\` + +After installation, restart Claude Desktop and ask Claude to "List my devboxes" or "Create a new devbox". + +### Server Modes + + + + \`\`\`bash + rli mcp start + \`\`\` + + Standard input/output mode for Claude Desktop integration. + + + \`\`\`bash + rli mcp start --http + rli mcp start --http --port 8080 + \`\`\` + + HTTP/SSE mode for web applications and remote access. + + + +## Output Formats + +All commands support multiple output formats via the \`--output\` flag: + + + JSON output for programmatic parsing + + + + YAML output for configuration files + + + + Plain text output for human readability + + +\`\`\`bash +rli devbox list --output json +rli devbox list --output yaml +rli devbox list --output text +\`\`\` + +## Contributing + +The Runloop CLI is open-source. We welcome contributions! + + + + View source code and submit PRs + + + Report bugs or request features + + +`; +} + +/** + * Updates the external docs cli.mdx file + */ +function updateDocsMdx(program, docsPath) { + if (!existsSync(docsPath)) { + console.log(`⚠️ Docs file not found at ${docsPath}, skipping docs update`); + console.log(" Set DOCS_PATH env var or use --docs-path to specify location"); + return false; + } + + const newContent = generateCliMdx(program); + writeFileSync(docsPath, newContent, "utf-8"); + console.log(`✅ Updated ${docsPath} with generated CLI documentation`); + return true; +} + +/** + * Parse command line arguments + */ +function parseArgs() { + const args = process.argv.slice(2); + const options = { + docsPath: process.env.DOCS_PATH || defaultDocsPath, + skipReadme: false, + skipDocs: false, + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--docs-path" && args[i + 1]) { + options.docsPath = args[i + 1]; + i++; + } else if (args[i] === "--skip-readme") { + options.skipReadme = true; + } else if (args[i] === "--skip-docs") { + options.skipDocs = true; + } else if (args[i] === "--help" || args[i] === "-h") { + console.log(` +Usage: generate-command-docs.js [options] + +Options: + --docs-path Path to cli.mdx file (default: ../docs/docs/tools/cli.mdx) + --skip-readme Skip updating README.md + --skip-docs Skip updating cli.mdx + --help, -h Show this help message + +Environment Variables: + DOCS_PATH Alternative way to specify docs path +`); + process.exit(0); + } + } + + return options; +} + async function main() { try { + const options = parseArgs(); const program = createProgram(); - const markdown = generateCommandStructure(program); - updateReadme(markdown); + + if (!options.skipReadme) { + const markdown = generateCommandStructure(program); + updateReadme(markdown); + } + + if (!options.skipDocs) { + updateDocsMdx(program, options.docsPath); + } } catch (error) { console.error("Error generating command docs:", error); process.exit(1);