repomatic.tooling package

The pinned-tool layer.

The registry of external tools, the runner resolving and invoking them, the bundled data files, and the plugin settings writer.

Submodules

repomatic.tooling.bundle module

Raw access to the data files bundled in repomatic/data/.

The lowest layer of bundled-data access, deliberately dependency-free so any module can read a data file without import cycles. Policy layers sit above: repomatic.init_project.export_content validates names against the exportable-file registry, and repomatic.tooling.tool_runner resolves tool configs.

repomatic.tooling.bundle.get_data_content(filename: str) str[source]

Get the content of a bundled data file.

This is the low-level function for reading any file from repomatic/data/.

Memoized for the process: bundled data is immutable for the life of an installed release, and the workflow lint re-reads the same canonical workflow per downstream file it checks.

Parameters:

filename (str) – Name of the file to retrieve (e.g., “labels.toml”).

Return type:

str

Returns:

Content of the file as a string.

Raises:

FileNotFoundError – If the file doesn’t exist.

repomatic.tooling.bundle.get_data_file_path(filename)[source]

Yield the filesystem path of a bundled data file.

Unlike get_data_content() which returns string content, this yields a Path suitable for passing to external tools via --config <path>. The path is valid only within the context manager.

Return type:

Iterator[Path]

repomatic.tooling.plugin module

Distribution of the bundled skills and agents as a Claude Code plugin.

Two halves of the same story, kept together because they share the plugin’s identity constants:

  • pack_plugin() assembles the zip the release engine attaches to every GitHub release, from the manifest and asset directories already in the tree.

  • merge_plugin_settings() writes the marketplace and enablement wiring into a consumer’s Claude Code settings, so a downstream repository can install the plugin instead of carrying copied skill files.

Caution

pack_plugin() relocates each asset into the spec’s default skills/ and agents/ directories, rather than mirroring the .claude/ layout it reads them from, and the manifest therefore declares no component paths at all.

That asymmetry is not a stylistic choice. A manifest naming individual agent files ("agents": ["./.claude/agents/qa-engineer.md", ...], the only form the published schema accepts, since it constrains the field to paths ending in .md) passes claude plugin validate –strict and then loads zero agents at runtime, silently. Naming the directory instead fails validation outright. The default location is the only shape that actually works, verified against Claude Code 2.1.220 by loading the packed archive and counting components with claude plugin details. skills does honor a custom directory, but there is no reason to keep one half on the mechanism that misbehaves, so both travel to their defaults and the manifest stays metadata-only.

Note

.claude/skills/ and .claude/agents/ remain the single source of truth: the relocation happens only inside the archive, so there is no symlink anywhere and no second copy of any skill in the tree. The trade-off is that the repository root is not itself an installable plugin: test a change by packing it and pointing claude --plugin-dir at the unpacked archive.

Note

The checked-in manifest carries no version: pack_plugin() injects the running __version__ into the copy it writes to the archive. Claude Code compares that string against a user’s installed copy to decide whether an update is due, so a hand-maintained value that went stale would silently strand everyone on the plugin they already had. Deriving it at pack time makes it impossible to forget, and keeps the one repomatic-specific [[tool.bumpversion.files]] entry out of a [tool.bumpversion] block that sync-bumpversion regenerates from a bundled template shared with every downstream repository.

Note

The marketplace entry is an archive source pointing at the release asset, and its URL ratchets forward: PrepareRelease.freeze_marketplace_archive_url() rewrites it to /releases/download/v{X.Y.Z}/ on each release commit, and nothing walks it back. So the default branch always names the newest published release, and a catalog added at a tag installs that tag’s plugin. The URL is never a latest redirect except before the very first release, and never a .devN tag.

Caution

The entry still carries no sha256. The archive is byte-deterministic, so a digest could in principle be committed alongside the pin, but only if the release runner reproduces those bytes exactly: ZIP_DEFLATED output depends on the zlib build behind CPython, and a one-byte difference would fail every install with Plugin archive integrity check failed rather than degrading. Integrity comes from the attestation the engine’s extra-assets job generates instead. Switching to ZIP_STORED would make a committed digest safe, at the cost of a larger asset.

Independently of that: a release that publishes without this asset breaks /plugin install until the next one, which is why a failed extra-assets now blocks publish-release.

repomatic.tooling.plugin.MANIFEST_PATH = '.claude-plugin/plugin.json'

Location of the plugin manifest.

The same path in both places it appears: relative to the repository root, where pack_plugin() reads it, and relative to the plugin root inside the archive, where Claude Code looks for it.

repomatic.tooling.plugin.MARKETPLACE_PATH = '.claude-plugin/marketplace.json'

Location of the marketplace catalog, relative to the repository root.

repomatic.tooling.plugin.PLUGIN_NAME = 'repomatic'

The plugin’s name, which namespaces every skill and agent it ships.

Users type it as /plugin install repomatic@kdeldycke and see it in the scoped component names (repomatic:qa-engineer). Renaming it breaks every existing install, so it lives here as a constant and is asserted against the manifest rather than read from it.

repomatic.tooling.plugin.MARKETPLACE_NAME = 'kdeldycke'

The marketplace’s name, the catalog this plugin is published in.

Named after the owner rather than the project, so sibling repositories can be listed in the same catalog later. Like PLUGIN_NAME, renaming it breaks every existing install.

repomatic.tooling.plugin.MARKETPLACE_REPO = 'kdeldycke/repomatic'

Repository a consumer registers to reach MARKETPLACE_PATH.

repomatic.tooling.plugin.BIOME_DEFAULT_INDENT: Final[str] = '\t'

Indent format-json writes when no Biome configuration overrides it.

Biome’s own default, so a repository declaring nothing gets a rendered document the formatter already agrees with.

repomatic.tooling.plugin.BIOME_DEFAULT_INDENT_WIDTH: Final[int] = 2

Spaces per level Biome assumes when a config asks for spaces without a width.

repomatic.tooling.plugin.ARCHIVE_NAME = 'repomatic-claude-plugin.zip'

Filename of the release asset pack_plugin() produces.

Carries claude because a bare repomatic-plugin.zip reads backwards: packaging names an extension after its host first (pytest-cov, mdformat-gfm), so that filename announces a plugin for repomatic on a release page, which is also what “plugin” means for the mdformat entries of tool_registry. The name mirrors the spec’s own .claude-plugin/ directory instead.

Also the default --output of repomatic pack-plugin, so the release job never spells it. It still appears in [tool.repomatic] release-assets and in the release-asset- run-artifact name the engine matches, which TOML and YAML cannot read from here; tests/test_workflows.py holds all three equal. freeze_marketplace_archive_url() rewrites the marketplace URL’s trailing filename from here too, so a rename reaches every consumer through one constant.

repomatic.tooling.plugin.ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)

Fixed modification time stamped on every archive member.

The earliest timestamp the ZIP format can represent. Together with a sorted member list and an explicit file mode, it makes pack_plugin() byte-deterministic, so re-packing an unchanged tree yields an identical archive. That matters more here than it usually would: with no sha256 pin in the marketplace entry, the archive’s own digest is what Claude Code falls back to for change detection.

repomatic.tooling.plugin.FILE_MODE = 420

Permission bits stamped on every archive member.

Stamped explicitly rather than copied from disk so the archive does not vary with the packing runner’s umask.

repomatic.tooling.plugin.AGENTS_DIR = 'agents'

Directory the plugin spec scans for agent definitions, inside the plugin root.

repomatic.tooling.plugin.SKILLS_DIR = 'skills'

Directory the plugin spec scans for skill folders, inside the plugin root.

repomatic.tooling.plugin.pack_plugin(repo_root, output, version='7.14.0.dev0')[source]

Pack the manifest and its assets into an installable plugin archive.

The archive holds a single top-level folder named after the plugin. That is one of the two layouts Claude Code accepts, and the one that makes unzip && claude –plugin-dir repomatic work on the downloaded asset. Inside it, assets sit at the spec’s default locations rather than the .claude/ paths they are read from: see the module docstring for why.

Parameters:
  • repo_root (Path) – Repository root the assets are read from.

  • output (Path) – Destination .zip path. Parent directories are created.

  • version (str) – Version stamped into the packaged manifest.

Return type:

list[str]

Returns:

Archive member names, sorted.

Raises:
  • FileNotFoundError – If the manifest, an agent file or a skill folder is missing.

  • TypeError – If the manifest is not a JSON object.

repomatic.tooling.plugin.render_plugin_settings(existing='', indent='\\t')[source]

Merge the plugin wiring into an existing settings document.

Only the two keys _plugin_settings() owns are touched, and within them only the entries this plugin and marketplace are named by: a repository’s own permissions, hooks and any unrelated marketplace survive untouched.

Sorted keys, and indent whichever way format-json writes JSON in the consuming repository, so writing the file leaves no drift for the formatter to raise a pull request about. Biome preserves key order, which is why only the indent has to be negotiated.

