diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b3a135d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Description + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Run command '...' +2. See error + +## Expected Behavior + +A clear and concise description of what you expected to happen. + +## Actual Behavior + +A clear and concise description of what actually happened. + +## Environment + +- OS: [e.g., macOS 14.0, Ubuntu 22.04, Windows 11] +- Node.js version: [e.g., 20.10.0] +- CLI version: [e.g., 0.7.0] +- Terminal: [e.g., iTerm2, Terminal.app, Windows Terminal] + +## Additional Context + +Add any other context about the problem here. + +## Screenshots/Logs + +If applicable, add screenshots or logs to help explain your problem. + +```bash +# Paste relevant logs here +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..7a1d7644 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Proposed Solution + +A clear and concise description of what you want to happen. + +## Alternatives Considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Use Case + +Describe the use case or scenario where this feature would be helpful. + +## Additional Context + +Add any other context, mockups, or examples about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..bb660121 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,53 @@ +## Description + + + +**Note:** PR titles should follow [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat(devbox): add support for custom env vars` or `fix(snapshot): resolve pagination issue`) as they are used for automatic release notes generation. + +## Type of Change + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Code refactoring +- [ ] Performance improvement +- [ ] Test updates + +## Related Issues + + +Closes # + +## Changes Made + + + +## Testing + + + +- [ ] I have tested locally +- [ ] I have added/updated tests +- [ ] All existing tests pass + +## Checklist + +- [ ] My code follows the code style of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have updated the documentation accordingly +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published + +## Screenshots (if applicable) + + + +## Additional Notes + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4df5579a..3f488177 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: CI on: pull_request: - types: [opened, synchronize, reopened] workflow_dispatch: jobs: @@ -10,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: runloopai/checkout@main + uses: actions/checkout@v4 - name: Setup Node.js - uses: runloopai/setup-node@main + uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" @@ -28,10 +27,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: runloopai/checkout@main + uses: actions/checkout@v4 - name: Setup Node.js - uses: runloopai/setup-node@main + uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" @@ -46,10 +45,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: runloopai/checkout@main + uses: actions/checkout@v4 - name: Setup Node.js - uses: runloopai/setup-node@main + uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" @@ -65,10 +64,10 @@ jobs: needs: build steps: - name: Checkout code - uses: runloopai/checkout@main + uses: actions/checkout@v4 - name: Setup Node.js - uses: runloopai/setup-node@main + uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e3a0f8b..185f153e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,4 +46,4 @@ jobs: run: npm run build - name: Publish to npm - run: NPM_CONFIG_PROVENANCE=false npm publish # remove prominance disable when we open source this package + run: npm publish diff --git a/.gitignore b/.gitignore index 927bf959..6d26bdee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules/ +.vscode dist/ *.log .DS_Store .env coverage -*.mcpb \ No newline at end of file +*.mcpb diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..6c6b0bc0 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,49 @@ +# Pre-push hook to ensure code quality and docs are up to date +# This hook runs format:check, lint, and docs:commands + +echo "Running format:check..." +if ! npm run format:check; then + echo "" + echo "❌ Code formatting check failed" + echo " Please run 'npm run format' to fix formatting issues" + echo "" + exit 1 +fi + +echo "Running lint..." +if ! npm run lint; then + echo "" + echo "❌ Linting failed" + echo " Please run 'npm run lint:fix' to fix linting issues" + echo "" + exit 1 +fi + +echo "Running docs:commands to ensure README.md is up to date..." + +# Run the docs generation script +npm run docs:commands + +# Check if README.md has uncommitted changes +if ! git diff --quiet README.md; then + echo "" + echo "❌ README.md has uncommitted changes after running docs:commands" + echo " Please commit the updated README.md before pushing:" + echo "" + echo " git add README.md" + echo " git commit -m 'docs: update command structure'" + echo "" + exit 1 +fi + +# Check if README.md has staged but uncommitted changes +if ! git diff --cached --quiet README.md 2>/dev/null; then + echo "" + echo "⚠️ README.md has staged changes that need to be committed" + echo " Please commit the staged README.md before pushing" + echo "" + exit 1 +fi + +echo "✅ README.md is up to date" +exit 0 diff --git a/CLAUDE_SETUP.md b/CLAUDE_SETUP.md index ebda34e6..47117f8f 100644 --- a/CLAUDE_SETUP.md +++ b/CLAUDE_SETUP.md @@ -197,4 +197,4 @@ Claude will automatically use these tools when you ask questions about your Runl --- -**Need help?** Open an issue at https://github.com/runloop/rl-cli-node/issues +**Need help?** Open an issue at https://github.com/runloopai/rl-cli/issues diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..b40c7e7b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**opensource@runloop.ai**. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cf59c443 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,176 @@ +# Contributing to Runloop CLI + +Thank you for your interest in contributing to the Runloop CLI! This document provides guidelines and instructions for contributing. + +## Getting Started + +### Prerequisites + +- Node.js >= 18.0.0 +- npm + +### Development Setup + +1. Fork and clone the repository: + +```bash +git clone https://github.com/runloopai/rl-cli.git +cd rl-cli +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Build the project: + +```bash +npm run build +``` + +4. Link for local development: + +```bash +npm link +``` + +Now you can use `rli` commands locally with your changes. + +### Development Workflow + +```bash +# Watch mode - rebuilds on file changes +npm run dev + +# Run the CLI +npm start -- + +# Or after linking +rli +``` + +## Code Style + +This project uses Prettier and ESLint to maintain code quality. + +### Formatting + +```bash +# Check formatting +npm run format:check + +# Auto-fix formatting +npm run format +``` + +### Linting + +```bash +# Run linter +npm run lint + +# Auto-fix lint issues +npm run lint:fix +``` + +## Testing + +```bash +# Run all tests +npm test + +# Run component tests with coverage +npm run test:components + +# Watch mode +npm run test:watch +``` + +Please ensure all tests pass before submitting a pull request. + +## Commit Messages + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for commit messages. This enables automatic changelog generation and semantic versioning. + +### Format + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +### Types + +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code changes that neither fix bugs nor add features +- `test`: Adding or updating tests +- `chore`: Maintenance tasks + +### Examples + +``` +feat(devbox): add support for custom environment variables +fix(snapshot): resolve pagination issue in list command +docs: update installation instructions +``` + +## Pull Request Process + +1. Create a new branch from `main`: + +```bash +git checkout -b feat/my-feature +``` + +2. Make your changes and commit using conventional commits. + +3. Push to your fork and open a pull request. + +4. Ensure CI checks pass: + - Formatting (Prettier) + - Linting (ESLint) + - Build (TypeScript) + - Tests + +5. Request review from maintainers. + +### PR Title + +PR titles should follow the conventional commit format as they are used for release notes. + +## Project Structure + +``` +src/ +├── cli.ts # Main CLI entry point +├── commands/ # Command implementations +│ ├── devbox/ # Devbox commands +│ ├── snapshot/ # Snapshot commands +│ ├── blueprint/ # Blueprint commands +│ └── object/ # Object storage commands +├── components/ # React/Ink UI components +├── hooks/ # Custom React hooks +├── mcp/ # MCP server implementation +├── router/ # Navigation router +├── screens/ # Full-screen views +├── services/ # API service wrappers +├── store/ # Zustand state management +└── utils/ # Utility functions +``` + +## Questions? + +If you have questions, feel free to: + +- Open an issue for discussion +- Check existing issues and pull requests + +Thank you for contributing! diff --git a/README.md b/README.md index 8772141f..f5d2c47f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,29 @@ # Runloop CLI -A beautiful, interactive CLI for managing Runloop devboxes built with Ink and TypeScript. +[![npm version](https://img.shields.io/npm/v/@runloop/rl-cli)](https://www.npmjs.com/package/@runloop/rl-cli) +[![CI](https://github.com/runloopai/rl-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/runloopai/rl-cli/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +A beautiful CLI for managing Runloop built with Ink and TypeScript. Use it as an **interactive command-line application** with rich UI components, or as a **traditional CLI** for scripting and automation. + +## Quick Example + +```bash +# Interactive mode - launches a beautiful UI menu +rli + +# Traditional CLI mode - perfect for scripts +rli devbox list # Outputs JSON/text +rli devbox create --name my-devbox +rli devbox exec echo "Hello World" +rli devbox delete +``` ## Features -- 🎨 Beautiful terminal UI with colors and gradients - ⚡ Fast and responsive with pagination - 📦 Manage devboxes, snapshots, and blueprints - 🚀 Execute commands in devboxes -- 📤 Upload files to devboxes - 🎯 Organized command structure with aliases - 🤖 **Model Context Protocol (MCP) server for AI integration** @@ -20,16 +35,6 @@ Install globally via npm: npm install -g @runloop/rl-cli ``` -Or install from source: - -```bash -git clone https://github.com/runloop/rl-cli-node.git -cd rl-cli-node -npm install -npm run build -npm link -``` - ## Setup Configure your API key: @@ -42,100 +47,78 @@ Get your API key from [https://runloop.ai/settings](https://runloop.ai/settings) ## Usage -### Theme Configuration - -The CLI supports both light and dark terminal themes. Set the theme via environment variable: +### Interactive CLI ```bash -export RUNLOOP_THEME=light # Force light mode (dark text on light background) -export RUNLOOP_THEME=dark # Force dark mode (light text on dark background) +rli # Run the interactive console +rli --help # See help information ``` -**How it works:** +## Command Structure -- **auto** (default): Detects correct theme by default -- **light**: Optimized for light-themed terminals (uses dark text colors) -- **dark**: Optimized for dark-themed terminals (uses light text colors) +The CLI is organized into command buckets: -### Devbox Commands +### Devbox Commands (alias: `d`) ```bash -# Create devboxes -rli devbox create # Create with auto-generated name -rli devbox create --name my-devbox # Create with custom name -rli devbox create --template nodejs # Create from template -rli d create -n my-devbox # Short alias - -# List devboxes (paginated) -rli devbox list # List all devboxes -rli devbox list --status running # Filter by status -rli d list # Short alias - -# Execute commands -rli devbox exec echo "Hello" # Run a command -rli devbox exec ls -la # List files -rli d exec # Short alias - -# Upload files -rli devbox upload ./file.txt # Upload to home -rli devbox upload ./file.txt -p /path # Upload to specific path -rli d upload # Short alias - -# Delete devboxes -rli devbox delete # Shutdown a devbox -rli devbox rm # Alias -rli d delete # Short alias +rli devbox create # Create a new devbox +rli devbox list # List all devboxes +rli devbox delete # Shutdown a devbox +rli devbox exec # Execute a command in a devbox +rli devbox exec-async # Execute a command asynchronously on a... +rli devbox upload # Upload a file to a devbox +rli devbox get # Get devbox details +rli devbox get-async # Get status of an async execution +rli devbox suspend # Suspend a devbox +rli devbox resume # Resume a suspended devbox +rli devbox shutdown # Shutdown a devbox +rli devbox ssh # SSH into a devbox +rli devbox scp # Copy files to/from a devbox using scp +rli devbox rsync # Sync files to/from a devbox using rsync +rli devbox tunnel # Create a port-forwarding tunnel to a ... +rli devbox read # Read a file from a devbox using the API +rli devbox write # Write a file to a devbox using the API +rli devbox download # Download a file from a devbox +rli devbox send-stdin # Send stdin to a running async execution +rli devbox logs # View devbox logs ``` -### Snapshot Commands +### Snapshot Commands (alias: `snap`) ```bash -# Create snapshots -rli snapshot create # Create snapshot -rli snapshot create --name backup-1 # Create with name -rli snap create # Short alias - -# List snapshots (paginated) -rli snapshot list # List all snapshots -rli snapshot list --devbox # Filter by devbox -rli snap list # Short alias - -# Delete snapshots -rli snapshot delete # Delete snapshot -rli snapshot rm # Alias -rli snap delete # Short alias +rli snapshot list # List all snapshots +rli snapshot create # Create a snapshot of a devbox +rli snapshot delete # Delete a snapshot +rli snapshot get # Get snapshot details +rli snapshot status # Get snapshot operation status ``` -### Blueprint Commands +### Blueprint Commands (alias: `bp`) ```bash -# List blueprints -rli blueprint list # List blueprints (coming soon) -rli bp list # Short alias +rli blueprint list # List all blueprints +rli blueprint create # Create a new blueprint +rli blueprint get # Get blueprint details by name or ID (... +rli blueprint logs # Get blueprint build logs by name or I... ``` -## Command Structure +### Object Commands (alias: `obj`) -The CLI is organized into command buckets: - -- **`devbox` (alias: `d`)** - Manage devboxes - - `create` - Create new devboxes - - `list` - List devboxes with pagination - - `exec` - Execute commands - - `upload` - Upload files - - `delete` (alias: `rm`) - Shutdown devboxes +```bash +rli object list # List objects +rli object get # Get object details +rli object download # Download object to local file +rli object upload # Upload a file as an object +rli object delete # Delete an object (irreversible) +``` -- **`snapshot` (alias: `snap`)** - Manage snapshots - - `create` - Create snapshots - - `list` - List snapshots with pagination - - `delete` (alias: `rm`) - Delete snapshots +### Mcp Commands -- **`blueprint` (alias: `bp`)** - Manage blueprints - - `list` - List blueprints (coming soon) +```bash +rli mcp start # Start the MCP server +rli mcp install # Install Runloop MCP server configurat... +``` -- **`mcp`** - Model Context Protocol server for AI integration - - `install` - Install MCP configuration in Claude Desktop - - `start` - Start the MCP server (stdio or HTTP mode) ## MCP Server (AI Integration) @@ -163,19 +146,14 @@ rli mcp start --http --port 8080 ``` **Documentation:** + - [CLAUDE_SETUP.md](./CLAUDE_SETUP.md) - Complete setup guide for Claude Desktop - [MCP_README.md](./MCP_README.md) - Full MCP documentation - [MCP_COMMANDS.md](./MCP_COMMANDS.md) - Quick command reference -## Interactive Features +## Theme Configuration -- **Pagination** - Lists show 10 items per page with keyboard navigation - - `n` - Next page - - `p` - Previous page - - `q` - Quit -- **Beautiful UI** - Gradient text, colored borders, Unicode icons -- **Real-time Status** - Spinners and progress indicators -- **Summary Stats** - Count running, stopped, and total resources +The CLI supports both light and dark terminal themes and will automatically select the appropriate theme. ## Development @@ -189,20 +167,9 @@ npm run build # Watch mode npm run dev -# Run CLI -npm start -- -``` - -## Publishing - -To publish a new version to npm: - -```bash -npm run build -npm publish -``` +## Contributing -**Note:** Make sure you're logged in to npm with access to the `@runloop` organization. +We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to contribute to this project. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ad30eb6e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,51 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities in the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 0.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +We take the security of our software seriously. If you believe you have found a security vulnerability, please report it to us as described below. + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please send an email to **security@runloop.ai** with: + +- A description of the vulnerability +- Steps to reproduce the issue +- Potential impact of the vulnerability +- Any possible mitigations you've identified + +You should receive a response within 48 hours. If for some reason you do not, please follow up via email to ensure we received your original message. + +Please include as much information as possible to help us understand and resolve the issue quickly: + +- Type of issue (e.g., command injection, credential exposure, etc.) +- Full paths of source file(s) related to the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit it + +## Preferred Languages + +We prefer all communications to be in English. + +## Disclosure Policy + +When we receive a security bug report, we will: + +1. Confirm the problem and determine affected versions +2. Audit code to find any similar problems +3. Prepare fixes for all supported versions +4. Release new versions and publish security advisories as needed + +## Comments on this Policy + +If you have suggestions on how this process could be improved, please submit a pull request. diff --git a/env.example b/env.example deleted file mode 100644 index 3b105dff..00000000 --- a/env.example +++ /dev/null @@ -1,30 +0,0 @@ -# Runloop CLI Environment Configuration -# Copy this file to .env and fill in your actual values - -# API Configuration -RUNLOOP_API_KEY=your_api_key_here -RUNLOOP_BASE_URL=https://api.runloop.pro -RUNLOOP_ENV=dev - -# UI Theme Configuration -# RUNLOOP_THEME=auto # Options: auto (default), light, dark -# RUNLOOP_DISABLE_THEME_DETECTION=1 # Set to 1 to disable auto-detection (avoids screen flashing in some terminals) - -# Test Configuration -RUN_E2E=false -NODE_ENV=test - -# SSH Configuration (for local testing) -SSH_KEY_PATH=~/.runloop/ssh_keys -SSH_TIMEOUT=180 -SSH_POLL_INTERVAL=3 - -# File Upload/Download Configuration -MAX_FILE_SIZE=100MB -TEMP_DIR=/tmp/rl-cli-tests - -# Logging Configuration -LOG_LEVEL=info -LOG_FORMAT=json - - diff --git a/eslint-plugins/require-component-tests.js b/eslint-plugins/require-component-tests.js deleted file mode 100644 index 701d5256..00000000 --- a/eslint-plugins/require-component-tests.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * ESLint plugin to enforce that all component files have corresponding test files. - * - * This plugin checks that for each .tsx file in src/components/, - * there exists a corresponding .test.tsx file in tests/__tests__/components/ - */ - -import { existsSync } from 'fs'; -import { basename, dirname, join } from 'path'; - -const rule = { - meta: { - type: 'problem', - docs: { - description: 'Require test files for all component files', - category: 'Best Practices', - recommended: true, - }, - schema: [], - messages: { - missingTest: 'Component "{{componentName}}" is missing a test file. Expected: {{expectedPath}}', - }, - }, - create(context) { - return { - Program(node) { - const filename = context.getFilename(); - - // Only check files in src/components/ - if (!filename.includes('src/components/') || !filename.endsWith('.tsx')) { - return; - } - - // Skip test files themselves - if (filename.includes('.test.') || filename.includes('.spec.')) { - return; - } - - const componentName = basename(filename, '.tsx'); - - // Find the project root (go up from src/components) - const srcIndex = filename.indexOf('src/components/'); - const projectRoot = filename.substring(0, srcIndex); - - // Expected test file path - const expectedTestPath = join( - projectRoot, - 'tests/__tests__/components', - `${componentName}.test.tsx` - ); - - // Check if test file exists - if (!existsSync(expectedTestPath)) { - context.report({ - node, - messageId: 'missingTest', - data: { - componentName, - expectedPath: `tests/__tests__/components/${componentName}.test.tsx`, - }, - }); - } - }, - }; - }, -}; - -export default { - rules: { - 'require-component-tests': rule, - }, -}; - diff --git a/eslint.config.js b/eslint.config.js index 4061bd5e..c0a96313 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,6 @@ import tsparser from '@typescript-eslint/parser'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import globals from 'globals'; -import requireComponentTests from './eslint-plugins/require-component-tests.js'; export default [ eslint.configs.recommended, @@ -29,7 +28,6 @@ export default [ '@typescript-eslint': tseslint, react: react, 'react-hooks': reactHooks, - 'require-component-tests': requireComponentTests, }, rules: { ...tseslint.configs.recommended.rules, @@ -46,7 +44,6 @@ export default [ 'no-case-declarations': 'off', 'no-control-regex': 'off', 'react/display-name': 'off', - 'require-component-tests/require-component-tests': 'error', }, settings: { react: { diff --git a/package-lock.json b/package-lock.json index 9d3e3fac..65cdc9d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^6.1.1", "globals": "^16.4.0", + "husky": "^9.1.7", "ink-testing-library": "^4.0.0", "jest": "^29.7.0", "prettier": "^3.6.2", @@ -6273,6 +6274,22 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", diff --git a/package.json b/package.json index bb3e2e1e..45714991 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:components": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.components.config.js --coverage --forceExit" + "test": "NODE_OPTIONS='--experimental-vm-modules' jest", + "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch", + "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage", + "test:components": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.components.config.js --coverage --forceExit", + "docs:commands": "npm run build && node scripts/generate-command-docs.js", + "prepare": "husky" }, "keywords": [ "runloop", @@ -46,12 +48,12 @@ ], "repository": { "type": "git", - "url": "https://github.com/runloopai/rl-cli-node.git" + "url": "https://github.com/runloopai/rl-cli.git" }, "bugs": { - "url": "https://github.com/runloopai/rl-cli-node/issues" + "url": "https://github.com/runloopai/rl-cli/issues" }, - "homepage": "https://github.com/runloopai/rl-cli-node#readme", + "homepage": "https://github.com/runloopai/rl-cli#readme", "engines": { "node": ">=18.0.0" }, @@ -96,6 +98,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^6.1.1", "globals": "^16.4.0", + "husky": "^9.1.7", "ink-testing-library": "^4.0.0", "jest": "^29.7.0", "prettier": "^3.6.2", diff --git a/scripts/generate-command-docs.js b/scripts/generate-command-docs.js new file mode 100644 index 00000000..edb8f536 --- /dev/null +++ b/scripts/generate-command-docs.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { readFileSync, writeFileSync } from "fs"; +import { createProgram } from "../dist/utils/commands.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, ".."); +const readmePath = join(rootDir, "README.md"); + +/** + * Generates markdown documentation for the command structure from Commander + * in the format: code blocks with grouped examples + */ +function generateCommandStructure(program) { + const lines = []; + lines.push("## Command Structure"); + lines.push(""); + lines.push( + "The CLI is organized into command buckets:", + ); + lines.push(""); + + // Get all top-level commands (excluding hidden ones) + const commands = program.commands.filter((cmd) => !cmd._hidden); + + // Group names for better organization + const groupNames = { + create: "Create", + list: "List", + get: "Get", + delete: "Delete", + exec: "Execute commands", + upload: "Upload files", + download: "Download files", + read: "Read files", + write: "Write files", + suspend: "Suspend", + resume: "Resume", + shutdown: "Shutdown", + ssh: "SSH", + scp: "Copy files (scp)", + rsync: "Sync files (rsync)", + tunnel: "Create tunnel", + logs: "View logs", + status: "Get status", + install: "Install", + start: "Start", + }; + + for (const command of commands) { + const commandName = command.name(); + const commandAlias = command.aliases()[0] || null; + const sectionTitleBase = commandName.charAt(0).toUpperCase() + commandName.slice(1) + " Commands"; + const sectionTitle = commandAlias + ? `${sectionTitleBase} (alias: \`${commandAlias}\`)` + : sectionTitleBase; + + lines.push(`### ${sectionTitle}`); + lines.push(""); + lines.push("```bash"); + + // Get subcommands + const subcommands = command.commands.filter((cmd) => !cmd._hidden); + + // Group subcommands by their base action + const grouped = {}; + for (const subcmd of subcommands) { + const baseName = subcmd.name().split("-")[0]; // e.g., "exec-async" -> "exec" + if (!grouped[baseName]) { + grouped[baseName] = []; + } + grouped[baseName].push(subcmd); + } + + // Generate examples for each group + for (const [groupName, cmds] of Object.entries(grouped)) { + for (const subcmd of cmds) { + // 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 description, make it shorter for inline comments + let desc = subcmd.description(); + if (desc.length > 40) { + desc = desc.substring(0, 37) + "..."; + } + + lines.push(`${fullCmd.padEnd(40)} # ${desc}`); + + // Show subcommand alias if exists (but not if command itself has alias) + if (subcmd.aliases().length > 0 && !commandAlias) { + const aliasName = subcmd.aliases()[0]; + const aliasFullCmd = `rli ${commandName} ${aliasName} ${args}`.trim(); + lines.push(`${aliasFullCmd.padEnd(40)} # Alias`); + } + } + } + + lines.push("```"); + lines.push(""); + } + + return lines.join("\n"); +} + +/** + * Updates the README.md file with the generated command structure + */ +function updateReadme(newCommandStructure) { + const readmeContent = readFileSync(readmePath, "utf-8"); + + // Find the start and end of the Command Structure section + const startMarker = "## Command Structure"; + const endMarker = "## MCP Server"; + + const startIndex = readmeContent.indexOf(startMarker); + if (startIndex === -1) { + throw new Error("Could not find '## Command Structure' section in README.md"); + } + + // Find the end of the section (before the next ## heading) + const afterStart = readmeContent.substring(startIndex); + const endIndex = afterStart.indexOf(endMarker); + if (endIndex === -1) { + throw new Error("Could not find end marker '## MCP Server' in README.md"); + } + + // Extract the content before and after the section + const before = readmeContent.substring(0, startIndex); + const after = readmeContent.substring(startIndex + endIndex); + + // Combine with the new command structure + const updatedContent = before + newCommandStructure + "\n\n" + after; + + writeFileSync(readmePath, updatedContent, "utf-8"); + console.log("✅ Updated README.md with generated command structure"); +} + +async function main() { + try { + const program = createProgram(); + const markdown = generateCommandStructure(program); + updateReadme(markdown); + } catch (error) { + console.error("Error generating command docs:", error); + process.exit(1); + } +} + +main(); diff --git a/src/cli.ts b/src/cli.ts index 950763c9..056e0f82 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,15 +1,9 @@ #!/usr/bin/env node -import { Command } from "commander"; -import { createDevbox } from "./commands/devbox/create.js"; -import { listDevboxes } from "./commands/devbox/list.js"; -import { deleteDevbox } from "./commands/devbox/delete.js"; -import { execCommand } from "./commands/devbox/exec.js"; -import { uploadFile } from "./commands/devbox/upload.js"; -import { getConfig } from "./utils/config.js"; -import { VERSION } from "./version.js"; import { exitAlternateScreenBuffer } from "./utils/screen.js"; import { processUtils } from "./utils/processUtils.js"; +import { createProgram } from "./utils/commands.js"; +import { getApiKeyErrorMessage } from "./utils/config.js"; // Global Ctrl+C handler to ensure it always exits processUtils.on("SIGINT", () => { @@ -19,585 +13,7 @@ processUtils.on("SIGINT", () => { processUtils.exit(130); // Standard exit code for SIGINT }); -const program = new Command(); - -program - .name("rli") - .description("Beautiful CLI for Runloop devbox management") - .version(VERSION); - -// Devbox commands -const devbox = program - .command("devbox") - .description("Manage devboxes") - .alias("d"); - -devbox - .command("create") - .description("Create a new devbox") - .option("-n, --name ", "Devbox name") - .option("-t, --template