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.
+
+
+
+
+
+ ### 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.
+
+
+
+
+
+
+ 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);