Prepare CmdForge 0.2.0 for PyPI

This commit is contained in:
rob 2026-07-21 15:19:02 -03:00
parent ca6135ac72
commit 1e698996b2
12 changed files with 342 additions and 25 deletions

View File

@ -4,6 +4,14 @@ All notable changes to CmdForge will be documented in this file.
## [Unreleased] ## [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 ### Added
#### System Dependencies #### System Dependencies

21
LICENSE Normal file
View File

@ -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.

133
PYPI_README.md Normal file
View File

@ -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.

View File

@ -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: For regular use, install natively:
```bash ```bash
# Clone and install # Install the application with MCP and interactive-provider support
git clone https://gitea.brrd.tech/rob/CmdForge.git pipx install "cmdforge[mcp,pty]"
cd CmdForge
pip install -e .
# Ensure ~/.local/bin is in PATH # Ensure ~/.local/bin is in PATH
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
# Install an AI provider (interactive guide) # Discover AI CLIs, API keys, and Ollama models already on this machine
cmdforge providers install cmdforge providers discover --add
# Launch the GUI # Launch the GUI
cmdforge cmdforge
@ -75,12 +73,24 @@ cmdforge create summarize
## Installation ## 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 ```bash
git clone https://gitea.brrd.tech/rob/CmdForge.git git clone https://gitea.brrd.tech/rob/CmdForge.git
cd CmdForge cd CmdForge
pip install -e . python -m pip install -e ".[mcp,pty]"
``` ```
### With Development Dependencies ### With Development Dependencies
@ -94,7 +104,7 @@ pip install -e ".[dev]"
### Requirements ### Requirements
- Python 3.10+ - 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) - PySide6 (included automatically - requires display server on Linux)
### Post-Install ### Post-Install
@ -195,7 +205,7 @@ cmdforge usage enable # Begin recording tool names in shell pipe
cmdforge usage suggestions # Show frequent pipelines cmdforge usage suggestions # Show frequent pipelines
cmdforge usage clear # Delete all local usage history 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 local --command npx --arg=-y --arg @scope/server
cmdforge mcp add remote --transport streamable-http --url https://example.com/mcp cmdforge mcp add remote --transport streamable-http --url https://example.com/mcp
cmdforge mcp connect remote cmdforge mcp connect remote

64
RELEASING.md Normal file
View File

@ -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 <https://pypi.org/>.
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.

View File

