Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/hooks/format-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
file="$CLAUDE_FILE_PATH"
if [ -n "$file" ] && echo "$file" | grep -q '\.py$'; then
cd /Users/jremitz/workspace/reeln-cli
.venv/bin/ruff format "$file" 2>/dev/null
.venv/bin/ruff check --fix "$file" 2>/dev/null
remaining=$(.venv/bin/ruff check "$file" 2>&1 | grep -v '^All checks passed')
if [ -n "$remaining" ]; then
echo "LINT ISSUES (fix before commit):" >&2
echo "$remaining" >&2
fi
fi
exit 0
9 changes: 9 additions & 0 deletions .claude/hooks/lint-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
cd /Users/jremitz/workspace/reeln-cli
result=$(.venv/bin/ruff check reeln/ tests/ 2>&1 | grep -v '^All checks passed')
if [ -n "$result" ]; then
echo "LINT FAILURES - fix before committing:" >&2
echo "$result" >&2
exit 1
fi
exit 0
86 changes: 85 additions & 1 deletion .claude/rules/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,88 @@ def on_game_init(self, context: HookContext) -> None:
- **Tests:** use `tmp_path` for file I/O, mock external API clients
- **Package naming:** `reeln-plugin-<name>` (PyPI), `reeln_<name>_plugin` (Python package)
- **Entry point:** `reeln.plugins` group, plugin name as key
- **No CLI arg registration:** plugins do not register CLI arguments. Use feature flags in plugin config (`smart_zoom_enabled`, etc.). Core CLI flags (`--smart`) trigger hooks; plugins decide behavior via their own config
- **Input contributions:** plugins declare additional user inputs via `input_schema: PluginInputSchema` class attribute. These are prompted interactively or passed via `--plugin-input KEY=VALUE`. See "Plugin Input Contributions" section below
- **No direct CLI arg registration:** plugins do not register CLI arguments directly. Use `input_schema` for user inputs and feature flags in plugin config for capability toggles. Core CLI flags (`--smart`) trigger hooks; plugins decide behavior via their own config

## Plugin Input Contributions

Plugins declare additional user inputs via `input_schema`, a class attribute of type `PluginInputSchema`. Each `InputField` is scoped to a CLI command and collected automatically.

### Declaration (plugin class)

```python
from reeln.models.plugin_input import InputField, PluginInputSchema

class GooglePlugin:
input_schema = PluginInputSchema(fields=(
InputField(
id="thumbnail_image",
label="Thumbnail Image",
field_type="file",
command="game_init",
plugin_name="google",
required=False,
description="Thumbnail image for YouTube livestream",
),
))
```

### Declaration (registry JSON)

For reeln-dock to render UI fields without loading Python code:

```json
{
"ui_contributions": {
"input_fields": {
"game_init": [
{
"id": "thumbnail_image",
"label": "Thumbnail Image",
"type": "file",
"required": false,
"description": "Thumbnail image for YouTube livestream"
}
]
}
}
}
```

### How inputs are collected

- **Interactive mode:** prompted via questionary after core prompts
- **Non-interactive mode:** passed via `--plugin-input KEY=VALUE` (repeatable, aliased `-I`)
- **reeln-dock:** rendered as form fields based on registry `input_fields`

### How plugins read collected inputs

Inputs are passed via `HookContext.data["plugin_inputs"]`:

```python
def on_game_init(self, context: HookContext) -> None:
inputs = context.data.get("plugin_inputs", {})
thumbnail = inputs.get("thumbnail_image")
```

### Valid command scopes

`game_init`, `game_finish`, `game_segment`, `render_short`, `render_preview`

### Valid field types

`str`, `int`, `float`, `bool`, `file`, `select`

### Conflict resolution

When two plugins declare the same `id` for the same command:
- **Same type:** first-registered wins (plugin load order), duplicate logged
- **Different type:** second field namespaced as `{plugin_name}.{id}`, warning logged

### Introspection

```bash
reeln plugins inputs # all registered fields
reeln plugins inputs --command game_init # filter by command
reeln plugins inputs --json # JSON output for tooling
```
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.0.37] - 2026-04-03