Parameters:
  • existing (str) – Current file content, or an empty string when absent.

  • indent (str) – One level of indentation, from _biome_json_indent().

Return type:

str

Returns:

The merged document, newline-terminated.

Raises:

TypeError – If existing is not a JSON object.

repomatic.tooling.plugin.merge_plugin_settings(target, root=None)[source]

Write the plugin wiring into target, creating the file if absent.

Idempotent: re-running against an already-wired document rewrites nothing and returns False, so repomatic init reports it as unchanged. That holds only while the rendered indent matches the repository’s own, which is why root is read rather than assumed: a repository declaring spaces would otherwise see this and format-json rewrite the file past each other on every run, each opening a pull request undoing the other’s.

Parameters:
  • target (Path) – Path to the Claude Code settings file to update.

  • root (Path | None) – Repository root whose Biome config sets the indent. Defaults to target’s own directory, which is right only for a root-level file.

Return type:

bool

Returns:

Whether the file was created or modified.

repomatic.tooling.tool_registry module

Declarative registry of the external tools repomatic run manages.

Each ToolSpec entry pins a tool’s version and, for binary-distributed tools, its per-platform download URLs and SHA-256 digests in CHECKSUMS; the paired VERSIONS map records the version each checksum set was computed for. The ArchiveFormat, NativeFormat, BinarySpec, and NpmSpec types describe how each tool is fetched and how its [tool.X] section is translated to the tool’s native config format. The repomatic run engine in tool_runner.py consumes this data to install and invoke each tool.

Note

sync-tool-versions and update-checksums rewrite this module’s version=, VERSIONS, and CHECKSUMS literals in place by string substitution, so their formatting must stay stable. The lint, autofix, and docs workflows key their tool caches on a hash of this file, so only a genuine version or checksum bump invalidates a cached tool download.

Todo

Drop the YAML-block fixup in the mdformat post-process callback once executablebooks/mdformat-myst#49 merges upstream. See _DIRECTIVE_YAML_OPTIONS_RE for the shape it repairs.

Todo

Drop the colon-fence fixup in the same callback once mdformat-myst ships colon-fence support, via either executablebooks/mdformat-myst#36 or executablebooks/mdformat-myst#48. See _ESCAPED_COLON_FENCE_RE for the shape it repairs.

exception repomatic.tooling.tool_registry.UnsupportedPlatformError[source]

Bases: RuntimeError

Raised when a tool publishes no binary for the running platform.

Distinguished from every other install failure (a failed download, a checksum mismatch) because it is a property of the tool’s release matrix rather than a fault: nothing about the current run can make the binary exist. Asking for such a tool directly is still fatal, but a caller provisioning it as a companion can catch this alone and carry on without it. See repomatic.tooling.tool_runner._path_tools_env().

repomatic.tooling.tool_registry.GENERATED_HEADER_TEMPLATE = 'Generated by {command} v{version} - https://github.com/kdeldycke/repomatic'

Template for the first line of generated-file headers.

Used by both CLI commands (e.g. sync-mailmap) and the tool runner (e.g. run shfmt) to stamp files with provenance. Format fields: command (full command path) and version (package version).

repomatic.tooling.tool_registry.generated_header(command, comment_prefix='# ')[source]

Return a generated-by header block with timestamp.

Parameters:
  • command (str) – Full command path (e.g. repomatic sync-mailmap).

  • comment_prefix (str) – Comment prefix for the target format.

Return type:

str

class repomatic.tooling.tool_registry.ArchiveFormat(*values)[source]

Bases: Enum

Archive format for binary tool downloads.

RAW = 'raw'
TAR_GZ = 'tar.gz'
TAR_XZ = 'tar.xz'
ZIP = 'zip'
tarfile_mode()[source]

Return the tarfile.open mode string for this format.

Raises:

ValueError – If called on a non-tar format.

Return type:

Literal['r:gz', 'r:xz']

class repomatic.tooling.tool_registry.NativeFormat(*values)[source]

Bases: Enum

Target format for [tool.X] translation.

YAML = 'yaml'
TOML = 'toml'
JSON = 'json'
EDITORCONFIG = 'editorconfig'
FLAGS = 'flags'
serialize(data, tool_name='')[source]

Serialize a config dict to this format’s string representation.

When data is a live [tool.X] table parsed from pyproject.toml (a tomlrt.Table), the TOML branch keeps the user’s comments by reparenting the section to the document root; see _reroot_section. A plain dict carries no trivia, so it is rendered as-is. The other formats (YAML, JSON, editorconfig) cannot carry TOML comments across the format boundary, so they serialize the values only.

Parameters:
  • data (dict) – Configuration dictionary to serialize.

  • tool_name (str) – Tool name for the generated-by header comment.

Raises:

ValueError – For FLAGS, which is not a file format.

Return type:

str

repomatic.tooling.tool_registry.PlatformKey

A (platform_or_group, architecture) pair used as binary lookup key.

The platform element can be a single Platform (like MACOS) or a Group (like LINUX, which matches any Linux distribution). The architecture is always a concrete Architecture.

Resolution order in BinarySpec.resolve_platform():

  1. Exact Platform match (current_platform() == key_platform).

  2. Group membership (current_platform() in key_group), preferring the group with fewest members (most specific).

  3. The LINUX family, only when current_platform() is UNKNOWN_PLATFORM, so a distribution extra-platforms cannot name still reaches a family-wide key.

alias of tuple[Platform | Group, Architecture]

class repomatic.tooling.tool_registry.ToolBackend(short_label, long_label)[source]

Bases: Enum

How a registry tool is delivered and executed.

Each member carries the display labels the documentation generators render, so backends and their vocabulary live in one place: adding a backend means adding a member here and a branch in ToolSpec.backend(), and every consumer (docs tables, version-sync candidate sources) follows.

Note

Code that dereferences a backend’s payload still tests the field directly (spec.binary is not None narrows the optional for mypy in a way an enum comparison cannot); this enum serves the sites that only need to know which backend, not its payload.

BINARY = ('Binary', 'Binary (downloaded from GitHub Releases)')
NPM = ('npm', 'npm registry, run via `node_modules/.bin`')
VENV = ('PyPI (venv)', 'PyPI, runs in project virtualenv via `uv run`')
UVX = ('PyPI', 'PyPI, installed via `uvx`')
short_label

Cell text for the docs summary table.

long_label

Installation-method line in the per-tool reference sections.

class repomatic.tooling.tool_registry.BinarySpec(urls, checksums, archive_format, archive_executable=None, strip_components=0)[source]

Bases: object

Platform-specific binary download specification.

Keys are PlatformKey tuples pairing an extra-platforms Platform or Group with an Architecture. This lets callers use broad groups (LINUX matches any distro) or specific platforms (DEBIAN) with full detection heuristics from extra-platforms.

Hint

Structural integrity checks (key types, checksum format, URL placeholders, strip_components consistency) are enforced in test_tool_spec_integrity.

Todo

Move those integrity checks to __post_init__ if the registry ever becomes user-configurable: a test only covers the specs shipped here.

urls: dict[tuple[Platform | Group, Architecture], str]

Platform key to URL template mapping. URLs use {version} placeholders.

checksums: dict[tuple[Platform | Group, Architecture], str]

Platform key to SHA-256 hex digest mapping.

archive_format: ArchiveFormat | dict[tuple[Platform | Group, Architecture] | Platform | Group, ArchiveFormat]

Archive format of the downloaded file.

A single ArchiveFormat applies to every platform. A dict maps platform specifiers to formats, allowing mixed archives in one spec:

archive_format={ALL_PLATFORMS: ArchiveFormat.TAR_GZ, WINDOWS: ArchiveFormat.ZIP}

Dict keys follow the same resolution as resolve_platform(): exact PlatformKey tuple first, then bare Platform equality, then Group membership (smallest group wins).

archive_executable: str | None = None

Path of the executable inside the archive. None defaults to the tool name. For RAW format, used as the final filename.

strip_components: int | dict[tuple[Platform | Group, Architecture] | Platform | Group, int] = 0

Number of leading path components to strip when extracting.

A single int applies to every platform. A dict maps platform specifiers to counts, using the same resolution as get_archive_format(), for a project whose archives are not laid out identically across platforms:

strip_components={ALL_PLATFORMS: 1, WINDOWS: 0}

gh is the motivating case: its Linux and macOS archives nest everything under a gh_{version}_{platform}_{arch}/ directory, while the Windows zip puts bin/gh.exe at the root. The nesting cannot be absorbed by archive_executable instead, since that is one string for all platforms and the directory name carries the version and platform.

resolve_platform()[source]

Match the current environment against registered platform keys.

Uses current_platform() and current_architecture() from extra-platforms, inheriting its full detection heuristics, then falls back to the LINUX family when those heuristics name no distribution at all.

Return type:

tuple[Platform | Group, Architecture]

Returns:

