|
| 1 | +# Community Plugin Installer |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Titan supports installing plugins from user-provided git repositories, not just the official ones bundled with the CLI. Installation always uses `pipx inject` to keep the plugin isolated in Titan's own venv. |
| 6 | + |
| 7 | +Community plugins are tracked separately in `~/.titan/community_plugins.toml` (global, not per-project). |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Key Files |
| 12 | + |
| 13 | +| File | Role | |
| 14 | +|------|------| |
| 15 | +| `titan_cli/core/plugins/community.py` | All business logic: URL parsing, host detection, pyproject.toml fetch, pipx install/uninstall, tracking file I/O | |
| 16 | +| `titan_cli/ui/tui/screens/install_plugin_screen.py` | 4-step install wizard | |
| 17 | +| `titan_cli/ui/tui/screens/plugin_management.py` | Modified: install button (`i`), uninstall (`u`), `[community]` badge | |
| 18 | +| `titan_cli/ui/tui/widgets/wizard.py` | Shared wizard widgets: `StepStatus`, `WizardStep`, `StepIndicator` | |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## URL Format |
| 23 | + |
| 24 | +Users must always include an explicit version — bare URLs without `@version` are rejected: |
| 25 | + |
| 26 | +``` |
| 27 | +# Accepted |
| 28 | +https://github.com/user/titan-plugin-custom@v1.2.0 |
| 29 | +https://github.com/user/titan-plugin-custom@abc123def456 |
| 30 | +
|
| 31 | +# Rejected |
| 32 | +https://github.com/user/titan-plugin-custom |
| 33 | +``` |
| 34 | + |
| 35 | +Internally this becomes: `git+https://github.com/user/titan-plugin-custom.git@v1.2.0` |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Install Flow (4 steps) |
| 40 | + |
| 41 | +### 1. URL |
| 42 | +User enters `https://repo@version`. Validated with `validate_url()` before advancing. |
| 43 | + |
| 44 | +### 2. Preview |
| 45 | +Fetches `pyproject.toml` from the repo at that exact version and shows: |
| 46 | +- Package name, version, description, authors |
| 47 | +- Titan entry points registered (`[titan.plugins]`) |
| 48 | +- Python dependencies |
| 49 | + |
| 50 | +**Host detection** (`PluginHost` StrEnum): `GITHUB`, `GITLAB`, `BITBUCKET`, `UNKNOWN` |
| 51 | + |
| 52 | +Raw URL patterns per host: |
| 53 | +``` |
| 54 | +GitHub: https://raw.githubusercontent.com/{path}/{version}/pyproject.toml |
| 55 | +GitLab: https://gitlab.com/{path}/-/raw/{version}/pyproject.toml |
| 56 | +Bitbucket: https://bitbucket.org/{path}/raw/{version}/pyproject.toml |
| 57 | +Unknown: fetch skipped — warning shown, install still allowed |
| 58 | +``` |
| 59 | + |
| 60 | +Error cases (all allow proceeding): |
| 61 | +- HTTP 404 → URL or version not found |
| 62 | +- Network error → connection problem |
| 63 | +- No `[titan.plugins]` entry point → warns plugin won't be visible in Titan |
| 64 | +- Unparseable pyproject.toml → warns metadata unreadable |
| 65 | + |
| 66 | +A **security warning** is always shown regardless of outcome. |
| 67 | + |
| 68 | +### 3. Install |
| 69 | +Runs `pipx inject titan-cli git+<url>.git@<version>` as a subprocess (async, non-blocking). |
| 70 | + |
| 71 | +Requires Titan to be running inside a pipx environment (`is_running_in_pipx()`). If not, shows an error and blocks install. |
| 72 | + |
| 73 | +On success: |
| 74 | +1. Saves record to `~/.titan/community_plugins.toml` |
| 75 | +2. Calls `self.config.load()` → auto-reloads registry + re-initializes all plugins (no restart needed) |
| 76 | + |
| 77 | +On failure: shows pipx stderr + actionable suggestions. |
| 78 | + |
| 79 | +### 4. Done |
| 80 | +Success or failure summary. "Finish" dismisses the wizard. |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## Tracking File |
| 85 | + |
| 86 | +`~/.titan/community_plugins.toml` — global, one record per installed community plugin: |
| 87 | + |
| 88 | +```toml |
| 89 | +[[plugins]] |
| 90 | +repo_url = "https://github.com/user/titan-plugin-custom" |
| 91 | +version = "v1.2.0" |
| 92 | +package_name = "titan-plugin-custom" |
| 93 | +titan_plugin_name = "custom" |
| 94 | +installed_at = "2026-03-06T10:30:00+00:00" |
| 95 | +``` |
| 96 | + |
| 97 | +Key functions in `community.py`: |
| 98 | +- `load_community_plugins()` → `list[CommunityPluginRecord]` |
| 99 | +- `save_community_plugin(record)` → appends to file |
| 100 | +- `remove_community_plugin(package_name)` → removes by package name |
| 101 | +- `get_community_plugin_names()` → `set[str]` of titan_plugin_names (used for `[community]` badge) |
| 102 | +- `get_community_plugin_by_titan_name(name)` → `Optional[CommunityPluginRecord]` |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Uninstall Flow |
| 107 | + |
| 108 | +In Plugin Management, pressing `u` (or clicking "Uninstall") on a community plugin: |
| 109 | +1. Runs `pipx runpip titan-cli uninstall -y <package_name>` |
| 110 | +2. Calls `remove_community_plugin(package_name)` |
| 111 | +3. Calls `self.config.load()` to reload registry |
| 112 | +4. Refreshes the plugin list |
| 113 | + |
| 114 | +Only community plugins show the Uninstall button/keybinding. |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## Wizard Widgets (`titan_cli/ui/tui/widgets/wizard.py`) |
| 119 | + |
| 120 | +Shared by `install_plugin_screen.py` and `plugin_config_wizard.py`: |
| 121 | + |
| 122 | +```python |
| 123 | +class StepStatus(StrEnum): |
| 124 | + PENDING = "pending" |
| 125 | + IN_PROGRESS = "in_progress" |
| 126 | + COMPLETED = "completed" |
| 127 | + |
| 128 | +@dataclass |
| 129 | +class WizardStep: |
| 130 | + id: str |
| 131 | + title: str |
| 132 | + |
| 133 | +class StepIndicator(Static): |
| 134 | + def __init__(self, step_number: int, step: WizardStep, status: StepStatus): ... |
| 135 | +``` |
| 136 | + |
| 137 | +Exported from `titan_cli/ui/tui/widgets/__init__.py`. |
| 138 | + |
| 139 | +--- |
| 140 | + |
| 141 | +## Known Technical Debt |
| 142 | + |
| 143 | +`plugin_config_wizard.py` still uses plain dicts (`{"id": ..., "title": ...}`) for its steps instead of `WizardStep`. It wraps them with `WizardStep(id=step["id"], title=step["title"])` when calling `StepIndicator`. Full refactor is pending a future PR. |
0 commit comments