Skip to content

repolish — LLM cheat sheet

Template-push + drift detection. Providers ship Jinja2 templates; projects run repolish apply. Never clobbers local state blindly — markers capture local values before render and put them back.

Commands

Command Does
repolish scaffold . Generate provider package (always start here)
repolish link Copy provider resources to .repolish/<alias>/
repolish apply Render + write files
repolish apply --check Same pipeline, report drift, write nothing
repolish preview f.yaml Debug one template's directives (pre-render only)
repolish lint Check provider templates against context model

New provider

uvx repolish scaffold . --package devkit_workspace   # flat
uvx repolish scaffold . --package devkit.workspace   # namespace (sibling providers)

Flat → devkit_workspace/repolish.py. Namespace → devkit/workspace/repolish.py. Namespace when team ships multiple providers; flat when standalone. Never hand-write the boilerplate — scaffold, then trim.

Provider API

from repolish import BaseContext, BaseInputs, Provider, TemplateMapping


class Ctx(BaseContext):
    python_version: str = '3.11'


class MyProvider(Provider[Ctx, BaseInputs]):
    def create_context(self) -> Ctx: ...                     # typed Jinja context
    def create_file_mappings(self, ctx) -> dict[str, TemplateMapping]: ...  # files it owns
    def promote_file_mappings(self, ctx): ...                # monorepo member mode (root-relative)
    def create_anchors(self, ctx) -> dict[str, str]: ...     # fills tag blocks
    def create_file_insertions(self, ctx): ...               # fn-filled blocks in any file
    def create_file_validators(self, ctx): ...               # inspect final files
    def create_default_symlinks(self): ...                   # root symlinks

All hooks optional. All public types import from repolish top level. Templates live in resources/templates/repolish/; path mirrors project destination.

repolish.yaml

providers:
  my-provider:
    provider_root: ./local/ # or: cli: my-provider-link
    overrides:
      context_merge: { python_version: '3.12' } # shallow, replaces top keys
      context_dotted: { tools.uv.version: '0.5' } # deep, one nested field
      anchors: { install-extras: 'pip install -e ".[dev]"' }

paused_files: [.github/workflows/ci.yml] # skip entirely

template_overrides:
  pyproject.toml: null # alias = pin file to provider; null = suppress

Deprecated (do not teach): top-level context:, context_overrides:, anchors: under a provider — use overrides.*.

Context order

Later wins: global repolish.repo.owner/name, repolish.year → per-provider create_context() → overrides.context_merge → overrides.context_dotted. Jinja2 StrictUndefined: undefined var = hard error.

Markers (template directives)

Grammar: repolish:<command>[tag] <payload> (whitespace after [tag]). Legacy repolish-<command>[tag]: <payload> still works but warns — removal in v2, never write it.

All directive lines are stripped from output. Default phase: pre-render; suffix |after-render inside the tag runs on rendered file ([name|after-render]).

Marker Use Rule
## repolish:start[n] … ## repolish:end[n] provider fills region (anchors) pre-render only; names are global — namespace them (docker-init)
## repolish:regex[n] <pattern> adopt one value from project file capture group = preserved value; pattern must also match template default; single-occurrence
## repolish:multiregex-block[n] + ## repolish:multiregex[n] merge key = "value" section provider owns keys, project owns values; local-only keys dropped; template needs [n] section header
## repolish:keep-block[n] start="..." end="..." (or end-regex=) preserve region in template-owned file directive directly above its region; repeated blocks pair first-to-first
## repolish:keep-rest[n] marker="..." preserve marker→EOF aliases: keep-the-rest, keep-footer
## repolish:keep-header[n] marker="..." preserve top→marker must be template's first line
## repolish:insert[n] start="..." end="..." provider-branded, function-filled zone tag n is the fn name by default (function="alias:fn" overrides); opening marker carries args (prefix match); dev arg edits survive; body always refilled; unknown/failing/disabled fn → template default stays; colon-only, no dash form

Insertions (file markers, not directives)

<!-- repolish:on:tag fn-name arg1 key=val -->
(filled by provider fn `fn-name` on every apply)
<!-- repolish:off:tag -->

Body always regenerated; never hand-edit inside. Dev edits to marker function/args survive re-apply (adoption). Works in developer files AND template-rendered files (markers pass through render, filled after write). No phase suffix ever.

Insertion zones (repolish:insert), the blended variant: provider declares the zone in the template with branded markers (<!-- generated:badges:on … -->), bodies fill from the file's function registry on apply, template default is the fallback for unknown/failing/disabled functions.

Debug workflow

repolish preview scratch.yaml — keys: template, target (simulated project file), config.anchors. Iterate directives there before a real apply. Add --show-patterns --show-steps -vv to see what matched.

Linking

repolish link → resources land in .repolish/<alias>/ (stable short path, like node_modules/pkg/config.yaml). Root symlinks: entries in repolish.yaml are absolute → gitignore them; fresh clones re-run repolish link.

Testing

from repolish.testing import ProviderTestBed, assert_snapshots, make_context

bed = ProviderTestBed(MyProvider, mode='root')   # or 'member'/'standalone'
rendered = bed.render_all()                       # {dest: content}
assert_snapshots(rendered, 'tests/snapshots')     # golden files, diff on fail

repolish does NOT

daemon/watch, manage Python envs, merge conflicts (use paused_files), support cookiecutter {{ cookiecutter.x }} (gone in v1).

installation · provider API · config schema · markers · grammar · insertions · insertion zones · testing