The matching PlatformKey.

Raises:

UnsupportedPlatformError – If no key matches the current environment.

get_archive_format(key)[source]

Return the archive format for the given platform key.

When archive_format is a single ArchiveFormat, returns it directly. When it is a dict, resolves through _resolve_per_platform().

Return type:

ArchiveFormat

get_strip_components(key)[source]

Return the leading path components to strip for a platform key.

When strip_components is a plain int, returns it directly. When it is a dict, resolves through _resolve_per_platform().

Return type:

int

static platform_cache_key(key)[source]

Derive a filesystem-safe cache path segment from a platform key.

Return type:

str

Returns:

A string like linux-aarch64 or macos-x86_64.

repomatic.tooling.tool_registry.MYPY_VERSION_MIN = (3, 8)

Earliest Python dialect Mypy’s --python-version 3.x parameter accepts.

Floors the value Metadata.mypy_params derives from the project’s requires-python, which the mypy entry in TOOL_REGISTRY passes through computed_params. A project declaring an older floor would otherwise hand mypy a version it rejects outright.

Sourced from Mypy’s own defaults.

repomatic.tooling.tool_registry.TOOL_LIST_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Tool', 'tool'), ('Version', 'version'), ('Config source', 'config-source'))

Column definitions for the repomatic run --list table.

Lives beside the registry it renders; the CLI derives its --sort-by choices from it.

repomatic.tooling.tool_registry.NPM_MIN_VERSION_FOR_COOLDOWN = '11.10.0'

First npm release honoring min-release-age, the cooldown gate for npm tools.

Older npm silently ignores the --min-release-age flag, so _install_npm() warns when it cannot enforce the cooldown. This is a fixed floor (the release that introduced the option), distinct from the auto-bumped npm@X bootstrap pin in lint.yaml, which tracks the latest npm.

class repomatic.tooling.tool_registry.NpmSpec[source]

Bases: object

npm-registry backend marker for a ToolSpec.

Presence (ToolSpec.npm is not None) selects the npm backend, the way a BinarySpec selects the download backend. The package name, executable, and version all derive from the ToolSpec fields, so no per-tool npm config is needed today; the class exists as a typed discriminator and a home for future npm-specific options.

Note

npm tools need Node.js and npm on PATH at run time: the one backend that depends on a runtime repomatic neither bundles nor provisions (binary tools are self-contained; the uv backends use uv). Integrity is npm’s own per-tarball verification on install, so unlike BinarySpec there is no repomatic-pinned checksum; the minimum-release-age cooldown (npm’s min-release-age, npm 11.10.0+) gates the transitive tree instead. Older npm ignores the gate, so the runner warns rather than silently skipping it.

class repomatic.tooling.tool_registry.ToolSpec(name, display_name=None, version='', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=NativeFormat.YAML, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url=None, tag_pattern=None, config_docs_url=None, cli_docs_url=None, docs_notes='')[source]

Bases: object

Specification for an external tool managed by repomatic.

Hint

Structural integrity checks (name format, version format, flag conventions, field consistency) are enforced in test_tool_spec_integrity.

Todo

Move those integrity checks to __post_init__ if the registry ever becomes user-configurable: a test only covers the specs shipped here.

Hint

CLI parser quirks for config_after_subcommand

Tools that use subcommands (tool <subcmd> [flags] [files]) may require config_flag to appear after the subcommand name, depending on the CLI parser framework:

  • clap (Rust): global flags accepted before or after the subcommand. No special handling needed. Used by: ruff, labelmaker.

  • cobra (Go): root-level flags inherited by all subcommands, accepted in both positions. No special handling needed. Used by: gitleaks.

  • click (Python): global flags accepted before or after the subcommand. No special handling needed. Used by: bump-my-version.

  • bpaf (Rust): #[bpaf(external)] fields are scoped inside the subcommand variant, so tool <subcmd> --flag works but tool --flag <subcmd> does not. Set config_after_subcommand=True. Used by: biome.

name: str

Tool identity: CLI name for repomatic run <name>, default PyPI package name, and default executable name.

display_name: str | None = None

Human-readable name with proper casing for documentation (like 'Biome', 'Gitleaks'). None defaults to name.

version: str = ''

Pinned version (e.g., '1.38.0').

package: str | None = None

Install target passed to uvx/uv run (and pip). None defaults to name. Only set when it differs from the tool name, and may carry an install extra (Nuitka’s nuitka[onefile]); for PyPI lookups query the bare project name through pypi_name, which strips the extra.

executable: str | None = None

Executable name if different from the tool name. None defaults to the registry key.

module: str | None = None

Python module name for -m module invocation, e.g. 'nuitka'.

When set, the tool is invoked as python -m <module> instead of the console script. Requires needs_venv=True. Use when the tool’s script entry point is not reliably found across platforms (for example, Nuitka installs only a .cmd wrapper on Windows, which uv run -- nuitka cannot locate).

native_config_files: tuple[str, ...] = ()

Config filenames the tool auto-discovers, checked in order.

Paths relative to repo root (e.g., 'zizmor.yaml', '.github/actionlint.yaml'). Empty for tools with no config file.

config_flag: str | None = None

CLI flag to pass a config file path (e.g., '--config', '--config-file'). None if the tool only reads from fixed paths.

native_format: NativeFormat = 'yaml'

Target format for [tool.X] translation.

NativeFormat.FLAGS translates the table to CLI flags (via config_table_to_flags) instead of a config file, for tools that expose their config keys as long options but read no config file themselves. It is mutually exclusive with reads_pyproject, config_flag, and native_config_files.

default_config: str | None = None

Filename in repomatic/data/ for bundled defaults, stored in native_format. None if no bundled default exists.

reads_pyproject: bool = False

Whether the tool natively reads [tool.X] from pyproject.toml.

When True and [tool.X] exists in pyproject.toml, repomatic skips Level 2 translation (the tool reads it directly). Resolution still falls through to Level 3 (bundled default) and Level 4 (bare) when no config is found.

default_flags: tuple[str, ...] = ()

Flags always passed to the tool (e.g., ('--strict',)).

ci_flags: tuple[str, ...] = ()

Flags added only when $GITHUB_ACTIONS is set (e.g., output format).

default_args: tuple[str, ...] = ()

Arguments used when the caller passes none of their own.

Together with default_paths, this makes a bare repomatic run <tool> the invocation CI performs, so nobody has to reconstruct it from a workflow step. It applies only when extra_args is empty: any explicit argument means the caller is driving, and nothing is injected on top of it.

That all-or-nothing rule is what keeps a subcommand safe to put here. biome’s defaults open with format, and splicing them into a caller’s check . would build biome format check .; because an explicit argument suppresses them entirely, that command cannot be built.

default_paths: str | None = None

Name of the FileInventory attribute supplying this tool’s targets, when the caller passes no arguments.

Caution

An empty inventory means the tool is skipped, not invoked with no path. The distinction is the whole point: a formatter handed zero paths does not no-op, it walks the entire tree in write mode. Replaying a workflow’s xargs pipe on a repo with no matching file did exactly that once, rewriting 3,000+ files, and it is the reason this resolves the list in-process rather than leaving it to a shell.

per_file: bool = False

Invoke the tool once per target rather than once for all of them.

Mirrors xargs -n1, for a tool whose per-file behaviour differs from its batch behaviour. Only meaningful alongside default_paths.

with_packages: tuple[str, ...] = ()

Extra packages installed alongside the tool (e.g., mdformat plugins).

Passed as --with <pkg> to uvx.

path_tools: tuple[str, ...] = ()

Other registry tools whose executable must be on PATH while this runs.

For a plugin that shells out to a second binary rather than importing it: mdformat-shfmt formats fenced shell blocks by invoking shfmt from PATH, so mdformat declares path_tools=("shfmt",).

Each name is installed through the same registry path as a direct repomatic run, so the companion arrives at the pinned version, checksum verified, from the shared cache. The alternative, letting the environment supply it, is what this field exists to prevent: a system package manager hands over whatever its archive holds, unpinned and outside the cooldown, and the same tool then behaves differently depending on which job invoked it.

Names must resolve in TOOL_REGISTRY and carry a binary spec; test_tool_spec_integrity enforces both.

needs_venv: bool = False

If True, use uv run (project venv) instead of uvx (isolated).

Required when the tool imports project code (mypy, pytest). The project venv materializes from the frozen uv.lock; in a repository without one the runner degrades to an isolated, cooldown-gated environment (uv run --no-project), see _build_install_args in tool_runner.py.

computed_params: Callable[[Metadata], list[str]] | None = None

Callable that receives a Metadata instance and returns extra CLI args derived from project metadata (e.g., mypy’s --python-version from requires-python). None if no computed params.

config_after_subcommand: bool = False

Insert config_flag after the first token of extra_args.

