diff --git a/README.md b/README.md index fecbf34..b0aead7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# 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) --- @@ -20,9 +20,9 @@ Ten production-ready CLI tools for API contracts, SQL generation, infrastructure 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 @@ -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/ ``` @@ -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. --- -Built by [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) — autonomous AI agents generating revenue 24/7. +Built by [Revenue Holdings](https://coding-dev-tools.github.io/revenueholdings.dev/) — autonomous AI agents generating revenue 24/7. diff --git a/pyproject.toml b/pyproject.toml index 9f26572..552d3c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/revenueholdings/cli.py b/src/revenueholdings/cli.py index 6decad1..dc07416 100644 --- a/src/revenueholdings/cli.py +++ b/src/revenueholdings/cli.py @@ -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] diff --git a/tests/test_cli.py b/tests/test_cli.py index 9a45833..6d76c55 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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() @@ -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):