@ -1,13 +1,14 @@
[build-system] [build-system]
requires = ["setuptools>=61.0", "wheel"] requires = ["setuptools>=77.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "cmdforge" name = "cmdforge"
version = "0.1.0" version = "0.2.0"
description = "Build custom AI-powered CLI commands in YAML" description = "Build, compose, and share AI-powered command-line tools you own"
readme = "README.md" readme = "PYPI_README.md"
license = {text = "MIT"} license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.10" requires-python = ">=3.10"
authors = [ authors = [
{name = "Rob"} {name = "Rob"}
@ -18,7 +19,6 @@ classifiers = [
"Environment :: Console", "Environment :: Console",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: System Administrators", "Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
"Operating System :: MacOS", "Operating System :: MacOS",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@ -45,6 +45,10 @@ dev = [
"pytest-cov>=4.0", "pytest-cov>=4.0",
"tomli>=1.1; python_version < '3.11'", "tomli>=1.1; python_version < '3.11'",
] ]
release = [
"build>=1.2",
"twine>=6.0",
]
registry = [ registry = [
"Flask>=2.3", "Flask>=2.3",
"argon2-cffi>=21.0", "argon2-cffi>=21.0",
@ -81,6 +85,7 @@ Homepage = "https://cmdforge.brrd.tech"
Documentation = "https://cmdforge.brrd.tech/docs" Documentation = "https://cmdforge.brrd.tech/docs"
Repository = "https://gitea.brrd.tech/rob/CmdForge.git" Repository = "https://gitea.brrd.tech/rob/CmdForge.git"
Issues = "https://gitea.brrd.tech/rob/CmdForge/issues" Issues = "https://gitea.brrd.tech/rob/CmdForge/issues"
Changelog = "https://gitea.brrd.tech/rob/CmdForge/src/branch/main/CHANGELOG.md"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]

View File

@ -1,3 +1,3 @@
"""CmdForge - A lightweight personal tool builder for AI-powered CLI commands.""" """CmdForge - A lightweight personal tool builder for AI-powered CLI commands."""
__version__ = "0.1.0" __version__ = "0.2.0"

View File

@ -451,9 +451,23 @@ def pick_args(tty_input: TTYInput, tool: dict) -> Optional[dict]:
def main(): def main():
"""Entry point for cf command.""" """Entry point for cf command."""
global _ui_out global _ui_out
import argparse
import subprocess import subprocess
import signal 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 # Handle Ctrl+C gracefully
def handle_sigint(sig, frame): def handle_sigint(sig, frame):
# Restore cursor and exit cleanly # Restore cursor and exit cleanly

View File

@ -17,20 +17,21 @@ install together; optional extras add MCP, interactive PTY providers, and the re
<h2 id="pipx">A Clean Personal Installation</h2> <h2 id="pipx">A Clean Personal Installation</h2>
<p><code>pipx</code> keeps CmdForge isolated while placing <code>cmdforge</code> and <code>cf</code> on <p><code>pipx</code> keeps CmdForge isolated while placing <code>cmdforge</code> and <code>cf</code> on
your path:</p> your path:</p>
<pre><code class="language-bash">pipx install 'cmdforge[all]' <pre><code class="language-bash">pipx install 'cmdforge[mcp,pty]'
cmdforge --version cmdforge --version
cmdforge --help</code></pre> cmdforge --help</code></pre>
<p>A normal virtual environment works just as well:</p> <p>A normal virtual environment works just as well:</p>
<pre><code class="language-bash">python3.10 -m venv .venv <pre><code class="language-bash">python3.10 -m venv .venv
. .venv/bin/activate . .venv/bin/activate
python -m pip install 'cmdforge[all]'</code></pre> python -m pip install 'cmdforge[mcp,pty]'</code></pre>
<h2 id="extras">Choose Only the Extras You Need</h2> <h2 id="extras">Choose Only the Extras You Need</h2>
<table class="w-full my-5"><thead class="bg-gray-100"><tr><th>Install</th><th>Adds</th></tr></thead><tbody> <table class="w-full my-5"><thead class="bg-gray-100"><tr><th>Install</th><th>Adds</th></tr></thead><tbody>
<tr class="border-b"><td><code>cmdforge</code></td><td>CLI, desktop GUI, contracts, registry client, and local tool runner</td></tr> <tr class="border-b"><td><code>cmdforge</code></td><td>CLI, desktop GUI, contracts, registry client, and local tool runner</td></tr>
<tr class="border-b"><td><code>cmdforge[mcp]</code></td><td>MCP client steps and MCP server integration</td></tr> <tr class="border-b"><td><code>cmdforge[mcp]</code></td><td>MCP client steps and MCP server integration</td></tr>
<tr class="border-b"><td><code>cmdforge[pty]</code></td><td>Interactive pseudo-terminal providers</td></tr> <tr class="border-b"><td><code>cmdforge[pty]</code></td><td>Interactive pseudo-terminal providers</td></tr>
<tr><td><code>cmdforge[all]</code></td><td>All optional runtime features, including the web stack</td></tr> <tr><td><code>cmdforge[mcp,pty]</code></td><td>The recommended personal installation with agent integration</td></tr>
<tr><td><code>cmdforge[all]</code></td><td>Every optional feature, including the registry web-server stack</td></tr>
</tbody></table> </tbody></table>
<h2 id="provider">Let CmdForge Inspect the Machine</h2> <h2 id="provider">Let CmdForge Inspect the Machine</h2>

View File

@ -1,5 +1,6 @@
"""Tests for packaging metadata.""" """Release metadata checks for the public Python distribution."""
from pathlib import Path
import sys import sys
import pytest import pytest
@ -9,12 +10,53 @@ if sys.version_info >= (3, 11):
else: else:
tomllib = pytest.importorskip("tomli") 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(): def test_web_templates_and_static_are_declared_as_package_data():
with open("pyproject.toml", "rb") as f: with (ROOT / "pyproject.toml").open("rb") as handle:
data = tomllib.load(f) data = tomllib.load(handle)
package_data = data["tool"]["setuptools"]["package-data"]["cmdforge.web"] package_data = data["tool"]["setuptools"]["package-data"]["cmdforge.web"]
assert "templates/**/*.html" in package_data assert "templates/**/*.html" in package_data
assert "static/**/*" in package_data assert "static/**/*" in package_data

View File

@ -1,10 +1,13 @@
"""Behavioral coverage for registry-aware picker and deprecation UX.""" """Behavioral coverage for registry-aware picker and deprecation UX."""
from io import StringIO from io import StringIO
import sys
import time import time
from unittest.mock import patch from unittest.mock import patch
import cmdforge.cli.picker as picker import cmdforge.cli.picker as picker
import pytest
from cmdforge import __version__
from cmdforge.cli.picker import PickerResult from cmdforge.cli.picker import PickerResult
@ -89,3 +92,19 @@ def test_local_deprecation_selection_prints_migration_guidance():
output = picker._ui_out.getvalue() output = picker._ui_out.getvalue()
assert "Moved." in output assert "Moved." in output
assert "official/new" 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

View File

@ -118,7 +118,7 @@ def test_installation_matches_runtime_and_current_provider_onboarding():
assert "Python 3.10" in installation assert "Python 3.10" in installation
assert "Python 3.8" not 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 installation
assert "cmdforge providers discover --add" in provider_setup assert "cmdforge providers discover --add" in provider_setup
assert "--type api" in provider_setup assert "--type api" in provider_setup