From 1e698996b244d6f177fbc41010c7afbc9b63547b Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 21 Jul 2026 15:19:02 -0300 Subject: [PATCH] Prepare CmdForge 0.2.0 for PyPI --- CHANGELOG.md | 8 ++ LICENSE | 21 +++++ PYPI_README.md | 133 ++++++++++++++++++++++++++++++++ README.md | 30 ++++--- RELEASING.md | 64 +++++++++++++++ pyproject.toml | 17 ++-- src/cmdforge/__init__.py | 2 +- src/cmdforge/cli/picker.py | 14 ++++ src/cmdforge/web/docs_modern.py | 7 +- tests/test_packaging.py | 50 +++++++++++- tests/test_picker_m8.py | 19 +++++ tests/test_web_docs_content.py | 2 +- 12 files changed, 342 insertions(+), 25 deletions(-) create mode 100644 LICENSE create mode 100644 PYPI_README.md create mode 100644 RELEASING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 37dc15c..2321391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to CmdForge will be documented in this file. ## [Unreleased] +## [0.2.0] - 2026-07-21 + +First public Python package release. This release brings the current CmdForge +application to PyPI, including provider routing and provenance, MCP client and +server support, provider-attached skills, delegated tools, contracts and +preflight analysis, regression evidence, quality scoring, registry integrity, +project-owned tools, agent discovery commands, and local usage suggestions. + ### Added #### System Dependencies diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..af4abc8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rob + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PYPI_README.md b/PYPI_README.md new file mode 100644 index 0000000..0c8ba83 --- /dev/null +++ b/PYPI_README.md @@ -0,0 +1,133 @@ +# CmdForge + +**Turn useful AI workflows into commands you own.** + +CmdForge is a personal tool-building environment for the command line. A tool +can combine prompts, Python, other CmdForge tools, and Model Context Protocol +(MCP) calls, then behave like an ordinary Unix command: + +```bash +cat meeting.txt | meeting-decisions +git diff --staged | review-change +cmdforge run classify-private --no-fallback --require-local +``` + +Tools are readable YAML, stored either in `~/.cmdforge/` for personal use or +`./.cmdforge/` when they belong to a project. You choose the provider at build +time or runtime: a local Ollama model, an installed coding CLI, or an +OpenAI-compatible API. + +## Install + +CmdForge requires Python 3.10 or newer. `pipx` is recommended for a personal +command-line application: + +```bash +pipx install "cmdforge[mcp,pty]" +``` + +Or install it in a virtual environment: + +```bash +python -m pip install "cmdforge[mcp,pty]" +``` + +The base `cmdforge` package includes the CLI and desktop tool builder. The +`mcp` extra adds MCP client/server support, while `pty` adds interactive CLI +providers. + +Verify the installation and discover providers already present on the machine: + +```bash +cmdforge --version +cmdforge providers discover +cmdforge providers discover --add +``` + +## Begin with One Useful Command + +```bash +cmdforge create explain --prompt "Explain this clearly for a beginner: {input}" +echo "def area(r): return 3.14159 * r * r" | cmdforge run explain +``` + +Use the deterministic mock provider while developing: + +```bash +echo "sample input" | cmdforge run explain --provider mock +cmdforge inspect explain +``` + +For AI-assisted creation, install CmdForge's official `forge-tool`: + +```bash +cmdforge registry install official/forge-tool + +echo "Create a tool that extracts decisions and owners from meeting notes" \ + | forge-tool --name meeting-decisions --project +``` + +## Four Kinds of Step + +- **Prompt steps** call a selected AI provider. +- **Code steps** perform deterministic Python transformations. +- **Tool steps** compose existing CmdForge tools and delegated contexts. +- **MCP steps** call tools exposed by external MCP servers. + +Contracts, deterministic preflight, regression baselines, schema compatibility, +and evidence-based quality scores help distinguish “worked once” from a tool +you can maintain. + +## Use CmdForge from Coding Agents + +CmdForge can act as an MCP server for Codex and Claude Code. It is closed by +default: configuring a host does not expose any tools until you add an explicit +allowlist to `~/.cmdforge/mcp.yaml`. + +```bash +cmdforge mcp configure codex --dry-run +cmdforge mcp configure codex + +cmdforge mcp configure claude-code --scope project --dry-run +``` + +Agents can also discover the local and remote catalogs without reading prompt +or code bodies: + +```bash +cmdforge list --json --filter "commit message" --limit 10 +cmdforge registry search "release notes" --json --limit 5 +git diff | cmdforge run-once "Summarize this change: {input}" +``` + +## Privacy and Provenance + +Fallback is a data-movement decision. Sensitive callers can require a local +provider, particular capabilities, and verified runtime identity: + +```bash +cmdforge run incident-summary \ + --provider local-reasoner \ + --no-fallback \ + --require-local \ + --require-capability structured-json \ + --data-classification private \ + --require-model-identity \ + --result-envelope json +``` + +The result envelope reports the requested provider, actual provider, attempted +fallback chain, model, digest, and locality. Those fields are produced by +CmdForge rather than invented by the model. + +## Learn More + +- [Documentation](https://cmdforge.brrd.tech/docs) +- [MCP and coding agents](https://cmdforge.brrd.tech/docs/mcp-overview) +- [Providers and privacy policy](https://cmdforge.brrd.tech/docs/providers) +- [Contracts and quality evidence](https://cmdforge.brrd.tech/docs/contracts-quality) +- [Registry](https://cmdforge.brrd.tech/tools) +- [Source and issues](https://gitea.brrd.tech/rob/CmdForge) + +CmdForge is beta software, distributed under the MIT License. Review generated +tools and third-party registry tools as you would any other executable code. diff --git a/README.md b/README.md index 01fb0d3..1d41393 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,14 @@ That's it - you just used AI to explain itself. The `eli5` tool uses a free mode For regular use, install natively: ```bash -# Clone and install -git clone https://gitea.brrd.tech/rob/CmdForge.git -cd CmdForge -pip install -e . +# Install the application with MCP and interactive-provider support +pipx install "cmdforge[mcp,pty]" # Ensure ~/.local/bin is in PATH export PATH="$HOME/.local/bin:$PATH" -# Install an AI provider (interactive guide) -cmdforge providers install +# Discover AI CLIs, API keys, and Ollama models already on this machine +cmdforge providers discover --add # Launch the GUI cmdforge @@ -75,12 +73,24 @@ cmdforge create summarize ## Installation -### Native Install +### From PyPI + +```bash +pipx install "cmdforge[mcp,pty]" +``` + +Or install into an active virtual environment: + +```bash +python -m pip install "cmdforge[mcp,pty]" +``` + +### From Source ```bash git clone https://gitea.brrd.tech/rob/CmdForge.git cd CmdForge -pip install -e . +python -m pip install -e ".[mcp,pty]" ``` ### With Development Dependencies @@ -94,7 +104,7 @@ pip install -e ".[dev]" ### Requirements - Python 3.10+ -- At least one AI CLI tool installed (see [Provider Setup](docs/reference/providers.md)) +- A configured provider for live AI calls; the built-in mock works without one - PySide6 (included automatically - requires display server on Linux) ### Post-Install @@ -195,7 +205,7 @@ cmdforge usage enable # Begin recording tool names in shell pipe cmdforge usage suggestions # Show frequent pipelines cmdforge usage clear # Delete all local usage history -# Model Context Protocol (optional: pip install -e ".[mcp]") +# Model Context Protocol (optional: python -m pip install "cmdforge[mcp]") cmdforge mcp add local --command npx --arg=-y --arg @scope/server cmdforge mcp add remote --transport streamable-http --url https://example.com/mcp cmdforge mcp connect remote diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..a38e2bb --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,64 @@ +# Publishing CmdForge to PyPI + +PyPI releases are public and immutable. A release version must never be reused +for different bytes, even if an upload contains a mistake. + +## One-time account setup + +1. Create or sign in to an account at . +2. Verify the account email address. +3. Enable two-factor authentication and store the recovery codes in the + password manager and an offline recovery location. +4. For the first upload only, create an account-scoped API token. PyPI cannot + create a project-scoped token until the project exists. +5. Do not put a PyPI token in Git, `.pypirc`, chat, command arguments, or shell + history. Let Twine prompt for it interactively. + +After the first successful upload, immediately revoke the account-scoped token +and create a new token restricted to the `cmdforge` project. + +## Prepare and validate a release + +Update the version in both `pyproject.toml` and `src/cmdforge/__init__.py`, then +add the release to `CHANGELOG.md`. From a clean checkout: + +```bash +python -m pip install -e '.[release]' +pytest tests/ -m "not integration" +python -m build +python -m twine check dist/* +``` + +Inspect the wheel and source archive, then install the wheel into a clean +temporary virtual environment and exercise both entry points. Do not upload an +artifact that was built before the release commit. + +## Upload + +Run Twine interactively so the token is not recorded in shell history: + +```bash +python -m twine upload dist/* +``` + +When prompted, use `__token__` as the username and paste the API token as the +password. Once uploaded, verify the public project and install from PyPI in a +new environment: + +```bash +python -m venv /tmp/cmdforge-pypi-check +/tmp/cmdforge-pypi-check/bin/pip install 'cmdforge[mcp,pty]' +/tmp/cmdforge-pypi-check/bin/cmdforge --version +/tmp/cmdforge-pypi-check/bin/cmdforge --help +``` + +Tag and push only the commit whose artifacts were published: + +```bash +git tag -a v0.2.0 -m "CmdForge 0.2.0" +git push origin main +git push origin v0.2.0 +``` + +If an upload is wrong, fix it, increment the version, rebuild, and publish a +new release. Never delete and reuse the version number. diff --git a/pyproject.toml b/pyproject.toml index cf36127..e0ffc5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,14 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=77.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "cmdforge" -version = "0.1.0" -description = "Build custom AI-powered CLI commands in YAML" -readme = "README.md" -license = {text = "MIT"} +version = "0.2.0" +description = "Build, compose, and share AI-powered command-line tools you own" +readme = "PYPI_README.md" +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.10" authors = [ {name = "Rob"} @@ -18,7 +19,6 @@ classifiers = [ "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", - "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Programming Language :: Python :: 3", @@ -45,6 +45,10 @@ dev = [ "pytest-cov>=4.0", "tomli>=1.1; python_version < '3.11'", ] +release = [ + "build>=1.2", + "twine>=6.0", +] registry = [ "Flask>=2.3", "argon2-cffi>=21.0", @@ -81,6 +85,7 @@ Homepage = "https://cmdforge.brrd.tech" Documentation = "https://cmdforge.brrd.tech/docs" Repository = "https://gitea.brrd.tech/rob/CmdForge.git" Issues = "https://gitea.brrd.tech/rob/CmdForge/issues" +Changelog = "https://gitea.brrd.tech/rob/CmdForge/src/branch/main/CHANGELOG.md" [tool.setuptools.packages.find] where = ["src"] diff --git a/src/cmdforge/__init__.py b/src/cmdforge/__init__.py index 5d9e724..5e587b5 100644 --- a/src/cmdforge/__init__.py +++ b/src/cmdforge/__init__.py @@ -1,3 +1,3 @@ """CmdForge - A lightweight personal tool builder for AI-powered CLI commands.""" -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/src/cmdforge/cli/picker.py b/src/cmdforge/cli/picker.py index aea6082..da61e43 100644 --- a/src/cmdforge/cli/picker.py +++ b/src/cmdforge/cli/picker.py @@ -451,9 +451,23 @@ def pick_args(tty_input: TTYInput, tool: dict) -> Optional[dict]: def main(): """Entry point for cf command.""" global _ui_out + import argparse import subprocess import signal + # Parse metadata flags before touching stdin or /dev/tty. This keeps + # ``cf --help`` and ``cf --version`` usable in packaging checks, CI, and + # other non-interactive environments. + parser = argparse.ArgumentParser( + prog="cf", + description="Interactively find and run local or registry CmdForge tools", + ) + from .. import __version__ + parser.add_argument( + "--version", action="version", version=f"%(prog)s {__version__}" + ) + parser.parse_args() + # Handle Ctrl+C gracefully def handle_sigint(sig, frame): # Restore cursor and exit cleanly diff --git a/src/cmdforge/web/docs_modern.py b/src/cmdforge/web/docs_modern.py index d0c54ff..3b90aac 100644 --- a/src/cmdforge/web/docs_modern.py +++ b/src/cmdforge/web/docs_modern.py @@ -17,20 +17,21 @@ install together; optional extras add MCP, interactive PTY providers, and the re

