From 9457cc18e3ac26957614a2dda40bf6a36071905e Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Tue, 24 Mar 2026 14:06:02 -0400 Subject: [PATCH 1/4] add helpful cli hints for ability discovery - abilities list shows copy-pasteable run commands under each ability - command_not_found hook suggests `abilities run` for ability names - fuzzy "did you mean?" for typos in command names --- package.json | 3 ++ src/commands/abilities/list.ts | 15 ++++++- src/hooks/command-not-found.ts | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/hooks/command-not-found.ts diff --git a/package.json b/package.json index 1fd0518..1f9bbea 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "@oclif/plugin-help", "@oclif/plugin-autocomplete" ], + "hooks": { + "command_not_found": "./dist/hooks/command-not-found.js" + }, "topicSeparator": " ", "macos": { "identifier": "com.mainwp.mainwpcontrol" diff --git a/src/commands/abilities/list.ts b/src/commands/abilities/list.ts index 16ad54b..2546289 100644 --- a/src/commands/abilities/list.ts +++ b/src/commands/abilities/list.ts @@ -7,6 +7,7 @@ import { Flags } from '@oclif/core'; import { BaseCommand, commonFlags } from '../../lib/base-command.js'; import { formatTable, formatHeading } from '../../output/formatter.js'; +import { color, colors } from '../../utils/colors.js'; export default class AbilitiesList extends BaseCommand { static description = 'List available abilities'; @@ -94,7 +95,19 @@ export default class AbilitiesList extends BaseCommand { return [a.name, a.label || a.description.slice(0, 50), type]; }); - lines.push(formatTable(headers, rows)); + // Render table with usage sub-rows under each ability + const tableStr = formatTable(headers, rows); + const tableLines = tableStr.split('\n'); + // Header and separator + lines.push(tableLines[0] ?? '', tableLines[1] ?? ''); + // Data rows with copy-pasteable usage hints + for (let j = 0; j < catAbilities.length; j++) { + lines.push(tableLines[j + 2] ?? ''); + const ability = catAbilities[j]!; + const shortName = ability.name.split('/').pop() ?? ability.name; + lines.push(color(` mainwpcontrol abilities run ${shortName}`, colors.dim)); + } + lines.push(''); } diff --git a/src/hooks/command-not-found.ts b/src/hooks/command-not-found.ts new file mode 100644 index 0000000..d6d15f3 --- /dev/null +++ b/src/hooks/command-not-found.ts @@ -0,0 +1,77 @@ +/** + * Oclif hook: command_not_found + * + * Provides helpful suggestions when a user enters an unknown command: + * 1. Ability names typed as commands → suggest `abilities run ` + * 2. Typos in known commands → "Did you mean ...?" + * 3. Everything else → generic error with help hint + */ + +import { Hook } from '@oclif/core'; + +/** Simple Levenshtein distance (no external deps) */ +function levenshtein(a: string, b: string): number { + const m = a.length; + const n = b.length; + const dp: number[][] = Array.from({ length: m + 1 }, () => + Array(n + 1).fill(0), + ); + for (let i = 0; i <= m; i++) dp[i]![0] = i; + for (let j = 0; j <= n; j++) dp[0]![j] = j; + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i]![j] = + a[i - 1] === b[j - 1] + ? dp[i - 1]![j - 1]! + : 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!); + } + } + return dp[m]![n]!; +} + +const hook: Hook<'command_not_found'> = async function ({ id, config }) { + // Strip common prefix mistakes (e.g., "mainwp/list-updates-v1") + const name = id.replace(/^mainwp\//, ''); + + // Ability name pattern: suggest abilities run + if (/-v\d+$/.test(name)) { + const suggestion = `${config.bin} abilities run ${name}`; + this.error( + `"${id}" is not a command. It looks like an ability name.\n\nRun it with:\n ${suggestion}`, + { exit: 1 }, + ); + } + + // Find closest matching command using Levenshtein distance + const commandIDs = [ + ...config.commandIDs, + ...config.commands.flatMap((c) => c.aliases), + ].filter( + (cid) => !config.commands.find((cmd) => cmd.id === cid)?.hidden, + ); + + if (commandIDs.length > 0) { + const matches = commandIDs + .map((cmd) => ({ cmd, distance: levenshtein(id, cmd) })) + .sort((a, b) => a.distance - b.distance); + + const best = matches[0]; + const threshold = Math.max(Math.ceil(id.length * 0.4), 3); + if (best && best.distance <= threshold) { + // Convert oclif internal separator (:) to display format (space) + const displayCmd = best.cmd.replace(/:/g, ' '); + this.error( + `"${id.replace(/:/g, ' ')}" is not a ${config.bin} command. Did you mean "${displayCmd}"?\n\nRun ${config.bin} help for a list of available commands.`, + { exit: 127 }, + ); + } + } + + // Fallback: no close match found + this.error( + `command "${id.replace(/:/g, ' ')}" not found. Run ${config.bin} help for a list of available commands.`, + { exit: 2 }, + ); +}; + +export default hook; From ce401efa86f689d34c5d9feef774eeb5c9a689ee Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Tue, 24 Mar 2026 14:20:00 -0400 Subject: [PATCH 2/4] rewrite readme for progressive disclosure, add login hint Restructure so users hit a working command before reading concepts. --- README.md | 470 +++++++++++++++++++++++++++--------------- src/commands/login.ts | 5 +- 2 files changed, 303 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index c126ec2..c5c15d4 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,123 @@ # MainWP Control -A CLI for managing your MainWP Dashboard from the terminal. The command is `mainwpcontrol`. +A CLI for managing your MainWP Dashboard from the terminal. List sites, push updates, sync data, run batch operations across dozens of sites. The command is `mainwpcontrol`. -You can list sites, check status, push updates, sync data, add or remove child sites, and run batch operations across dozens of sites. Everything outputs structured JSON for piping into other tools. Exit codes are deterministic so CI pipelines can branch on them. There's also an optional chat mode if you want to explore abilities conversationally before scripting them. +**Looking for the MCP Server instead?** [MainWP MCP Server](https://github.com/mainwp/mainwp-mcp) is for conversational AI management inside Claude, Cursor, or any MCP-compatible client. MainWP Control is for automation: cron jobs, CI/CD pipelines, monitoring scripts, and batch operations. Both talk to the same Abilities API with the same safety model. --- -## When to Use MainWP Control vs MCP Server +## Quick Start + +You need Node.js 20+ and a MainWP Dashboard (v6+) with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/). + +```bash +npm install -g @mainwp/control -**[MainWP MCP Server](https://github.com/mainwp/mainwp-mcp)** is for conversational AI management: natural language queries inside Claude, Cursor, ChatGPT, or any MCP-compatible client. Good for exploration, ad-hoc questions, and interactive workflows. +mainwpcontrol login + +mainwpcontrol abilities list +``` -**MainWP Control** is for automation: cron jobs, CI/CD pipelines, monitoring scripts, and batch operations. `mainwpcontrol` gives you deterministic exit codes, stable JSON output, and composability with standard Unix tools. Both talk to the same Abilities API with the same safety model. +You should see something like this: + +``` +Abilities (87 total) + + Sites +┌──────────────────┬──────────────────────┬────────────────┐ +│ Name │ Description │ Type │ +├──────────────────┼──────────────────────┼────────────────┤ +│ list-sites-v1 │ List MainWP sites │ 📖 read │ +│ get-site-v1 │ Get site details │ 📖 read │ +│ sync-sites-v1 │ Sync all sites │ ✏️ write │ +└──────────────────┴──────────────────────┴────────────────┘ + mainwpcontrol abilities run list-sites-v1 + mainwpcontrol abilities run get-site-v1 + ... +``` + +That's it. You're connected and you can see every operation your Dashboard supports. --- -## Quick Start +## What Just Happened + +`abilities list` shows every operation available on your Dashboard. These are called "abilities" and they cover sites, plugins, themes, updates, clients, tags, and more. + +Each ability has a name (like `list-sites-v1`) that you pass to `abilities run` to execute it. The list tells you whether each one is read-only, a write operation, or destructive. + +--- + +## Common Use Cases + +**List all your sites:** + +```bash +mainwpcontrol abilities run list-sites-v1 --json +``` + +**Check for pending updates across sites:** + +```bash +mainwpcontrol abilities run list-updates-v1 --json +``` + +**Get details for a specific site:** + +```bash +mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json +``` + +**Preview a destructive action before running it:** -### Prerequisites +```bash +mainwpcontrol abilities run delete-site-v1 \ + --input '{"site_id_or_domain": "mysite.com"}' \ + --dry-run --json +``` -1. **Node.js 20 or later** (the LTS version from [nodejs.org](https://nodejs.org/) is recommended) -2. **A MainWP Dashboard** (version 6+) with the Abilities API enabled -3. **A WordPress Application Password** (not your login password). Create one in WordPress admin under Users > Your Profile > Application Passwords. See the [WordPress Application Passwords guide](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) for details. +Nothing changes until you explicitly pass `--confirm`. -### Option A: Standard install (recommended) +**Update plugins and wait for completion:** -This is the best path for most users. Pre-built keychain binaries are included for macOS, Windows, and Linux (x64 and arm64). On other platforms, you may need C++ build tools during installation. +```bash +mainwpcontrol abilities run update-site-plugins-v1 \ + --input '{"site_id": 1}' \ + --wait --json +``` + +`--wait` blocks until the operation finishes. Useful in CI pipelines. + +--- + +## Installation + +### Standard install (recommended) + +Pre-built keychain binaries are included for macOS, Windows, and Linux (x64 and arm64). On other platforms you may need C++ build tools during installation. ```bash -# Install globally npm install -g @mainwp/control -# Log in (stores credentials in your OS keychain) +# Interactive login (stores credentials in your OS keychain) mainwpcontrol login - -# Verify it works -mainwpcontrol abilities run list-sites-v1 --json ``` -### Option B: Environment variable auth (no keychain) +### Environment variable auth (CI, Docker, headless) -Use this if keytar fails to build, or in CI, Docker, and headless environments where no OS keychain is available. +Use this when no OS keychain is available, or if keytar fails to build. ```bash -# Install globally npm install -g @mainwp/control -# Set your Application Password as an env var export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx' -# Log in non-interactively (credentials stay in the environment, not on disk) mainwpcontrol login --url https://dashboard.example.com --username admin - -# Verify it works -mainwpcontrol abilities run list-sites-v1 --json ``` -> **Tip:** If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it entirely. +When the OS keychain is unavailable, credentials are not stored on disk. Keep `MAINWP_APP_PASSWORD` set for each run. -When the OS keychain is unavailable, `mainwpcontrol` does not persist plaintext credentials. Keep `MAINWP_APP_PASSWORD` set for each run on CI, cron hosts, and headless servers. +If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it. --- @@ -111,136 +171,257 @@ When you run a command, the output appears in your terminal. A few things to kno --- -## Real-World Workflows +## Basic Usage -We recommend a two-step pattern for destructive operations: preview first, then execute. +### Abilities ```bash -# Step 1: Preview what will be deleted (nothing changes) -mainwpcontrol abilities run delete-site-v1 \ - --input '{"site_id_or_domain": "mysite.com"}' \ - --dry-run --json +# List all abilities +mainwpcontrol abilities list -# Step 2: Execute after reviewing the preview -mainwpcontrol abilities run delete-site-v1 \ - --input '{"site_id_or_domain": "mysite.com"}' \ - --confirm --force --json +# Filter by category +mainwpcontrol abilities list --category sites + +# Get full details and input schema for an ability +mainwpcontrol abilities info list-sites-v1 + +# Run an ability +mainwpcontrol abilities run list-sites-v1 --json + +# Run with input parameters +mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json ``` -Each workflow guide below walks you from creating an Application Password through a working result, with every step verified. +### Profiles -| Workflow | Description | -|----------|-------------| -| [Daily Health Check](docs/workflows/daily-health-check.md) | Cron job that checks site connectivity and alerts via Slack | -| [Plugin Deployment Verification](docs/workflows/plugin-deployment-verification.md) | GitHub Actions workflow to verify a plugin exists across all sites | -| [Monthly Batch Updates](docs/workflows/monthly-batch-updates.md) | Preview and apply updates safely, scripted and GitHub Actions variants | -| [Input from File](docs/workflows/input-from-file.md) | Pass complex parameters via JSON files, stdin pipes, or heredocs | -| [Monitoring Integration](docs/workflows/monitoring-integration.md) | Send site metrics to Datadog, StatsD, or other monitoring tools | +```bash +# List all profiles +mainwpcontrol profile list + +# Switch active profile +mainwpcontrol profile use production.example.com + +# Use a profile for one command without switching +mainwpcontrol abilities list --profile staging.example.com + +# Delete a profile and its keychain credentials +mainwpcontrol profile delete staging.example.com +``` + +### Diagnostics + +```bash +# Check configuration and connectivity +mainwpcontrol doctor + +# Verbose output +mainwpcontrol doctor -v + +# JSON output +mainwpcontrol doctor --json +``` + +### Chat Mode + +Optional interactive mode for exploring abilities in natural language. Requires an LLM provider key. + +```bash +# Interactive chat +mainwpcontrol chat + +# Single message (works in scripts) +mainwpcontrol chat "list all sites with pending updates" +``` + +### Global Flags + +| Flag | Description | +|------|-------------| +| `--json` | Structured JSON output | +| `--quiet` / `-q` | Suppress output (exit code only) | +| `--profile ` | Use a specific profile | +| `--debug` | Show redacted debug diagnostics on stderr | +| `--help` | Show help | + +### Abilities Run Flags + +| Flag | Description | +|------|-------------| +| `--input` / `-i` | Input parameters as JSON (use `-` for stdin) | +| `--input-file` | Read input from a JSON file | +| `--dry-run` | Preview changes without executing | +| `--confirm` | Execute a destructive ability | +| `--force` | Skip interactive confirmation (CI mode) | +| `--wait` | Block until batch job completes | +| `--wait-timeout` | Max seconds to wait (default: 300) | --- -## Commands +## Concepts ### Abilities -The MainWP Abilities API provides all available operations: +Abilities are the operations your MainWP Dashboard exposes through its REST API. Each one has: + +- A versioned name (e.g., `list-sites-v1`, `delete-site-v1`) +- An input schema (what parameters it accepts) +- Annotations that tell you what kind of operation it is + +The annotations matter: +- **Readonly**: Safe to run anytime. Cannot modify data. +- **Destructive**: Permanently changes or deletes data. Requires `--dry-run` preview, then `--confirm` to execute. +- **Idempotent**: Safe to re-run. Same result on repeated calls. + +Run `mainwpcontrol abilities info ` to see the full schema and annotations for any ability. + +### Profiles + +A profile is a named connection to a MainWP Dashboard. It stores the Dashboard URL and username. Your password stays in the OS keychain (or in the `MAINWP_APP_PASSWORD` environment variable when no keychain is available). + +Running `mainwpcontrol login` creates a profile automatically, named after the Dashboard hostname: ```bash -# List all abilities -mainwpcontrol abilities list +# Creates profile "staging.example.com" +mainwpcontrol login --url https://staging.example.com --username admin + +# Creates profile "production.example.com" +mainwpcontrol login --url https://production.example.com --username admin +``` + +The profile file at `~/.config/mainwpcontrol/profiles.json` never contains passwords. -# Get ability details (input schema, annotations) -mainwpcontrol abilities info +### Safety Model -# Execute an ability -mainwpcontrol abilities run [--input JSON] [--input-file path] [--json] +Destructive operations follow a two-step pattern: preview first, then execute. -# Execute and wait for batch completion -mainwpcontrol abilities run --wait [--wait-timeout 300] --json +```bash +# Step 1: Preview (nothing changes) +mainwpcontrol abilities run delete-site-v1 \ + --input '{"site_id_or_domain": "mysite.com"}' \ + --dry-run --json + +# Step 2: Execute after reviewing the preview +mainwpcontrol abilities run delete-site-v1 \ + --input '{"site_id_or_domain": "mysite.com"}' \ + --confirm --force --json ``` +`--dry-run` and `--confirm` are mutually exclusive. You cannot pass both. + +In CI/scripted workflows where you've already validated the operation, pass `--confirm --force` directly to skip the interactive prompt. + ### Batch Jobs -Monitor long-running batch operations: +Operations that affect many items (200+) are automatically queued as batch jobs. The command returns a `job_id` immediately, and you can watch progress: ```bash -# Watch a batch job mainwpcontrol jobs watch -# With timeout +# With a timeout mainwpcontrol jobs watch --timeout 120 ``` -### Diagnostics +Or use `--wait` on the original command to block until completion: ```bash -# Check configuration and connectivity -mainwpcontrol doctor +mainwpcontrol abilities run sync-sites-v1 --wait --wait-timeout 300 --json +``` -# Verbose output with details -mainwpcontrol doctor -v +--- -# JSON output for scripting -mainwpcontrol doctor --json -``` +## Advanced Usage -### Profile Management +### CI/CD Patterns ```bash -# List all profiles -mainwpcontrol profile list +# Non-interactive login +export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx' +mainwpcontrol login --url https://dashboard.example.com --username admin -# Switch active profile -mainwpcontrol profile use +# Silent execution with exit codes +mainwpcontrol abilities run list-sites-v1 --json --quiet +echo "Exit code: $?" + +# Pipeline branching on exit codes +if mainwpcontrol abilities run check-sites-v1 --json --quiet; then + echo "All sites healthy" +else + echo "Issues detected" +fi ``` -### Authentication +### Input from Files and Stdin ```bash -# Interactive login (stores credentials in OS keychain) -mainwpcontrol login +# From a JSON file +mainwpcontrol abilities run update-site-plugins-v1 --input-file params.json --json -# Non-interactive login for CI/containers (password from env var) -export MAINWP_APP_PASSWORD='your-application-password' -mainwpcontrol login --url https://dashboard.example.com --username admin +# From stdin +echo '{"site_id": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json + +# Heredoc +mainwpcontrol abilities run get-site-v1 --input - --json < Users > Your Profile > Application Passwords > add a new password named "mainwpcontrol". +### Chat Mode Configuration -When the OS keychain is unavailable, `mainwpcontrol` does not persist plaintext credentials. Keep `MAINWP_APP_PASSWORD` set for each non-interactive run on CI, cron hosts, and headless servers. +Chat requires one of these environment variables: -### Chat Mode +| Variable | Provider | +|----------|----------| +| `ANTHROPIC_API_KEY` | Anthropic Claude | +| `OPENAI_API_KEY` | OpenAI GPT | +| `GOOGLE_API_KEY` | Google Gemini | +| `OPENROUTER_API_KEY` | OpenRouter | +| `LOCAL_LLM_API_KEY` | Local LLM (with optional `LOCAL_LLM_URL`) | + +Additional chat flags: `--provider`, `--model`, `--max-turns`, `--max-context-messages`, `--no-stream`. -Optional interactive mode for exploring abilities before scripting them. Requires an LLM provider key. +In non-TTY environments (pipes, CI), `mainwpcontrol chat` without a message argument exits with guidance instead of hanging. + +### Shell Completion ```bash -# Interactive chat -mainwpcontrol chat +# Bash +source /path/to/mainwp-control/scripts/completions/mainwpcontrol.bash -# Single message (works in scripts) -mainwpcontrol chat "list all sites with pending updates" +# Zsh +source /path/to/mainwp-control/scripts/completions/mainwpcontrol.zsh ``` -Chat requires one of these environment variables: -- `ANTHROPIC_API_KEY` (Anthropic Claude) -- `OPENAI_API_KEY` (OpenAI GPT) -- `GOOGLE_API_KEY` (Google Gemini) -- `OPENROUTER_API_KEY` (OpenRouter) -- `LOCAL_LLM_API_KEY` (Local endpoint, with optional `LOCAL_LLM_URL`) +### Configuration File -In non-TTY environments (pipes, CI), `mainwpcontrol chat` without a message exits with guidance. Use `mainwpcontrol chat "message"` for single-message mode in scripts. +Settings live at `~/.config/mainwpcontrol/settings.json`: ---- +```json +{ + "defaultJsonOutput": true, + "timeout": 30000, + "debug": false, + "llmProvider": "openai", + "chatContextMessages": 20 +} +``` -## Safety Model +### Workflow Guides -Destructive operations support a two-step workflow: preview with `--dry-run`, then execute with `--confirm`. Server-side confirmation enforcement is handled by the Abilities REST API. See the [Real-World Workflows](#real-world-workflows) section above for examples. +Step-by-step guides for common automation patterns: -In CI/scripted workflows, you can pass `--confirm --force` directly if you've already validated the operation. +| Workflow | Description | +|----------|-------------| +| [Daily Health Check](docs/workflows/daily-health-check.md) | Cron job that checks site connectivity and alerts via Slack | +| [Plugin Deployment Verification](docs/workflows/plugin-deployment-verification.md) | GitHub Actions workflow to verify a plugin exists across all sites | +| [Monthly Batch Updates](docs/workflows/monthly-batch-updates.md) | Preview and apply updates safely, scripted and GitHub Actions variants | +| [Input from File](docs/workflows/input-from-file.md) | Pass complex parameters via JSON files, stdin pipes, or heredocs | +| [Monitoring Integration](docs/workflows/monitoring-integration.md) | Send site metrics to Datadog, StatsD, or other monitoring tools | --- -## Exit Codes +## Reference + +### Exit Codes | Code | Meaning | CI Usage | |------|---------|----------| @@ -251,100 +432,42 @@ In CI/scripted workflows, you can pass `--confirm --force` directly if you've al | 4 | API error | Check ability parameters | | 5 | Internal error | Report bug | ---- +### Environment Variables -## Global Flags - -| Flag | Description | -|------|-------------| -| `--json` | Structured JSON output | -| `--quiet` / `-q` | Suppress output (exit code only) | -| `--profile ` | Use specific profile | -| `--debug` | Show redacted debug diagnostics on stderr | -| `--help` | Show help | - -### Abilities Run Flags - -| Flag | Description | -|------|-------------| -| `--input` / `-i` | Input parameters as JSON (use `-` for stdin) | -| `--input-file` | Read input from a JSON file | -| `--dry-run` | Preview changes without executing | -| `--confirm` | Execute destructive ability | -| `--force` | Skip interactive confirmation (CI mode) | -| `--wait` | Block until batch job completes | -| `--wait-timeout` | Max seconds to wait (default: 300) | - ---- - -## Environment Variables - -### MainWP Configuration +#### MainWP Configuration | Variable | Description | |----------|-------------| -| `MAINWP_APP_PASSWORD` | Application password for non-interactive login and commands when keychain storage is unavailable | +| `MAINWP_APP_PASSWORD` | Application password for non-interactive login and commands when keychain is unavailable | | `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keytar (keychain) loading entirely | | `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure HTTP Dashboard URLs | -### Chat Configuration (optional) +#### Chat/LLM Configuration -| Variable | Provider | -|----------|----------| +| Variable | Description | +|----------|-------------| | `ANTHROPIC_API_KEY` | Anthropic Claude | | `OPENAI_API_KEY` | OpenAI GPT | | `GOOGLE_API_KEY` | Google Gemini | | `OPENROUTER_API_KEY` | OpenRouter | -| `LOCAL_LLM_API_KEY` | Local LLM provider (required, enables local provider) | +| `LOCAL_LLM_API_KEY` | Local LLM provider (required to enable local provider) | | `LOCAL_LLM_URL` | Local endpoint URL (optional, defaults to localhost) | | `MAINWP_LLM_PROVIDER` | Override auto-detected provider | | `MAINWP_LLM_MODEL` | Specify model to use | ---- - -## Configuration File +### Configuration Settings -Settings in `~/.config/mainwpcontrol/settings.json`: - -```json -{ - "defaultJsonOutput": true, - "timeout": 30000, - "debug": false, - "llmProvider": "openai", - "chatContextMessages": 20 -} -``` +All settings in `~/.config/mainwpcontrol/settings.json`: | Setting | Type | Description | |---------|------|-------------| | `defaultJsonOutput` | boolean | Default to JSON output | -| `timeout` | number | Default HTTP request timeout in milliseconds | +| `timeout` | number | HTTP request timeout in milliseconds | | `debug` | boolean | Enable debug output | | `llmProvider` | string | Default LLM provider for chat | | `chatContextMessages` | number | Max messages in chat context | -| `skipSSLVerification` | boolean | Advanced fallback: disable TLS verification when the active profile does not set its own SSL preference | -| `allowInsecureHttp` | boolean | Advanced fallback: allow `http://` Dashboard URLs without setting `MAINWP_ALLOW_HTTP=1` | - -`skipSSLVerification` and `allowInsecureHttp` are insecure overrides. Prefer storing TLS behavior on the profile with `mainwpcontrol login --skip-ssl-verify`, and keep HTTPS as the default transport. - ---- - -## Shell Completion - -```bash -# Bash -source /path/to/mainwp-control/scripts/completions/mainwpcontrol.bash - -# Zsh -source /path/to/mainwp-control/scripts/completions/mainwpcontrol.zsh -``` - -## Requirements - -- Node.js 20 LTS or later -- MainWP Dashboard 6+ with Abilities API -- WordPress Application Password +| `skipSSLVerification` | boolean | Disable TLS verification (insecure, prefer per-profile setting via `login --skip-ssl-verify`) | +| `allowInsecureHttp` | boolean | Allow `http://` Dashboard URLs without `MAINWP_ALLOW_HTTP=1` | --- @@ -416,7 +539,6 @@ npm run lint # Check code style `npm run test:live` runs tests against a real MainWP Dashboard, including workflow documentation validation. These require a running Dashboard and credentials: ```bash -# Set credentials (or export from an .env file) export MAINWP_API_URL=https://your-dashboard.example.com export MAINWP_USER=your-admin-username export MAINWP_APP_PASSWORD=your-application-password @@ -424,9 +546,7 @@ export MAINWP_APP_PASSWORD=your-application-password npm run test:live ``` -The live suite includes: -- **API tests**: login, abilities discovery, read-only execution, safety model, exit codes -- **Workflow doc tests**: validates that every jq expression, field name, and data pipeline documented in `docs/workflows/` works against the real API +The live suite includes API tests (login, abilities discovery, read-only execution, safety model, exit codes) and workflow doc tests (validates that every jq expression, field name, and data pipeline documented in `docs/workflows/` works against the real API). Live tests are safe: they only run read-only operations and `--dry-run` previews, never mutations. @@ -438,6 +558,14 @@ GPL-3.0-or-later --- +## Requirements + +- Node.js 20 LTS or later +- MainWP Dashboard 6+ with Abilities API +- WordPress Application Password + +--- + - [MainWP](https://mainwp.com/) - [MainWP MCP Server](https://github.com/mainwp/mainwp-mcp) - [Issue Tracker](https://github.com/mainwp/mainwp-control/issues) diff --git a/src/commands/login.ts b/src/commands/login.ts index a8f372e..3392d62 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -9,7 +9,7 @@ import { BaseCommand, commonFlags } from '../lib/base-command.js'; import { getProfileStore, type Profile } from '../config/profile-store.js'; import { getKeychain } from '../config/keychain.js'; import { createHttpClient } from '../core/http-client.js'; -import { formatSuccess, formatWarning } from '../output/formatter.js'; +import { formatSuccess, formatWarning, formatInfo } from '../output/formatter.js'; import { AuthError, InputError } from '../utils/errors.js'; import { promptForInput, promptForPassword, isInteractive } from '../utils/prompt.js'; @@ -209,6 +209,9 @@ export default class Login extends BaseCommand { lines.push(formatWarning('Using HTTP instead of HTTPS. Credentials may be exposed.')); } + lines.push(''); + lines.push(formatInfo('Next: mainwpcontrol abilities list')); + return lines.join('\n'); } ); From 01b993fbf481c62f341b48d9560ad71391fda060 Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Tue, 24 Mar 2026 14:33:31 -0400 Subject: [PATCH 3/4] add intro text to readme basic usage sections --- README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c5c15d4..c63d4a2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ mainwpcontrol abilities list You should see something like this: -``` +```text Abilities (87 total) Sites @@ -175,6 +175,8 @@ When you run a command, the output appears in your terminal. A few things to kno ### Abilities +Your Dashboard exposes its operations as "abilities." You browse them, pick one, and run it. Every ability has a versioned name like `list-sites-v1` that you pass to `abilities run`. + ```bash # List all abilities mainwpcontrol abilities list @@ -194,6 +196,8 @@ mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json ### Profiles +Each `mainwpcontrol login` creates a profile, a named connection to a Dashboard, identified by hostname. If you manage multiple Dashboards, run `login` once per Dashboard to create a profile for each. + ```bash # List all profiles mainwpcontrol profile list @@ -210,6 +214,8 @@ mainwpcontrol profile delete staging.example.com ### Diagnostics +`doctor` checks your configuration, credentials, and Dashboard connectivity. Run it first if something isn't working. + ```bash # Check configuration and connectivity mainwpcontrol doctor @@ -223,18 +229,24 @@ mainwpcontrol doctor --json ### Chat Mode -Optional interactive mode for exploring abilities in natural language. Requires an LLM provider key. +If you have an LLM API key, you can talk to your Dashboard in plain English instead of constructing commands. Good for exploration, not required for anything. + +Set one of these environment variables to enable it: ```bash -# Interactive chat -mainwpcontrol chat +# Pick one (Anthropic, OpenAI, Google, or OpenRouter) +export ANTHROPIC_API_KEY='sk-ant-...' -# Single message (works in scripts) +mainwpcontrol chat mainwpcontrol chat "list all sites with pending updates" ``` +See [Chat Mode Configuration](#chat-mode-configuration) for all supported providers and flags. + ### Global Flags +These flags work on every command. + | Flag | Description | |------|-------------| | `--json` | Structured JSON output | @@ -245,6 +257,8 @@ mainwpcontrol chat "list all sites with pending updates" ### Abilities Run Flags +Extra flags for `abilities run`. These control input, safety checks, and batch job behavior. + | Flag | Description | |------|-------------| | `--input` / `-i` | Input parameters as JSON (use `-` for stdin) | From 023306ee4c0f24809806d9999bb64d37ffdcd089 Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Tue, 24 Mar 2026 15:01:03 -0400 Subject: [PATCH 4/4] fix review findings: exit codes, sanitization, hidden alias filter - use ExitCode.INPUT_ERROR consistently in command-not-found hook - sanitize ability.name before building hint line - fix hidden command alias filter to check aliases not just id - update readme sample to match actual formatTable output --- README.md | 14 ++++++-------- src/__tests__/process/smoke.test.ts | 4 ++-- src/commands/abilities/list.ts | 4 +++- src/hooks/command-not-found.ts | 9 +++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c63d4a2..a4b6fa4 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,14 @@ You should see something like this: Abilities (87 total) Sites -┌──────────────────┬──────────────────────┬────────────────┐ -│ Name │ Description │ Type │ -├──────────────────┼──────────────────────┼────────────────┤ -│ list-sites-v1 │ List MainWP sites │ 📖 read │ -│ get-site-v1 │ Get site details │ 📖 read │ -│ sync-sites-v1 │ Sync all sites │ ✏️ write │ -└──────────────────┴──────────────────────┴────────────────┘ +Name Description Type +---------------- -------------------- -------------- +list-sites-v1 List MainWP sites 📖 read mainwpcontrol abilities run list-sites-v1 +get-site-v1 Get site details 📖 read mainwpcontrol abilities run get-site-v1 - ... +sync-sites-v1 Sync all sites ✏️ write + mainwpcontrol abilities run sync-sites-v1 ``` That's it. You're connected and you can see every operation your Dashboard supports. diff --git a/src/__tests__/process/smoke.test.ts b/src/__tests__/process/smoke.test.ts index 12a027d..a4780ee 100644 --- a/src/__tests__/process/smoke.test.ts +++ b/src/__tests__/process/smoke.test.ts @@ -102,7 +102,7 @@ describe('smoke tests', () => { expect(result.exitCode).toBe(0); }); - it('mainwpcontrol "some message" exits 2 with command-not-found error', async () => { + it('mainwpcontrol "some message" exits 1 with command-not-found error', async () => { // oclif treats the argument as a command name lookup, not as a chat message arg. // Since there is no command called "some message", oclif exits with a command-not-found error. configWithProfile = await ConfigDir.create({ @@ -123,7 +123,7 @@ describe('smoke tests', () => { }, }); - expect(result.exitCode).toBe(2); + expect(result.exitCode).toBe(1); const combined = result.stdout + result.stderr; expect(combined).toMatch(/not found/i); }); diff --git a/src/commands/abilities/list.ts b/src/commands/abilities/list.ts index 2546289..bc8e106 100644 --- a/src/commands/abilities/list.ts +++ b/src/commands/abilities/list.ts @@ -8,6 +8,7 @@ import { Flags } from '@oclif/core'; import { BaseCommand, commonFlags } from '../../lib/base-command.js'; import { formatTable, formatHeading } from '../../output/formatter.js'; import { color, colors } from '../../utils/colors.js'; +import { stripControlChars } from '../../utils/terminal-sanitizer.js'; export default class AbilitiesList extends BaseCommand { static description = 'List available abilities'; @@ -104,7 +105,8 @@ export default class AbilitiesList extends BaseCommand { for (let j = 0; j < catAbilities.length; j++) { lines.push(tableLines[j + 2] ?? ''); const ability = catAbilities[j]!; - const shortName = ability.name.split('/').pop() ?? ability.name; + const safeName = stripControlChars(ability.name); + const shortName = safeName.split('/').pop() ?? safeName; lines.push(color(` mainwpcontrol abilities run ${shortName}`, colors.dim)); } diff --git a/src/hooks/command-not-found.ts b/src/hooks/command-not-found.ts index d6d15f3..13a2cf8 100644 --- a/src/hooks/command-not-found.ts +++ b/src/hooks/command-not-found.ts @@ -8,6 +8,7 @@ */ import { Hook } from '@oclif/core'; +import { ExitCode } from '../utils/exit-codes.js'; /** Simple Levenshtein distance (no external deps) */ function levenshtein(a: string, b: string): number { @@ -38,7 +39,7 @@ const hook: Hook<'command_not_found'> = async function ({ id, config }) { const suggestion = `${config.bin} abilities run ${name}`; this.error( `"${id}" is not a command. It looks like an ability name.\n\nRun it with:\n ${suggestion}`, - { exit: 1 }, + { exit: ExitCode.INPUT_ERROR }, ); } @@ -47,7 +48,7 @@ const hook: Hook<'command_not_found'> = async function ({ id, config }) { ...config.commandIDs, ...config.commands.flatMap((c) => c.aliases), ].filter( - (cid) => !config.commands.find((cmd) => cmd.id === cid)?.hidden, + (cid) => !config.commands.find((cmd) => cmd.id === cid || cmd.aliases?.includes(cid))?.hidden, ); if (commandIDs.length > 0) { @@ -62,7 +63,7 @@ const hook: Hook<'command_not_found'> = async function ({ id, config }) { const displayCmd = best.cmd.replace(/:/g, ' '); this.error( `"${id.replace(/:/g, ' ')}" is not a ${config.bin} command. Did you mean "${displayCmd}"?\n\nRun ${config.bin} help for a list of available commands.`, - { exit: 127 }, + { exit: ExitCode.INPUT_ERROR }, ); } } @@ -70,7 +71,7 @@ const hook: Hook<'command_not_found'> = async function ({ id, config }) { // Fallback: no close match found this.error( `command "${id.replace(/:/g, ' ')}" not found. Run ${config.bin} help for a list of available commands.`, - { exit: 2 }, + { exit: ExitCode.INPUT_ERROR }, ); };