Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
# Revenue Holdings CLI
# Revenue Holdings CLI

[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/revenueholdings?style=social)](https://github.com/Coding-Dev-Tools/revenueholdings/stargazers)
[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/devforge?style=social)](https://github.com/Coding-Dev-Tools/devforge/stargazers)

**The `rh` command — one install, ten developer CLI tools.**
**The `rh` command one install, ten developer CLI tools.**

[![PyPI](https://img.shields.io/pypi/v/revenueholdings)](https://pypi.org/project/revenueholdings/)
[![Python Versions](https://img.shields.io/pypi/pyversions/revenueholdings)](https://pypi.org/project/revenueholdings/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Ten production-ready CLI tools for API contracts, SQL generation, infrastructure diffs, config drift, API mocking, key management, env syncing, schema conversion, MCP servers, and dead code removal — in a single package. Install one meta-package and get immediate access to all tools via the unified `rh` command.
Ten production-ready CLI tools for API contracts, SQL generation, infrastructure diffs, config drift, API mocking, key management, env syncing, schema conversion, MCP servers, and dead code removal in a single package. Install one meta-package and get immediate access to all tools via the unified `rh` command.

---

[🏠 Landing Page](https://coding-dev-tools.github.io/revenueholdings.dev/) · [📝 Blog](https://coding-dev-tools.github.io/revenueholdings.dev/blog.html) · [🐛 Report a Bug](https://github.com/Coding-Dev-Tools/revenueholdings/issues)
[🏠 Landing Page](https://coding-dev-tools.github.io/revenueholdings.dev/) · [📝 Blog](https://coding-dev-tools.github.io/revenueholdings.dev/blog.html) · [🐛 Report a Bug](https://github.com/Coding-Dev-Tools/devforge/issues)

---

## Why the Suite?

Instead of installing ten separate tools and learning ten different CLIs, `pip install revenueholdings[all]` gives you:

- **Single CLI** (`rh`) to invoke any tool — no context switching
- **Single CLI** (`rh`) to invoke any tool no context switching
- **Consistent flags, output formats, and help** across all tools
- **Shared configuration** — one install, all tools
- **Shared configuration** one install, all tools

## Installation

Expand Down Expand Up @@ -55,34 +55,34 @@ rh versions # Show installed tool versions
Run any tool directly through `rh`:

```bash
# API Contract Guardian — detect OpenAPI breaking changes
# API Contract Guardian detect OpenAPI breaking changes
rh guard check spec-v1.yaml spec-v2.yaml

# json2sql — convert JSON to SQL INSERT statements
# json2sql convert JSON to SQL INSERT statements
rh sql convert data.json --dialect postgres

# DeployDiff — preview infrastructure changes with cost estimates
# DeployDiff preview infrastructure changes with cost estimates
rh deploy preview plan.json

# ConfigDrift — catch config drift between environments
# ConfigDrift catch config drift between environments
rh drift check dev.yaml prod.yaml

# APIGhost — spawn mock API server from OpenAPI spec
# APIGhost spawn mock API server from OpenAPI spec
rh ghost serve openapi.yaml

# APIAuth — generate API keys and JWTs
# APIAuth generate API keys and JWTs
rh auth generate --type api-key

# Envault — sync .env files across environments
# Envault sync .env files across environments
rh envault diff .env.dev .env.prod

# SchemaForge — convert between ORM formats
# SchemaForge convert between ORM formats
rh schema convert schema.prisma --to drizzle

# click-to-mcp — wrap CLI as MCP server
# click-to-mcp wrap CLI as MCP server
rh mcp wrap my-cli --transport http

# DeadCode — find unused exports in React/Next.js
# DeadCode find unused exports in React/Next.js
rh deadcode scan src/
```

Expand All @@ -97,20 +97,20 @@ rh deadcode scan src/
| `ghost` | apighost | Mock API server from OpenAPI specs with VCR cassette recording and realistic fake data |
| `auth` | apiauth | API key and JWT lifecycle management with AES-256-GCM encrypted local store |
| `envault` | envault | Env variable syncing, diffing, and secret rotation with Vault/AWS SSM/Doppler/1Password support |
| `schema` | schemaforge | Bidirectional ORM schema converter — 11 formats with zero-loss roundtripping |
| `mcp` | click-to-mcp | Auto-wrap any Click/typer CLI as an MCP server — zero code changes |
| `schema` | schemaforge | Bidirectional ORM schema converter 11 formats with zero-loss roundtripping |
| `mcp` | click-to-mcp | Auto-wrap any Click/typer CLI as an MCP server zero code changes |
| `deadcode` | deadcode | Detect unused exports, dead routes, orphaned CSS in TypeScript/React/Next.js projects |

## Links

- [Landing Page](https://coding-dev-tools.github.io/revenueholdings.dev/)
- [GitHub Organization](https://github.com/Coding-Dev-Tools)
- [Report an Issue](https://github.com/Coding-Dev-Tools/revenueholdings/issues)
- [Report an Issue](https://github.com/Coding-Dev-Tools/devforge/issues)

## License

MIT — see [LICENSE](LICENSE) for details.
MIT see [LICENSE](LICENSE) for details.

---

<sub>Built by [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) — autonomous AI agents generating revenue 24/7.</sub>
<sub>Built by [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) autonomous AI agents generating revenue 24/7.</sub>
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ all = [
dev = ["pytest>=7.0.0"]

[project.urls]
Homepage = "https://github.com/Coding-Dev-Tools/revenueholdings"
Repository = "https://github.com/Coding-Dev-Tools/revenueholdings"
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/revenueholdings/issues"
Homepage = "https://github.com/Coding-Dev-Tools/devforge"
Repository = "https://github.com/Coding-Dev-Tools/devforge"
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/devforge/issues"

[project.scripts]
rh = "revenueholdings.cli:app"
Expand Down
6 changes: 5 additions & 1 deletion src/revenueholdings/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ def show_versions(
tool: str | None = typer.Argument(None, help="Check version of a specific tool."),
):
"""Show installed tool versions."""
targets = ([tool] if tool in TOOLS else []) if tool else _builtins.list(TOOLS.keys())
if tool is not None and tool not in TOOLS:
console.print(f"[red]Unknown tool: {tool}[/red]")
console.print(f"Available: {', '.join(TOOLS.keys())}")
raise typer.Exit(code=1)
targets = [tool] if tool else _builtins.list(TOOLS.keys())

for t in targets:
info = TOOLS[t]
Expand Down
64 changes: 63 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from revenueholdings import TOOLS, __version__
from revenueholdings.cli import app
from typer.testing import CliRunner
from unittest import mock

runner = CliRunner()

Expand Down Expand Up @@ -35,12 +36,73 @@ def test_unknown_tool(self):
assert "Unknown" in result.stdout


class TestInstallCommand:
@mock.patch("revenueholdings.cli.subprocess.run")
def test_install_specific_tool(self, mock_run):
"""Install a specific tool by name."""
mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="")
result = runner.invoke(app, ["install", "guard"])
assert result.exit_code == 0
assert "Successfully" in result.stdout
mock_run.assert_called_once()

@mock.patch("revenueholdings.cli.subprocess.run")
def test_install_all(self, mock_run):
"""Install all tools via the 'all' alias."""
mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="")
result = runner.invoke(app, ["install", "all"])
assert result.exit_code == 0
assert "Successfully" in result.stdout
mock_run.assert_called_once()

def test_install_unknown_tool(self):
"""Error on unknown tool name."""
result = runner.invoke(app, ["install", "nonexistent"])
assert result.exit_code == 1
assert "Unknown" in result.stdout
assert "Available:" in result.stdout

@mock.patch("revenueholdings.cli.subprocess.run")
def test_install_failure(self, mock_run):
"""Handle pip install failure gracefully."""
mock_run.return_value = mock.MagicMock(returncode=1, stdout="", stderr="Error message")
result = runner.invoke(app, ["install", "guard"])
assert result.exit_code == 1
assert "failed" in result.stdout.lower()


class TestVersionsCommand:
def test_versions_runs(self):
"""List all tool versions without error."""
result = runner.invoke(app, ["versions"])
# Should succeed even if tools aren't installed
assert result.exit_code == 0

def test_versions_unknown_tool_fails(self):
"""Error on unknown tool name."""
result = runner.invoke(app, ["versions", "nonexistent"])
assert result.exit_code == 1
assert "Unknown" in result.stdout

@mock.patch("revenueholdings.cli.subprocess.run")
def test_versions_specific_tool_not_installed(self, mock_run):
"""Show 'not installed' for a tool that isn't installed."""
mock_run.return_value = mock.MagicMock(
returncode=1, stdout="", stderr=""
)
result = runner.invoke(app, ["versions", "guard"])
assert result.exit_code == 0
assert "guard" in result.stdout
assert "not installed" in result.stdout


class TestDispatchCommands:
def test_invalid_tool_subcommand(self):
"""Reject dispatch to an unknown tool subcommand."""
result = runner.invoke(app, ["nonexistent"])
# typer outputs error to stderr, not stdout
assert result.exit_code != 0
assert "No such command" in result.stdout or "No such command" in result.stderr


class TestHelp:
def test_help(self):
Expand Down