Skip to main content

typer-cli

Add or modify CLI commands in Oscilla using Typer. Use when: adding new CLI subcommands, wrapping async functions for CLI use, understanding the CLI entrypoint structure, or following the @syncify pattern for async commands.

ソース情報

リポジトリ
tedivm/oscilla
ソースの最終更新活動
2026年4月19日 17:28
検出された SKILL.md の言語
英語
スター
1
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
typer-cli
description
Add or modify CLI commands in Oscilla using Typer. Use when: adding new CLI subcommands, wrapping async functions for CLI use, understanding the CLI entrypoint structure, or following the @syncify pattern for async commands.
# Typer CLI > **context7**: If the `mcp_context7` tool is available, resolve and load the full `typer` documentation before making any changes to the CLI system: > ``` > mcp_context7_resolve-library-id: "typer" > mcp_context7_get-library-docs: <resolved-id> > ``` Any command or script that must be accessible to users must be exposed through the Typer library. For full developer documentation see [docs/dev/cli.md](../../docs/dev/cli.md). --- ## CLI Structure The CLI is registered in `pyproject.toml` as a project script: ```toml [project.scripts] oscilla = "oscilla.cli:app" ``` After installation, the `oscilla` command is available. The main app is defined in `oscilla/cli.py`. Domain-specific subcommand groups live in separate `oscilla/cli_<domain>.py` files and are mounted onto the main app. ### Current Command Tree ``` oscilla ├── game # Launch the interactive TUI game loop ├── validate # Validate game package manifests (also: oscilla validate) ├── version # Print installed version ├── hello # Friendly greeting (development helper) ├── data-path # Print the user data directory path ├── test-data # Install test/development data into the database └── content # Content authoring subcommands (oscilla/cli_content.py) ├── list # List all manifests of a given kind ├── show # Inspect one manifest with cross-references ├── graph # Visualize world/adventure/dependency graphs ├── trace # Trace all execution paths through an adventure ├── schema # Export JSON Schema for manifest kinds └── create # Scaffold a new manifest file ``` The `content` subapp is defined in `oscilla/cli_content.py` and mounted in `cli.py`: ```python from .cli_content import content_app app.add_typer(content_app, name="content") ``` --- ## Async Commands — `@syncify` Typer runs commands synchronously, but Oscilla uses async throughout (database access, HTTP calls, etc.). Use the `@syncify` decorator from `oscilla/cli.py` to bridge them: ```python from oscilla.cli import syncify @app.command() @syncify async def my_command(name: str) -> None: """This async function will run correctly from the CLI.""" result = await some_async_operation(name) typer.echo(result) ``` **Critical**: `@app.command()` must appear **before** `@syncify` in decorator order. Do **not** use `asyncio.run()` directly — `syncify` handles the event loop correctly. ### Database Access in CLI Commands Use `get_session` from `oscilla.services.db` for database-backed commands. Always use it as an async context manager: ```python from oscilla.services.db import get_session @app.command() @syncify async def my_db_command() -> None: """Example command using database access.""" async with get_session() as session: result = await session.execute(select(MyModel)) items = result.scalars().all() for item in items: typer.echo(item.name) ``` For commands that need database migrations applied first, call `migrate_database()` at the top of the command (see the `game` command as a reference). --- ## Parameter Pattern Use `Annotated[]` for all arguments and options — it keeps the signature clean and is the Typer-recommended style: ```python import typer from typing import Annotated app = typer.Typer() @app.command() def process( input_file: Annotated[str, typer.Argument(help="Path to the input file")], output_file: Annotated[str | None, typer.Option(help="Path to the output file")] = None, verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")] = False, ) -> None: """Process the input file and generate output.""" if verbose: typer.echo(f"Processing {input_file}...") typer.echo("Done!") @app.command() @syncify async def fetch( url: Annotated[str, typer.Argument(help="URL to fetch data from")], ) -> None: """Fetch data from a URL asynchronously.""" data = await fetch_url(url) typer.echo(data) ``` ### Arguments vs Options | Type | Typer class | CLI usage | | ------------------------ | ------------------ | -------------------------- | | Positional (required) | `typer.Argument()` | `oscilla my-cmd value` | | Named flag/option | `typer.Option()` | `oscilla my-cmd --flag x` | | Boolean flag pair | `typer.Option("--yes/--no")` | `oscilla my-cmd --yes` | ### Output Format Pattern Many existing commands support `--format text|json|yaml`. When adding a command that returns structured data, follow this pattern from `cli_content.py`: ```python output_format: Annotated[ str, typer.Option("--format", "-F", help="Output format: text | json | yaml."), ] = "text" ``` Use `_emit_structured_output(data, output_format)` (defined in `cli_content.py`) for JSON/YAML output, and Rich tables/panels for `text` output. --- ## Adding a New Command Group Create `oscilla/cli_<domain>.py`: ```python # oscilla/cli_reports.py import typer from typing import Annotated reports_app = typer.Typer( name="reports", help="Report generation commands.", no_args_is_help=True, ) @reports_app.command("generate") def generate_report( output: Annotated[str, typer.Argument(help="Output file path")], ) -> None: """Generate a report and write it to the output path.""" ... ``` Then mount it in `oscilla/cli.py`: ```python from .cli_reports import reports_app app.add_typer(reports_app, name="reports") ``` Use `no_args_is_help=True` on subapp `Typer()` instances so that `oscilla reports` without arguments shows help rather than an error. --- ## Output: Rich vs `typer.echo` - Use `typer.echo()` for simple single-line output. - Use `rich.console.Console` and Rich components (tables, panels) for formatted multi-line output — this is the pattern used throughout `cli_content.py`. - For error output, write to stderr: `Console(stderr=True)` or `typer.echo(..., err=True)`. - Exit with a non-zero code on failure: `raise SystemExit(1)`. --- ## Error Handling Pattern Follow the pattern used in existing commands: print a Rich-formatted error to stderr and raise `SystemExit(1)`: ```python from rich.console import Console _err_console = Console(stderr=True) @app.command() def my_command(name: str) -> None: """...""" item = lookup(name) if item is None: _err_console.print(f"[bold red]{name!r} not found.[/bold red]") raise SystemExit(1) typer.echo(item) ``` --- ## Style Checklist - [ ] New commands in `oscilla/cli.py` or `oscilla/cli_<domain>.py` - [ ] Async commands use `@syncify` (not `asyncio.run()`) - [ ] `@app.command()` decorator appears **before** `@syncify` - [ ] All parameters use `Annotated[type, typer.Argument/Option(...)]` - [ ] Every command has a docstring (used as `--help` text) - [ ] Subapp `Typer()` instances use `no_args_is_help=True` - [ ] New subapp mounted in `oscilla/cli.py` with `app.add_typer(...)` - [ ] Error output goes to stderr via `Console(stderr=True)` or `err=True` - [ ] Failures raise `SystemExit(1)` (not `sys.exit(1)` or exceptions)
GitHubで見る