### Added
- `reeln game list` — list games in output directory with status badges (finished/in progress)
- `reeln game info` — show detailed game information (teams, venue, progress, livestreams)
- `reeln game delete` — delete a game directory with confirmation prompt (`--force` to skip)
- Colored CLI output: `--version`, `plugins list`, `plugins search`, `plugins info`, `plugins inputs`, `game list`, `game info`, `game delete` all use consistent styled output (bold names, green/red/yellow badges, dim labels)
- Shared `reeln.commands.style` module for consistent CLI formatting
- Plugin Input Contributions: plugins declare additional user inputs via `input_schema` class attribute (`PluginInputSchema`, `InputField`)
- `--plugin-input KEY=VALUE` (`-I`) repeatable option on `game init`, `render short`, and `render preview`
- Interactive prompts for plugin-contributed inputs (questionary-based, preset-first pattern)
- Conditional prompting: `thumbnail` and `tournament` only prompted when a plugin declares them
- `InputCollector` with conflict resolution (same-type dedup, cross-type namespacing)
- `get_input_schema()` method support: plugins can conditionally declare inputs based on feature flags (e.g., only prompt for thumbnail when `create_livestream` is enabled)
- Registry fallback: plugins without `input_schema` / `get_input_schema()` get inputs from `ui_contributions.input_fields` in registry JSON
- `reeln plugins inputs` introspection command (text + JSON output for reeln-dock)
- `input_contributions` field on `RegistryEntry` model, parsed from `ui_contributions.input_fields`
- Google plugin registry entry updated with `thumbnail_image` input field for `game_init`

### Changed
- `init_game()` accepts `plugin_inputs` kwarg, included in `ON_GAME_INIT` / `ON_GAME_READY` hook data
- `PRE_RENDER` / `POST_RENDER` hook data includes `plugin_inputs` when present
- `activate_plugins()` now registers plugin input schemas with the `InputCollector` singleton
- `game init` checks for unfinished games **before** interactive prompts (fail fast instead of prompting then failing)
- `_resolve_game_dir` now sorts by `created_at` from `game.json` instead of filesystem mtime (which is unreliable due to Spotlight/Time Machine)
- `_resolve_game_dir` prefers unfinished games over finished ones, so `reeln game finish` finds the right game

## [0.0.36] - 2026-04-02

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion reeln/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from __future__ import annotations

__version__ = "0.0.36"
__version__ = "0.0.37"
70 changes: 33 additions & 37 deletions reeln/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,41 @@
app.add_typer(hooks_cmd.app, name="hooks")


def _build_version_lines() -> list[str]:
"""Build version output lines: reeln version, ffmpeg info, plugins."""
lines: list[str] = [f"reeln {__version__}"]

# FFmpeg info
try:
from reeln.core.ffmpeg import check_version, discover_ffmpeg

ffmpeg_path = discover_ffmpeg()
version = check_version(ffmpeg_path)
lines.append(f"ffmpeg {version} ({ffmpeg_path})")
except Exception:
lines.append("ffmpeg: not found")

# Plugin versions
try:
from reeln.core.plugin_registry import get_installed_version
from reeln.plugins.loader import discover_plugins

plugins = discover_plugins()
plugin_lines: list[str] = []
for p in plugins:
if p.package:
ver = get_installed_version(p.package)
if ver:
plugin_lines.append(f" {p.name} {ver}")
if plugin_lines:
lines.append("plugins:")
lines.extend(plugin_lines)
except Exception:
pass

return lines


def _version_callback(value: bool) -> None:
if value:
for line in _build_version_lines():
typer.echo(line)
from reeln.commands.style import bold, error, label, success

typer.echo(f" {bold('reeln')} {success(__version__)}")

# FFmpeg info
try:
from reeln.core.ffmpeg import check_version, discover_ffmpeg

ffmpeg_path = discover_ffmpeg()
version = check_version(ffmpeg_path)
typer.echo(f" {bold('ffmpeg')} {version} {label(str(ffmpeg_path))}")
except Exception:
typer.echo(f" {bold('ffmpeg')} {error('not found')}")

# Plugin versions
try:
from reeln.core.plugin_registry import get_installed_version
from reeln.plugins.loader import discover_plugins

plugins = discover_plugins()
plugin_lines: list[str] = []
for p in plugins:
if p.package:
ver = get_installed_version(p.package)
if ver:
plugin_lines.append(f" {p.name} {label(ver)}")
if plugin_lines:
typer.echo(f" {bold('plugins:')}")
for pl in plugin_lines:
typer.echo(pl)
except Exception:
pass

raise typer.Exit()


Expand Down
Loading
Loading