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).
Read next¶
installation · provider API · config schema · markers · grammar · insertions · insertion zones · testing