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
-[](https://github.com/Coding-Dev-Tools/revenueholdings/stargazers)
+[](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.**
[](https://pypi.org/project/revenueholdings/)
[](https://pypi.org/project/revenueholdings/)
[](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):