Needed for tools whose CLI parser (e.g., bpaf) scopes global options inside the subcommand, so tool subcommand --config-path X is valid but tool --config-path X subcommand is not. When True, config_args are spliced after the first element of extra_args (the subcommand name).

post_process: Callable[[Sequence[str]], None] | None = None

Callback invoked on extra_args after the tool exits successfully.

Intended for temporary workarounds that fix known upstream formatting bugs in-place. Each callback carries its own todo admonition naming the upstream release that retires it.

Note

The callback runs only after a successful write-mode exit (return code 0) and rewrites files on disk, so it cannot apply in check/dry-run mode, which writes nothing. Pair it with check_flags so run_tool warns when a check invocation would silently bypass it. See check_bypasses_post_process().

output_flag: str | None = None

Flag whose argument names the tool’s report destination, when the tool refuses to create missing parent directories itself.

run_tool pre-creates the parent directory of the path following this flag (both --flag path and --flag=path forms), so a workflow can point the tool into a scratch subdirectory without a separate mkdir step. lychee is the motivating case: docs.yaml collects its report from a dedicated subdirectory, and lychee errors out rather than creating it.

check_flags: tuple[str, ...] = ()

Flags that put the tool in check/dry-run mode, writing no files.

Warning

Check mode bypasses post_process: that fixup rewrites files on disk, but check mode writes nothing. So when a tool defines both a post_process and check_flags, its check-mode exit status is unreliable. run_tool detects the pairing via check_bypasses_post_process() and warns. Verify formatting by running the write path, not the check flag: repomatic.tooling.tool_runner.verify_via_write_path() does exactly that against throwaway copies, so the answer is authoritative and the working tree is still never written to.

rewrite_exit_code: int | None = None

Exit code the tool returns when it rewrote at least one file.

Formatters that signal “I reformatted something” with a non-zero status force every caller to tolerate that code, which is what lets a crash pass for a success: pyproject-fmt exits 1 both when it reformats a file and when it dies on a PanicException, and the autofix job cannot tell the two apart from the status alone.

Declaring the code here gives run_tool the second signal it needs: the files themselves. A run exiting with this code and leaving every target byte-identical contradicts what the code claims, so it is reported as a failure instead of being waved through. See repomatic.tooling.tool_runner.TOOL_CRASH_EXIT_CODE.

None for tools with no such convention, which is most of them: a formatter that exits 0 whether or not it wrote anything needs no disambiguation.

binary: BinarySpec | None = None

Platform-specific binary download spec. When set, the tool is downloaded as a binary instead of installed via uvx or uv run.

npm: NpmSpec | None = None

npm-registry backend marker. When set, the tool is installed from npm and run via its node_modules/.bin executable, instead of a binary download or a uv install. Mutually exclusive with binary and needs_venv.

source_url: str | None = None

GitHub repository or project homepage URL.

tag_pattern: str | None = None

Regex extracting the version from a GitHub release tag.

Used by sync-tool-versions for binary tools whose tags do not follow the common vX.Y.Z scheme. The pattern must define a version named group (e.g. r"^lychee-v(?P<version>.+)$" for lychee, r”^@biomejs/biome@ (?P<version>.+)$”``for biome). When``None, the version is the tag with a leading v stripped.

config_docs_url: str | None = None

URL to the tool’s configuration reference.

cli_docs_url: str | None = None

URL to the tool’s CLI usage documentation.

docs_notes: str = ''

Hand-written Markdown appended to the tool’s section in tool-runner.md.

Free-form usage notes the registry cannot derive: a **Try it:** shell session, a minimal [tool.X] example, caveats. Rendered live by tool_reference() after the generated metadata lines, so the prose stays next to the spec it documents.

property backend: ToolBackend

Delivery mechanism, derived from which spec fields are set.

binary and npm win over needs_venv; test_tool_spec_integrity keeps the three mutually exclusive so the order never actually decides.

property pypi_name: str

Bare PyPI project name for version and metadata lookups.

package doubles as the install target, so it may carry an install extra (Nuitka’s nuitka[onefile]) that _build_install_args needs at install time. The PyPI JSON API is keyed by the bare project name, though, and 404s on a bracketed extra, so sync-tool-versions and the held-back PR links query this stripped name (nuitka) instead.

property datasource_url: str

Human-facing URL for the tool’s version datasource.

npmjs for npm tools, the GitHub source_url when set, else the PyPI project page. Used by sync-tool-versions for the diff-table and held-back links.

check_bypasses_post_process(extra_args)[source]

Return True when a check-mode flag will skip post_process.

Check/dry-run flags (check_flags) make the tool exit without writing files, so the post_process fixup never runs and the exit status cannot be trusted: it may flag drift the write path would reconcile, or miss drift the write path would introduce. run_tool warns on this. Returns False for tools with no post_process, where check mode is authoritative.

Return type:

bool

