Skip to content

CLI

provider_cli builds a cyclopts app with one subcommand per lane plus an all subcommand that runs the provider's full pass (still fast: single provider load). New lanes need no pyproject.toml edits because the subcommands are generated from the hook.

That generated CLI is the feature worth noticing. A provider author does not need a second package, separate parser setup, or hand-maintained subcommand table just to expose useful project commands. The provider already owns the templates, knows the standards, and has a place to declare lane-specific context. provider_cli turns that into a real project tool with almost no extra surface area.

Each subcommand takes the apply flags: --config, --check, --skip-post-process, --fail-on-warnings, and -v. The flag set lives in the parameter model the repolish apply command itself uses, so a flag added there shows up on lane subcommands too; the two CLIs cannot drift apart.

The CLI runs in any project, registered or not. When a repolish.yaml exists the lane run reads it, but only for the keys a lane run needs (the table below); provider loading, registration, and link commands never run, so no repolish link is ever required. Without a config file, or when the config does not list the provider, the run is built in memory around the package the CLI is running from.

The provider's identity in repolish.provider.alias is provider-owned: pass alias= to provider_cli and that name is used everywhere, in rendered headers and logged events alike, whether or not the config lists it. Without an explicit alias, a config entry whose provider_root points at the package's resources/templates directory names the run. A run with no match falls back to the package name (the directory holding resources/): fine for bookkeeping, but it changes if the package is renamed, so alias= is the stable choice for headers. An alias like kitkat-databricks-cli is entirely up to the provider developer; repolish never invents one.

What a lane run reads from the config

key honored notes
paused_files yes applies to lane dests and copies
fast_lanes.resolutions yes coupled and decoupled lanes alike
fast_lanes.config[<lane>].post_process yes replaces project post_process in named lane runs
own entry fields yes symlinks, overrides, resources_dir; the located root wins
template_overrides yes keeps render parity
project post_process all only named lanes run their own commands, or none
other providers no never loaded, imported, or registered
delete_files no lanes never delete

Wiring the CLI into a provider

repolish scaffold generates this wiring for new providers. An established provider adds it by hand once: one small module, one script entry, one reinstall.

  1. Create mylib/repolish/cli.py next to linker.py, exporting main:
from repolish.fastlane import provider_cli

from mylib.repolish import MyProvider

main = provider_cli(MyProvider)
  1. Register it as a console script in pyproject.toml, next to the linker entry:
[project.scripts]
mylib-link = 'mylib.repolish.linker:main'
mylib-cli = 'mylib.repolish.cli:main'
  1. Reinstall the package the way the environment normally gets its console scripts (uv pip install -e ., uv sync, or the equivalent) so mylib-cli lands on PATH.

After that the CLI never needs touching again: subcommands come from create_fast_lanes, so adding a lane later is a provider-code change only.

This is what makes lanes attractive even when the lane is not about "apply but faster". A team can expose component generators, documentation updaters, workflow bootstrappers, or one-off maintenance commands through the same provider package they already ship for repolish.

Local providers

A project-local provider has no resources/templates for discovery to walk to: its provider_root holds repolish.py and the repolish/ template tree directly. State that root and discovery is skipped entirely:

from pathlib import Path

from repolish.fastlane import provider_cli

from local_provider.provider import LocalProvider

main = provider_cli(LocalProvider, provider_root=Path('internal/templates'))

A stated root resolves against the working directory, like --config, and gets the same semantics the config gives a provider_root entry: resources_dir defaults to the root itself, and the root's directory name is the bookkeeping fallback identity. Identity still works through the config: the entry whose provider_root points at the same directory names the run, so alias= is only needed when the project has no config entry.

repolish scaffold --local writes this wiring for you. The CLI lives inside the provider package (internal/local_provider/cli.py), where the pyproject exposes it as the local-cli console script:

$ local-cli all

The --flat tier puts cli.py beside the provider root (internal/cli.py) instead and loads repolish.py from the stated root. That file sits outside the root on purpose: running a file as a script puts its directory first on sys.path, where the provider's repolish.py would shadow the repolish package itself. Run it from the project root:

$ python internal/cli.py all

Trying a lane out

The smallest possible lane needs one template, one spec entry, and nothing else. Add a template that lives entirely off extra_context, so it satisfies the self-containment rule by construction:

{{ message }}

The template lives at mylib/resources/templates/repolish/hello.txt.jinja, alongside the provider's other templates. Declare the lane on the provider:

def create_fast_lanes(self, repolish):
    return {
        'hello': FastLaneSpec(
            file_mappings={
                'hello.txt': TemplateMapping(
                    'hello.txt.jinja',
                    extra_context={'message': 'hi from the lane'},
                ),
            },
        ),
    }

From any project directory with the provider installed (no registration, link, or config change needed; the new template is picked up from disk):

$ mylib-cli hello # writes hello.txt into the project
$ mylib-cli hello --check # exit 0: the lane output is stable
$ repolish apply --check # no drift on hello.txt: the full run agrees

If more than one config entry points at the same resources/templates directory, the CLI resolves the first match; pass alias= in cli.py to pin the provider's identity.