A Clean Personal Installation

pipx keeps CmdForge isolated while placing cmdforge and cf on your path:

-
pipx install 'cmdforge[all]'
+
pipx install 'cmdforge[mcp,pty]'
 cmdforge --version
 cmdforge --help

A normal virtual environment works just as well:

python3.10 -m venv .venv
 . .venv/bin/activate
-python -m pip install 'cmdforge[all]'
+python -m pip install 'cmdforge[mcp,pty]'

Choose Only the Extras You Need

- + +
InstallAdds
cmdforgeCLI, desktop GUI, contracts, registry client, and local tool runner
cmdforge[mcp]MCP client steps and MCP server integration
cmdforge[pty]Interactive pseudo-terminal providers
cmdforge[all]All optional runtime features, including the web stack
cmdforge[mcp,pty]The recommended personal installation with agent integration
cmdforge[all]Every optional feature, including the registry web-server stack

Let CmdForge Inspect the Machine

diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 75b2940..3577f53 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -1,5 +1,6 @@ -"""Tests for packaging metadata.""" +"""Release metadata checks for the public Python distribution.""" +from pathlib import Path import sys import pytest @@ -9,12 +10,53 @@ if sys.version_info >= (3, 11): else: tomllib = pytest.importorskip("tomli") +from cmdforge import __version__ + + +ROOT = Path(__file__).resolve().parents[1] + + +def _project_metadata() -> dict: + with (ROOT / "pyproject.toml").open("rb") as handle: + return tomllib.load(handle)["project"] + + +def test_runtime_and_distribution_versions_match(): + assert _project_metadata()["version"] == __version__ + + +def test_public_distribution_metadata_is_complete(): + project = _project_metadata() + + assert project["name"] == "cmdforge" + assert project["readme"] == "PYPI_README.md" + assert project["license"] == "MIT" + assert project["license-files"] == ["LICENSE"] + assert project["requires-python"] == ">=3.10" + assert {"Homepage", "Documentation", "Repository", "Issues", "Changelog"} <= set( + project["urls"] + ) + + +def test_public_readme_describes_installation_and_current_capabilities(): + readme = (ROOT / "PYPI_README.md").read_text(encoding="utf-8") + + for term in ( + 'pipx install "cmdforge[mcp,pty]"', + "cmdforge providers discover", + "cmdforge mcp configure codex", + "--no-fallback", + "--result-envelope json", + ): + assert term in readme + + assert (ROOT / "LICENSE").is_file() + def test_web_templates_and_static_are_declared_as_package_data(): - with open("pyproject.toml", "rb") as f: - data = tomllib.load(f) + with (ROOT / "pyproject.toml").open("rb") as handle: + data = tomllib.load(handle) package_data = data["tool"]["setuptools"]["package-data"]["cmdforge.web"] - assert "templates/**/*.html" in package_data assert "static/**/*" in package_data diff --git a/tests/test_picker_m8.py b/tests/test_picker_m8.py index cef8cc7..9349876 100644 --- a/tests/test_picker_m8.py +++ b/tests/test_picker_m8.py @@ -1,10 +1,13 @@ """Behavioral coverage for registry-aware picker and deprecation UX.""" from io import StringIO +import sys import time from unittest.mock import patch import cmdforge.cli.picker as picker +import pytest +from cmdforge import __version__ from cmdforge.cli.picker import PickerResult @@ -89,3 +92,19 @@ def test_local_deprecation_selection_prints_migration_guidance(): output = picker._ui_out.getvalue() assert "Moved." in output assert "official/new" in output + + +def test_picker_help_does_not_require_a_terminal(capsys): + with patch.object(sys, "argv", ["cf", "--help"]), pytest.raises(SystemExit) as exc: + picker.main() + + assert exc.value.code == 0 + assert "Interactively find and run" in capsys.readouterr().out + + +def test_picker_version_does_not_require_a_terminal(capsys): + with patch.object(sys, "argv", ["cf", "--version"]), pytest.raises(SystemExit) as exc: + picker.main() + + assert exc.value.code == 0 + assert f"cf {__version__}" in capsys.readouterr().out diff --git a/tests/test_web_docs_content.py b/tests/test_web_docs_content.py index 8058889..fa2aae2 100644 --- a/tests/test_web_docs_content.py +++ b/tests/test_web_docs_content.py @@ -118,7 +118,7 @@ def test_installation_matches_runtime_and_current_provider_onboarding(): assert "Python 3.10" in installation assert "Python 3.8" not in installation - assert "cmdforge[mcp]" in installation + assert "cmdforge[mcp,pty]" in installation assert "cmdforge providers discover --add" in installation assert "cmdforge providers discover --add" in provider_setup assert "--type api" in provider_setup