repomatic.tooling.tool_registry.CHECKSUMS: dict[str, dict[tuple[Platform | Group, Architecture], str]] = {'actionlint': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'cadcf7ea4efe3a68728893813643cebe1185e5b1d4be5b96245f65c9a4d5ea41', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9'}, 'biome': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'bf593f7955e3a437fb8056b255142b50872baa3e81371cda2c3fce9239af1890', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '013eb5158b9e53235dbbf31255cb3b776fb9338b32fa6ff4a44ee1ceed65ee63', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '7d8b51dec857ffa8aa35ce5eaa3a4476cd62bed013adc2896bf43cac0a67a79b', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'df2b50ee283634cdd6f7570b7da06bc3c9cd7ec0590ecbbaf986c7590bef3289', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'd6845ff075043300551d4ad74985814e4e4c56b49ffa87b3724ddf5055a2efe9', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'f8474a0f9f457df176c10be3f0e82be890e8986ff2805d4a7f3c5a4cba5962ca'}, 'gh': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '73ea440ecad9c9e284429997ee6f93577bc6f7bc6fba357ef62c53ad8fb641a5', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'a58b8fd77b417a38f47a0b54d1370c59b0fcdb324ccc9ca002b0998f7c4c999e', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '63298c998cc2a924c9e254c6af6a1caad6ece281122687a91f079bc0a462700e', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '3e2d4a166da4ee5020c592737b65eec0e724946d5d5b962f5fe59d99116dc4bf', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '35d7fe05c4dd1411ffda1e73dfc7c6f44b75c936ca51fa6595c657fdc0350cec'}, 'gitleaks': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'b95f5e4f5c425cedca7ee203d9afd29597e692c4924a12ed42f970537c72cc0f', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'd29144deff3a68aa93ced33dddf84b7fdc26070add4aa0f4513094c8332afc4e'}, 'labelmaker': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '4685e142da55150904d16624fe1052161de5dba1a859cddef19ab41833c37728', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'd76f8e64f9671884dac1758fe54a28a6680c5d9bf0ffd593a2c68ba558cc49a2', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'a52a4e102f0760ce1632da5fdaee2b0debe0e6ddea577b88a94a60172fe85751', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'dc8374d6a9bec4ebf143fb42e3024aeffabe8585bb9bd6f134cfaf0693be7688', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '939195930f9d5fd2b15a5cf43497019a52083e6c6713807d3379de49395c2e10'}, 'lychee': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '91a7bd65685da41b90ccb9bc867a3d649a7818042dae04ff405e55a25bddee4c', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'c9d3740ea2d891854d37116c9fba840f37b6e7c89d330e7db84ac333631c4977', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '32975d1493ee1a975d6bb41e4fb56fe419cb442ded628bb772ba2e614acfacad'}, 'oxipng': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '97d168c6c0d1dbcb36e7438eb489804748a2ba40d94fe21aa7dab7372e9efe9b', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '9aad3927d095b6ade2aacb92b89ebaca442483c1f7cde5d7a2486b283c2ed5f9', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'c45acf40a70cc02539c55555ac240bf5ef24544b7ea9959d22da19f606cec205', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'a5ad52c9c288dc99c2eae90dcad73dee64e39bf3f5aa5303c0fb55ac9c5f069b'}, 'shfmt': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '60cd368533d0ad73fa86d93d5bbf95ef40587245ce684ed138c1b31557b5fe97'}, 'typos': {(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '85c8b87b22a0fb1da130cd4d495e0beba7f1225eb580933184509e146ec4c509', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '8c0e7bd40b2b60c0b0cfe9f74dd814b4d4385c956ce86860f7da9e62d91fdc73', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '4cecbf653a9fc45f023abf57f4e2e2f6b138c2d2387b09289beacdd3f0ea7bfd', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '06d3a1b71c282e021671070696a72696d5c60ea485b47dc4f8f1fbcf90144d02'}}

Tool name to platform-keyed SHA-256 hex digest mapping.

Recomputed in place by repomatic update-checksums and sync-tool-versions. Kept as a flat sidecar dict (rather than inline in each BinarySpec) so the checksum recompute can replace a hash by exact string match without re-parsing the registry, and so VERSIONS can anchor the offline staleness test.

repomatic.tooling.tool_registry.VERSIONS: dict[str, str] = {'actionlint': '1.7.12', 'biome': '2.5.9', 'gh': '2.97.0', 'gitleaks': '8.30.1', 'labelmaker': '0.6.4', 'lychee': '0.24.2', 'oxipng': '10.2.0', 'shfmt': '3.13.1', 'typos': '1.49.0'}

Tool name to the version each checksum set was computed for.

test_tool_spec_integrity asserts this equals the matching ToolSpec.version, so a bump whose checksums were never refreshed (a stale CHECKSUMS entry) fails CI offline, without downloading anything.

repomatic.tooling.tool_registry.TOOL_REGISTRY: dict[str, ToolSpec] = {'actionlint': ToolSpec(name='actionlint', display_name=None, version='1.7.12', package=None, executable=None, module=None, native_config_files=('.github/actionlint.yaml', '.github/actionlint.yml'), config_flag='--config-file', native_format=<NativeFormat.YAML: 'yaml'>, default_config='actionlint.yaml', reads_pyproject=False, default_flags=('-color',), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_linux_arm64.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_linux_amd64.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_darwin_arm64.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_darwin_amd64.tar.gz', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_windows_arm64.zip', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_windows_amd64.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'cadcf7ea4efe3a68728893813643cebe1185e5b1d4be5b96245f65c9a4d5ea41', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=0), npm=None, source_url='https://github.com/rhysd/actionlint', tag_pattern=None, config_docs_url='https://github.com/rhysd/actionlint/blob/main/docs/config.md', cli_docs_url='https://github.com/rhysd/actionlint/blob/main/docs/usage.md', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run actionlint\n```\n\n**Minimal `[tool.actionlint]`:**\n\n```toml\n[tool.actionlint.self-hosted-runner]\nlabels = ["my-linux-runner"]\n```\n\nWith no arguments actionlint lints every workflow under `.github/workflows`. The `[tool.actionlint]` section is bridged to a temporary YAML config: declaring self-hosted runner labels stops custom `runs-on:` values being flagged as unknown.'), 'autopep8': ToolSpec(name='autopep8', display_name=None, version='2.3.2', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=True, default_flags=('--recursive', '--in-place', '--max-line-length', '88', '--select', 'E501'), ci_flags=(), default_args=(), default_paths='python_files', per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/hhatto/autopep8', tag_pattern=None, config_docs_url=None, cli_docs_url='https://pypi.org/project/autopep8/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run autopep8 -- .\n```\n\nautopep8 takes its configuration from CLI flags only. repomatic passes `--recursive --in-place --max-line-length 88 --select E501` by default; append more flags after `--`.'), 'awesome-lint': ToolSpec(name='awesome-lint', display_name=None, version='2.3.0', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=NpmSpec(), source_url=None, tag_pattern=None, config_docs_url=None, cli_docs_url='https://github.com/sindresorhus/awesome-lint#usage', docs_notes=''), 'biome': ToolSpec(name='biome', display_name='Biome', version='2.5.9', package=None, executable=None, module=None, native_config_files=('biome.json', 'biome.jsonc', '.biome.json', '.biome.jsonc'), config_flag='--config-path', native_format=<NativeFormat.JSON: 'json'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=('format', '--write', '--no-errors-on-unmatched', '--json-parse-allow-comments=true', '--json-parse-allow-trailing-commas=true'), default_paths='json_files', per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=True, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-linux-arm64', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-linux-x64', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-darwin-arm64', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-darwin-x64', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-win32-arm64.exe', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-win32-x64.exe'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'bf593f7955e3a437fb8056b255142b50872baa3e81371cda2c3fce9239af1890', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '013eb5158b9e53235dbbf31255cb3b776fb9338b32fa6ff4a44ee1ceed65ee63', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '7d8b51dec857ffa8aa35ce5eaa3a4476cd62bed013adc2896bf43cac0a67a79b', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'df2b50ee283634cdd6f7570b7da06bc3c9cd7ec0590ecbbaf986c7590bef3289', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'd6845ff075043300551d4ad74985814e4e4c56b49ffa87b3724ddf5055a2efe9', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'f8474a0f9f457df176c10be3f0e82be890e8986ff2805d4a7f3c5a4cba5962ca'}, archive_format=<ArchiveFormat.RAW: 'raw'>, archive_executable=None, strip_components=0), npm=None, source_url='https://github.com/biomejs/biome', tag_pattern='^@biomejs/biome@(?P<version>.+)$', config_docs_url='https://biomejs.dev/reference/configuration/', cli_docs_url='https://biomejs.dev/reference/cli/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run biome -- check .\n```\n\n**Minimal `[tool.biome]`:**\n\n```toml\n[tool.biome.formatter]\nindentStyle = "space"\n```\n\n`biome check` reports formatting and lint issues; add `--write` after `--` to apply fixes. The `[tool.biome]` section is bridged to a temporary `biome.json`, so keys keep Biome\'s camelCase spelling.'), 'bump-my-version': ToolSpec(name='bump-my-version', display_name=None, version='1.5.1', package=None, executable=None, module=None, native_config_files=('.bumpversion.toml',), config_flag='--config-file', native_format=<NativeFormat.TOML: 'toml'>, default_config=None, reads_pyproject=True, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/callowayproject/bump-my-version', tag_pattern=None, config_docs_url='https://callowayproject.github.io/bump-my-version/reference/configuration/', cli_docs_url='https://callowayproject.github.io/bump-my-version/reference/cli/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run bump-my-version -- show-bump\n```\n\n**Minimal `[tool.bumpversion]`:**\n\n```toml\n[tool.bumpversion]\ncurrent_version = "1.2.3"\n```\n\nThe configuration table is `[tool.bumpversion]`, not `[tool.bump-my-version]`: the section name predates the project\'s rename. `show-bump` previews the next versions without writing; `repomatic run bump-my-version -- bump minor` performs the bump.'), 'gh': ToolSpec(name='gh', display_name='GitHub CLI', version='2.97.0', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_linux_arm64.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_linux_amd64.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_macOS_arm64.zip', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_macOS_amd64.zip', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_windows_arm64.zip', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/cli/cli/releases/download/v{version}/gh_{version}_windows_amd64.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '73ea440ecad9c9e284429997ee6f93577bc6f7bc6fba357ef62c53ad8fb641a5', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'a58b8fd77b417a38f47a0b54d1370c59b0fcdb324ccc9ca002b0998f7c4c999e', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '63298c998cc2a924c9e254c6af6a1caad6ece281122687a91f079bc0a462700e', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '3e2d4a166da4ee5020c592737b65eec0e724946d5d5b962f5fe59d99116dc4bf', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '35d7fe05c4dd1411ffda1e73dfc7c6f44b75c936ca51fa6595c657fdc0350cec'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='macos', name='macOS'): <ArchiveFormat.ZIP: 'zip'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable='bin/gh', strip_components={Group(id='all_platforms', name='All platforms'): 1, Platform(id='windows', name='Windows'): 0}), npm=None, source_url='https://github.com/cli/cli', tag_pattern=None, config_docs_url=None, cli_docs_url='https://cli.github.com/manual/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run gh -- --version\n```\n\nPinned so the release lane gets the same `gh` everywhere. The manylinux container the Linux binaries compile in ships no `gh`, and the runner images that do ship one leave its version to the image. `gh` reads no project configuration: it authenticates from `GH_TOKEN` in the environment.'), 'gitleaks': ToolSpec(name='gitleaks', display_name='Gitleaks', version='8.30.1', package=None, executable=None, module=None, native_config_files=('.gitleaks.toml',), config_flag='--config', native_format=<NativeFormat.TOML: 'toml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_linux_arm64.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_linux_x64.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_darwin_arm64.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_darwin_x64.tar.gz', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_windows_arm64.zip', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_windows_x64.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709', (Platform(id='windows', name='Windows'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'b95f5e4f5c425cedca7ee203d9afd29597e692c4924a12ed42f970537c72cc0f', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'd29144deff3a68aa93ced33dddf84b7fdc26070add4aa0f4513094c8332afc4e'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=0), npm=None, source_url='https://github.com/gitleaks/gitleaks', tag_pattern=None, config_docs_url='https://github.com/gitleaks/gitleaks#configuration', cli_docs_url='https://github.com/gitleaks/gitleaks#usage', docs_notes="**Try it:**\n\n```shell-session\n$ repomatic run gitleaks -- dir .\n```\n\n**Minimal `[tool.gitleaks]`:**\n\n```toml\n[tool.gitleaks.extend]\nuseDefault = true\n\n[tool.gitleaks.allowlist]\npaths = ['''\\.env\\.sample$''']\n```\n\n`gitleaks dir .` scans the working tree; `gitleaks git` scans history instead. The `[tool.gitleaks]` section is bridged to a temporary `.gitleaks.toml`: keep `extend.useDefault = true`, or a custom config silently replaces the built-in rule set."), 'labelmaker': ToolSpec(name='labelmaker', display_name=None, version='0.6.4', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-aarch64-unknown-linux-gnu.tar.xz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-unknown-linux-gnu.tar.xz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-aarch64-apple-darwin.tar.xz', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-apple-darwin.tar.xz', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-pc-windows-msvc.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '4685e142da55150904d16624fe1052161de5dba1a859cddef19ab41833c37728', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'd76f8e64f9671884dac1758fe54a28a6680c5d9bf0ffd593a2c68ba558cc49a2', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'a52a4e102f0760ce1632da5fdaee2b0debe0e6ddea577b88a94a60172fe85751', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'dc8374d6a9bec4ebf143fb42e3024aeffabe8585bb9bd6f134cfaf0693be7688', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '939195930f9d5fd2b15a5cf43497019a52083e6c6713807d3379de49395c2e10'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_XZ: 'tar.xz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=1), npm=None, source_url='https://github.com/jwodder/labelmaker', tag_pattern=None, config_docs_url=None, cli_docs_url='https://github.com/jwodder/labelmaker', docs_notes="labelmaker syncs a repository's issue and PR labels from a label-definition file, so unlike the linters it needs a target repository and a `GITHUB_TOKEN`, not a path in the working tree. There is no `[tool.labelmaker]` section: the label file is the configuration. See the [upstream usage docs](https://github.com/jwodder/labelmaker) for its flags and file schema."), 'lychee': ToolSpec(name='lychee', display_name='Lychee', version='0.24.2', package=None, executable=None, module=None, native_config_files=('lychee.toml',), config_flag='--config', native_format=<NativeFormat.TOML: 'toml'>, default_config=None, reads_pyproject=True, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag='--output', check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-aarch64-unknown-linux-gnu.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-x86_64-unknown-linux-gnu.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-aarch64-apple-darwin.tar.gz', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-x86_64-pc-windows-msvc.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '91a7bd65685da41b90ccb9bc867a3d649a7818042dae04ff405e55a25bddee4c', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'c9d3740ea2d891854d37116c9fba840f37b6e7c89d330e7db84ac333631c4977', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '32975d1493ee1a975d6bb41e4fb56fe419cb442ded628bb772ba2e614acfacad'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=1), npm=None, source_url='https://github.com/lycheeverse/lychee', tag_pattern='^lychee-v(?P<version>.+)$', config_docs_url='https://lychee.cli.rs/guides/config/', cli_docs_url='https://lychee.cli.rs/guides/cli/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run lychee -- .\n```\n\n**Minimal `[tool.lychee]`:**\n\n```toml\n[tool.lychee]\nmax_redirects = 5\n```\n\nlychee checks links found in the given path. Since v0.24 it reads `[tool.lychee]` from `pyproject.toml` natively, so repomatic does not translate it.'), 'mdformat': ToolSpec(name='mdformat', display_name=None, version='1.0.0', package=None, executable=None, module=None, native_config_files=('.mdformat.toml',), config_flag=None, native_format=<NativeFormat.TOML: 'toml'>, default_config='mdformat.toml', reads_pyproject=True, default_flags=('--strict-front-matter',), ci_flags=(), default_args=(), default_paths='markdown_files', per_file=True, with_packages=('mdformat_admon==2.1.1', 'mdformat-config==0.2.1', 'mdformat_deflist==0.1.4', 'mdformat_footnote==0.1.3', 'mdformat-front-matters==2.0.0', 'mdformat-gfm==1.0.0', 'mdformat_gfm_alerts==2.1.0', 'mdformat_myst==0.3.0', 'mdformat-pelican==1.0.0', 'mdformat_pyproject==0.1.1', 'mdformat-recover-urls==0.0.2', 'mdformat-shfmt==0.2.0', 'mdformat_simple_breaks==0.1.0', 'mdformat-toc==0.5.0', 'mdformat-web==0.2.0'), path_tools=('shfmt',), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=<function _fix_myst_directives>, output_flag=None, check_flags=('--check',), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/hukkin/mdformat', tag_pattern=None, config_docs_url='https://mdformat.readthedocs.io/en/stable/users/configuration_file.html', cli_docs_url='https://mdformat.readthedocs.io/en/stable/users/installation_and_usage.html', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run mdformat -- .\n```\n\n**Minimal `[tool.mdformat]`:**\n\n```toml\n[tool.mdformat]\nwrap = "no"\n```\n\nmdformat rewrites Markdown in place. repomatic bundles a plugin set (GFM, MyST, front-matter, and others) and a baseline `mdformat.toml`; `[tool.mdformat]` in your `pyproject.toml` overrides it.'), 'mypy': ToolSpec(name='mypy', display_name=None, version='2.3.1', package=None, executable=None, module=None, native_config_files=(), config_flag='--config-file', native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=True, default_flags=('--color-output',), ci_flags=(), default_args=(), default_paths='python_files', per_file=False, with_packages=(), path_tools=(), needs_venv=True, computed_params=<function <lambda>>, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/python/mypy', tag_pattern=None, config_docs_url='https://mypy.readthedocs.io/en/stable/config_file.html', cli_docs_url='https://mypy.readthedocs.io/en/stable/command_line.html', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run mypy -- .\n```\n\n**Minimal `[tool.mypy]`:**\n\n```toml\n[tool.mypy]\nstrict = true\n```\n\nmypy runs inside the project virtualenv (via `uv run`) so it can import your dependencies. repomatic derives `--python-version` from `requires-python`, so the check matches your lowest supported interpreter. In a repository without a `uv.lock` there is no project virtualenv to freeze, so mypy runs in an isolated environment instead and only resolves the standard library: fine for standalone scripts, but dependency imports then report `import-not-found`.\n\n`uv run` provisions only the default dependency groups, so a module that imports a dep declared solely in a non-default group (`docs`, `typing`, …) sees it as missing and mypy reports `import-not-found`. Either move the stub/dependency somewhere mypy resolves, or silence it with an override:\n\n```toml\n[[tool.mypy.overrides]]\nmodule = "the_docs_only_package.*"\nignore_missing_imports = true\n```'), 'nuitka': ToolSpec(name='nuitka', display_name='Nuitka', version='4.1.3', package='nuitka[onefile]', executable=None, module='nuitka', native_config_files=(), config_flag=None, native_format=<NativeFormat.FLAGS: 'flags'>, default_config=None, reads_pyproject=False, default_flags=('--mode=onefile', '--assume-yes-for-downloads'), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=True, computed_params=<function <lambda>>, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/Nuitka/Nuitka', tag_pattern=None, config_docs_url='https://nuitka.net/doc/user-manual.html', cli_docs_url='https://nuitka.net/doc/user-manual.html', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run nuitka -- my_app/__main__.py\n```\n\n**Minimal `[tool.nuitka]`:**\n\n```toml\n[tool.nuitka]\nonefile = true\noutput-dir = "build"\n```\n\nrepomatic reads every key from `[tool.nuitka]` and forwards it as a CLI flag: `true` becomes a bare `--flag`, a string or number becomes `--key=value`, and a list repeats the flag once per item. Nuitka does not read `[tool.nuitka]` natively yet ([Nuitka#3909](https://github.com/Nuitka/Nuitka/issues/3909)); repomatic\'s bridge fills the gap until it does.\n\nBinaries skip `tkinter` by default, via the `nuitka.nofollow-imports` setting of [`[tool.repomatic]`](configuration.md): set it to `[]` to bundle Tcl/Tk in a GUI project.'), 'oxipng': ToolSpec(name='oxipng', display_name='Oxipng', version='10.2.0', package=None, executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-aarch64-unknown-linux-gnu.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-unknown-linux-gnu.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-aarch64-apple-darwin.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-apple-darwin.tar.gz', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-pc-windows-msvc.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '97d168c6c0d1dbcb36e7438eb489804748a2ba40d94fe21aa7dab7372e9efe9b', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '9aad3927d095b6ade2aacb92b89ebaca442483c1f7cde5d7a2486b283c2ed5f9', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'c45acf40a70cc02539c55555ac240bf5ef24544b7ea9959d22da19f606cec205', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'a5ad52c9c288dc99c2eae90dcad73dee64e39bf3f5aa5303c0fb55ac9c5f069b'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=1), npm=None, source_url='https://github.com/shssoichiro/oxipng', tag_pattern=None, config_docs_url=None, cli_docs_url='https://github.com/shssoichiro/oxipng#usage', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run oxipng -- --opt 4 --strip safe image.png\n```\n\nLossless PNG optimizer. `repomatic format-images` reaches it through {func}`repomatic.tooling.tool_runner.ensure_binary`, so the pinned, checksum-verified build is used instead of whatever the runner image or the distro archive supplies.'), 'pyproject-fmt': ToolSpec(name='pyproject-fmt', display_name=None, version='2.28.0', package=None, executable=None, module=None, native_config_files=('pyproject-fmt.toml',), config_flag='--config', native_format=<NativeFormat.TOML: 'toml'>, default_config=None, reads_pyproject=True, default_flags=(), ci_flags=(), default_args=(), default_paths='pyproject_files', per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=1, binary=None, npm=None, source_url='https://github.com/tox-dev/pyproject-fmt', tag_pattern=None, config_docs_url='https://pyproject-fmt.readthedocs.io/en/latest/', cli_docs_url='https://pyproject-fmt.readthedocs.io/en/latest/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run pyproject-fmt -- pyproject.toml\n```\n\n**Minimal `[tool.pyproject-fmt]`:**\n\n```toml\n[tool.pyproject-fmt]\nindent = 4\n```\n\npyproject-fmt normalizes and reorders `pyproject.toml` in place. It reads its own `[tool.pyproject-fmt]` section natively.'), 'ruff': ToolSpec(name='ruff', display_name='Ruff', version='0.16.3', package=None, executable=None, module=None, native_config_files=('.ruff.toml', 'ruff.toml'), config_flag='--config', native_format=<NativeFormat.TOML: 'toml'>, default_config='ruff.toml', reads_pyproject=True, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/astral-sh/ruff', tag_pattern=None, config_docs_url='https://docs.astral.sh/ruff/configuration/', cli_docs_url='https://docs.astral.sh/ruff/configuration/#command-line-interface', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run ruff -- check .\n```\n\n**Minimal `[tool.ruff]`:**\n\n```toml\n[tool.ruff]\nline-length = 100\n```\n\n`ruff check .` lints; `ruff format .` reformats. Both read `[tool.ruff]` natively. With no project config, repomatic falls back to its bundled `ruff.toml` baseline.'), 'shfmt': ToolSpec(name='shfmt', display_name=None, version='3.13.1', package=None, executable=None, module=None, native_config_files=('.editorconfig',), config_flag=None, native_format=<NativeFormat.EDITORCONFIG: 'editorconfig'>, default_config=None, reads_pyproject=False, default_flags=('--write',), ci_flags=(), default_args=(), default_paths='shfmt_files', per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_linux_arm64', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_linux_amd64', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_darwin_arm64', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_darwin_amd64', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_windows_amd64.exe'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '60cd368533d0ad73fa86d93d5bbf95ef40587245ce684ed138c1b31557b5fe97'}, archive_format=<ArchiveFormat.RAW: 'raw'>, archive_executable=None, strip_components=0), npm=None, source_url='https://github.com/mvdan/sh', tag_pattern=None, config_docs_url='https://github.com/mvdan/sh/blob/master/cmd/shfmt/shfmt.1.scd', cli_docs_url='https://github.com/mvdan/sh#shfmt', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run shfmt -- .\n```\n\nshfmt formats shell scripts in place. It has no `[tool.shfmt]` section: indentation and style come from `.editorconfig` (`indent_size`, `shell_variant`, and the `shfmt`-specific keys).'), 'typos': ToolSpec(name='typos', display_name=None, version='1.49.0', package=None, executable=None, module=None, native_config_files=('typos.toml', '_typos.toml', '.typos.toml'), config_flag='--config', native_format=<NativeFormat.TOML: 'toml'>, default_config=None, reads_pyproject=True, default_flags=('--write-changes',), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=BinarySpec(urls={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-aarch64-unknown-linux-musl.tar.gz', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-unknown-linux-musl.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): 'https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-aarch64-apple-darwin.tar.gz', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-apple-darwin.tar.gz', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): 'https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-pc-windows-msvc.zip'}, checksums={(Group(id='linux', name='Linux distributions'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '85c8b87b22a0fb1da130cd4d495e0beba7f1225eb580933184509e146ec4c509', (Group(id='linux', name='Linux distributions'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10', (Platform(id='macos', name='macOS'), Architecture(id='aarch64', name='ARM64 (AArch64)')): '8c0e7bd40b2b60c0b0cfe9f74dd814b4d4385c956ce86860f7da9e62d91fdc73', (Platform(id='macos', name='macOS'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '4cecbf653a9fc45f023abf57f4e2e2f6b138c2d2387b09289beacdd3f0ea7bfd', (Platform(id='windows', name='Windows'), Architecture(id='x86_64', name='x86-64 (AMD64)')): '06d3a1b71c282e021671070696a72696d5c60ea485b47dc4f8f1fbcf90144d02'}, archive_format={Group(id='all_platforms', name='All platforms'): <ArchiveFormat.TAR_GZ: 'tar.gz'>, Platform(id='windows', name='Windows'): <ArchiveFormat.ZIP: 'zip'>}, archive_executable=None, strip_components=0), npm=None, source_url='https://github.com/crate-ci/typos', tag_pattern=None, config_docs_url='https://github.com/crate-ci/typos/blob/master/docs/reference.md', cli_docs_url='https://github.com/crate-ci/typos/blob/master/docs/reference.md', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run typos -- .\n```\n\n**Minimal `[tool.typos]`:**\n\n```toml\n[tool.typos.files]\nextend-exclude = ["*.lock"]\n```\n\ntypos scans the tree and, with repomatic\'s default `--write-changes`, fixes what it finds. It reads `[tool.typos]` natively; use `[tool.typos.default.extend-words]` to map project-specific terms to their intended spelling.\n\nBecause the `fix-typos` workflow job ships whatever typos rewrites as an unattended pull request, guard content where a "correction" is a corruption with `[tool.typos.default.extend-ignore-re]` patterns. The two known traps are encoded hashes, whose random letter runs typos happily respells (a Guix `(base32 "...")` source hash losing its value to an `an`-to-`and` fix), and intentional-typo examples that docs or tests exercise on purpose:\n\n```toml\n[tool.typos.default]\nextend-ignore-re = [\n  \'base32 "[0-9a-z]{52}"\',\n  "\\\\{query\\\\}",\n]\n```'), 'yamllint': ToolSpec(name='yamllint', display_name=None, version='1.38.0', package=None, executable=None, module=None, native_config_files=('.yamllint', '.yamllint.yaml', '.yamllint.yml'), config_flag='--config-file', native_format=<NativeFormat.YAML: 'yaml'>, default_config='yamllint.yaml', reads_pyproject=False, default_flags=('--strict',), ci_flags=('--format', 'github'), default_args=('.',), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/adrienverge/yamllint', tag_pattern=None, config_docs_url='https://yamllint.readthedocs.io/en/stable/configuration.html', cli_docs_url='https://yamllint.readthedocs.io/en/stable/quickstart.html', docs_notes="**Try it:**\n\n```shell-session\n$ repomatic run yamllint -- .\n```\n\n**Minimal `[tool.yamllint]`:**\n\n```toml\n[tool.yamllint.rules.line-length]\nmax = 120\n```\n\nyamllint has no native `pyproject.toml` support, so repomatic bridges `[tool.yamllint]` to a temporary YAML config passed via `--config-file`. With no project config it uses repomatic's strict bundled `yamllint.yaml`."), 'zizmor': ToolSpec(name='zizmor', display_name=None, version='1.29.0', package=None, executable=None, module=None, native_config_files=('.github/zizmor.yml', '.github/zizmor.yaml', 'zizmor.yml', 'zizmor.yaml'), config_flag='--config', native_format=<NativeFormat.YAML: 'yaml'>, default_config='zizmor.yaml', reads_pyproject=False, default_flags=('--offline',), ci_flags=('--format', 'github'), default_args=('.',), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url='https://github.com/zizmorcore/zizmor', tag_pattern=None, config_docs_url='https://docs.zizmor.sh/configuration/', cli_docs_url='https://docs.zizmor.sh/usage/', docs_notes='**Try it:**\n\n```shell-session\n$ repomatic run zizmor -- .\n```\n\nzizmor audits GitHub Actions workflows for security issues, offline by default. repomatic bridges `[tool.zizmor]` to a temporary YAML config (passed via `--config`); with none, it uses the bundled `zizmor.yaml`. See the [configuration reference](https://docs.zizmor.sh/configuration/) for available keys.')}

Tool name to the specification driving its install, config and invocation.

The single source of truth for repomatic run: every command, workflow job and documentation renderer reads the roster from here rather than hard-coding a tool name, a version or a flag.

Todo

Give the mdformat entry a config_flag="--config" once mdformat accepts an explicit config path, so it stops relying on discovery from the working directory: hukkin/mdformat#432 and hukkin/mdformat#562.

repomatic.tooling.tool_registry.tool_summary()[source]

Render the summary table of all managed tools.

Return type:

str

repomatic.tooling.tool_registry.tool_reference()[source]

Render the per-tool detail sections.

The metadata of each section (version, install, config, flags, links) is generated from the registry; the trailing free-form prose comes from the spec’s own docs_notes field, so hand-written examples and caveats live next to the spec they document.

Return type:

str

repomatic.tooling.tool_runner module

Unified tool runner with managed config resolution.

Provides repomatic run <tool> — a single entry point that installs an external tool at a pinned version, resolves its configuration through a strict 4-level precedence chain, translates [tool.X] sections from pyproject.toml into the tool’s native format, and invokes the tool with the resolved config. The tool catalog it drives (ToolSpec entries, pinned versions, checksums) lives in tool_registry.py.

Important

Config resolution precedence (first match wins, no merging):

  1. Native config file — tool’s own config file in the repo.

  2. ``[tool.X]`` in ``pyproject.toml`` — translated to native format.

  3. Bundled default — from repomatic/data/.

  4. Bare invocation — no config at all.

repomatic.tooling.tool_runner.load_pyproject_tool_section(tool_name)[source]

Load [tool.<tool_name>] from pyproject.toml in the current directory.

Returns the live tomlrt.Table (a dict subclass) rather than a plain-dict copy, so the section keeps its comment trivia for formats that can preserve it on materialization (see NativeFormat.serialize()). Callers that only read values or test truthiness are unaffected.

Return type:

dict[str, Any]

Returns:

The tool’s config table, or empty dict if not found.

repomatic.tooling.tool_runner.resolve_config(spec, tool_config=None)[source]

Resolve config for a tool using the 4-level precedence chain.

Caution

The levels do not merge. The walk stops at its first hit, so a native config file or a [tool.X] section replaces the bundled default in full rather than layering on top of it. A downstream repo overriding one rule must restate every bundled rule it wants to keep, and gains nothing when the bundled default later grows a rule. resolve_config_source() labels a shadowing config so repomatic run --list shows the loss.

Parameters:
  • spec (ToolSpec) – Tool specification.

  • tool_config (dict[str, Any] | None) – Pre-loaded [tool.X] config dict. If None, reads from pyproject.toml in the current directory.

Return type:

tuple[list[str], Path | None]

Returns:

Tuple of (extra CLI args for config, path to clean up). The path is None when no cleanup is needed (cache-based configs persist across runs). Non-None paths are CWD files written for tools that have no --config flag.

repomatic.tooling.tool_runner.DOWNLOAD_TIMEOUT = 30

Socket-level timeout for artifact downloads, in seconds.

A stall guard, not a transfer budget: urlopen applies it to each blocking socket operation, so a healthy multi-minute download is unaffected while a dead connection fails in seconds instead of hanging a CI job to the runner ceiling. Deliberately larger than repomatic.http.DEFAULT_TIMEOUT, which is sized for small JSON API responses.

repomatic.tooling.tool_runner.download_to(url, dest_path, *, label=None, progress=True)[source]

Stream url into dest_path and return its SHA-256 hex digest.

Chunked download with incremental hash computation, so large binaries never load fully into memory. Shows a progress bar on interactive terminals when the server provides a Content-Length header; pass progress=False from concurrent callers whose fan-out draws its own progress display.

The single download seam for every artifact repomatic fetches by hand: whatever consumes the digest (verification in _download_and_verify(), checksum harvesting in checksums.py) builds on this so the truncation guard below applies to all of them. A short body (proxy hiccup, dropped connection) hashes to a wrong digest, so without the guard it would surface later as a checksum mismatch: that reads as a stale pin or a tampered artifact when nothing is wrong upstream. Name the real failure instead.

Parameters:
  • url (str) – URL to download.

  • dest_path (Path) – Where to write the downloaded file.

  • label (str | None) – Progress bar label. Defaults to the destination filename.

  • progress (bool) – Draw per-download feedback on interactive terminals.

Return type:

str

Returns:

Lowercase hex SHA-256 digest of the downloaded bytes.

Raises:

OSError – If the body is shorter than the advertised Content-Length.

repomatic.tooling.tool_runner.ensure_binary(name: str) Path[source]

Install a registry binary tool and return the path to its executable.

The seam for repomatic code that shells out to a third-party binary but is not itself a run_tool() invocation. It buys the same guarantees every repomatic run binary gets: the registry-pinned version, its archive verified against the recorded SHA-256, and a shared cache so repeated calls in one run download once.

Prefer this over looking the tool up on PATH. Whatever PATH offers is whichever version the machine or CI image happens to carry, unpinned and unverified, and it differs between a developer’s laptop and every runner.

Memoized per tool name: callers in a loop (format-images optimizing one PNG per call) hit the install-and-verify path once per process, not once per file. Failures are not memoized, so a transient download error can be retried.

Parameters:

name (str) – Registry key of a tool whose ToolSpec declares a binary.

Return type:

Path

Returns:

Absolute path to the ready-to-run executable.

Raises:

ClickException – If the tool is unknown, ships no binary, or cannot be downloaded and verified.

repomatic.tooling.tool_runner.resolve_default_args(spec)[source]

Build the argument batches for a bare repomatic run <tool>.

Combines default_args with the file list named by default_paths, splitting into one batch per file when per_file is set.

Parameters:

spec (ToolSpec) – The tool to resolve defaults for.

Return type:

list[list[str]] | None

Returns:

One argument list per invocation; a single empty-argument batch when the tool declares no defaults, so the caller runs it bare as before. None when the tool wants targets and the repository holds none, which means skip the tool rather than invoke it pathless.

repomatic.tooling.tool_runner.TOOL_CRASH_EXIT_CODE = 70

Exit code reported when a tool contradicts its own rewrite status.

EX_SOFTWARE from sysexits.h: an internal error in the tool being run. Deliberately outside the set a formatter’s caller tolerates, so a crash cannot land on the code that means “I reformatted a file”. See rewrite_exit_code.

repomatic.tooling.tool_runner.run_tool(name, extra_args=(), version=None, checksum=None, skip_checksum=False, no_cache=False)[source]

Run an external tool with managed config resolution.

With no extra_args, a tool declaring default_args or default_paths runs the invocation CI performs, resolved in-process by resolve_default_args(). Any explicit argument suppresses that entirely and is passed through as before.

Parameters:
  • name (str) – Tool name (must be in TOOL_REGISTRY).

  • extra_args (Sequence[str]) – Extra arguments passed through to the tool.

  • version (str | None) – Override the pinned version.

  • checksum (str | None) – Override the SHA-256 checksum for the current platform.

  • skip_checksum (bool) – Skip SHA-256 verification entirely.

  • no_cache (bool) – Bypass the binary cache when True.

Return type:

int

Returns:

The tool’s exit code; the first non-zero one when the defaults resolved to several invocations, or TOOL_CRASH_EXIT_CODE when a tool declaring rewrite_exit_code reports a rewrite it did not perform.

repomatic.tooling.tool_runner.verify_via_write_path(name, extra_args=(), **run_kwargs)[source]

Check a post_process tool’s formatting without touching the tree.

A tool pairing post_process with check_flags has no trustworthy check mode: the fixup only runs on the write path, so the check status can flag drift the write path would reconcile, or miss drift it would introduce (see check_flags). This runs the write path against throwaway copies instead, then compares, which is the only authoritative answer.

Important

The copies are made inside the working directory, not in the system temp area. Formatters discover their config by walking up from each file, so a copy parked outside the repository resolves a different config and silently reports drift that does not exist.

The working tree is never written to: only the copies are formatted, and they are removed before returning.

Parameters:
  • name (str) – Tool name, as in run_tool().

  • extra_args (Sequence[str]) – Arguments for the tool. Any existing path among them is copied and rewritten to its copy; check flags are dropped, since they would defeat the write path this relies on. Every other argument is passed through untouched. Empty resolves the tool’s registry defaults, the same set run_tool() would have run, flattened into one batch: the copies are per-path already, so a per_file split would only cost extra invocations.

  • run_kwargs (Any) – Forwarded verbatim to run_tool().

Return type:

tuple[int, list[str]]

Returns:

(exit_code, drifted), where exit_code is 0 when every target is already formatted and 1 otherwise, and drifted names the paths the write path would have changed. A tool that fails on the copies yields its own exit code and no drift, since it measured nothing.

repomatic.tooling.tool_runner.resolve_config_source(spec)[source]

Return a human-readable description of the active config source.

Used by repomatic run --list to show which precedence level is active for each tool in the current repo.

Return type:

str

repomatic.tooling.tool_runner.find_unmodified_configs(root=None)[source]

Find native config files identical to their bundled defaults.

Iterates over every tool in TOOL_REGISTRY that has a default_config. For each, checks whether any of its native_config_files exists on disk and is content-identical to the bundled default after trailing-whitespace normalization.

The normalization (rstrip() + "\n") matches the convention used by _init_config_files when writing files during init.

Parameters:

root (Path | None) – Directory the relative config paths resolve against. Defaults to the working directory; run_init passes its output_dir so the scan and the deletion the CLI derives from it (--delete-unmodified) agree on one tree.

Return type:

list[tuple[str, str]]

Returns:

List of (tool_name, relative_path) tuples for each unmodified file found.