{"id":46886789,"url":"https://github.com/skhokhlov/rewrite-runner","last_synced_at":"2026-06-15T12:00:54.649Z","repository":{"id":342846518,"uuid":"1172858500","full_name":"skhokhlov/rewrite-runner","owner":"skhokhlov","description":"A CLI tool and library for running OpenRewrite recipes against arbitrary repositories — without requiring the target project's build to be working.","archived":false,"fork":false,"pushed_at":"2026-06-12T17:54:47.000Z","size":1143,"stargazers_count":3,"open_issues_count":28,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T19:22:41.882Z","etag":null,"topics":["cli","java-library","kotlin","kotlin-library","openrewrite","rewrite"],"latest_commit_sha":null,"homepage":"https://skhokhlov.github.io/rewrite-runner/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skhokhlov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T19:02:21.000Z","updated_at":"2026-06-12T17:53:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"4b42e00a-4e87-40c5-ab19-3e9f6c50a37d","html_url":"https://github.com/skhokhlov/rewrite-runner","commit_stats":null,"previous_names":["skhokhlov/super-duper-fiesta","skhokhlov/rewrite-runner"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/skhokhlov/rewrite-runner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skhokhlov%2Frewrite-runner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skhokhlov%2Frewrite-runner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skhokhlov%2Frewrite-runner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skhokhlov%2Frewrite-runner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skhokhlov","download_url":"https://codeload.github.com/skhokhlov/rewrite-runner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skhokhlov%2Frewrite-runner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34361403,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","java-library","kotlin","kotlin-library","openrewrite","rewrite"],"created_at":"2026-03-10T22:17:49.942Z","updated_at":"2026-06-15T12:00:54.639Z","avatar_url":"https://github.com/skhokhlov.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rewrite-runner\n\n[![Build](https://github.com/skhokhlov/rewrite-runner/actions/workflows/build.yml/badge.svg)](https://github.com/skhokhlov/rewrite-runner/actions/workflows/build.yml)\n[![CodeQL](https://github.com/skhokhlov/rewrite-runner/actions/workflows/codeql.yml/badge.svg)](https://github.com/skhokhlov/rewrite-runner/actions/workflows/codeql.yml)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.skhokhlov.rewriterunner/core)](https://central.sonatype.com/artifact/io.github.skhokhlov.rewriterunner/core)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n\nA self-hosted CLI tool for running [OpenRewrite](https://docs.openrewrite.org/) recipes against arbitrary repositories — without requiring the target project's build to be working.\n\n## Features\n\n- Run any OpenRewrite recipe against a local project directory\n- Works even when the project's build is broken, credentials are missing, or private registries are unavailable\n- Automatically downloads recipe JARs from Maven coordinates — no manual dependency management\n- Supports Java, Kotlin, Groovy, YAML, JSON, XML, Properties, TOML, HCL/Terraform, Protobuf, Dockerfile, and plain-text mask files (`pom.xml` uses `MavenParser` for full Maven recipe support)\n- Three output modes: unified diffs, changed file paths, or a structured JSON report\n- Composable recipes via `rewrite.yaml`\n- Configurable Maven repositories for enterprise environments with private Nexus/Artifactory\n\n## Installation\n\n`rewrite-runner` is published to Maven Central. The `core` module is the library; the `cli` module ships a thin JAR plus a `-all` fat JAR for direct CLI use.\n\n### Gradle\n\n```kotlin\n// build.gradle.kts\ndependencies {\n    implementation(\"io.github.skhokhlov.rewriterunner:core:1.0.0\")\n}\n```\n\n### Maven\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.skhokhlov.rewriterunner\u003c/groupId\u003e\n    \u003cartifactId\u003ecore\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### CLI fat JAR\n\nDownload the `-all` jar directly from Maven Central:\n\n```bash\ncurl -L -o rewrite-runner.jar \\\n  \"https://repo1.maven.org/maven2/io/github/skhokhlov/rewriterunner/cli/1.0.0/cli-1.0.0-all.jar\"\njava -jar rewrite-runner.jar --help\n```\n\n## Getting Started\n\n### Build\n\nRequires JDK 21+.\n\n```bash\n./gradlew shadowJar\n# Produces: cli/build/libs/cli-1.0-SNAPSHOT-all.jar\n```\n\n### Run a recipe\n\n```bash\njava -jar cli/build/libs/cli-1.0-SNAPSHOT-all.jar \\\n  --project-dir /path/to/your/project \\\n  --active-recipe org.openrewrite.java.format.AutoFormat \\\n  --recipe-artifact org.openrewrite.recipe:rewrite-static-analysis:LATEST\n```\n\n### Dry run (preview changes without writing to disk)\n\n```bash\njava -jar cli/build/libs/cli-1.0-SNAPSHOT-all.jar \\\n  --project-dir /path/to/your/project \\\n  --active-recipe org.openrewrite.java.migrate.UpgradeToJava21 \\\n  --recipe-artifact org.openrewrite.recipe:rewrite-migrate-java:LATEST \\\n  --dry-run \\\n  --output diff\n```\n\n## Library Usage\n\n`rewrite-runner` can be used as a library from Java and Kotlin code without the CLI layer. Use the plain JAR (not the `-all` fat JAR) as a dependency.\n\n### Adding as a dependency\n\n```kotlin\n// build.gradle.kts — Maven Central (recommended)\ndependencies {\n    implementation(\"io.github.skhokhlov.rewriterunner:core:1.0.0\")\n}\n```\n\n```kotlin\n// build.gradle.kts — local JAR (for development)\ndependencies {\n    implementation(files(\"libs/rewrite-runner-core-1.0-SNAPSHOT.jar\"))\n}\n```\n\n### Kotlin usage\n\n```kotlin\nimport io.github.skhokhlov.rewriterunner.RewriteRunner\nimport java.nio.file.Paths\nimport java.time.Duration\n\nfun main() {\n    val result = RewriteRunner.builder()\n        .projectDir(Paths.get(\"/path/to/project\"))\n        .activeRecipe(\"org.openrewrite.java.format.AutoFormat\")\n        .recipeArtifact(\"org.openrewrite.recipe:rewrite-static-analysis:LATEST\")\n        .processTimeout(Duration.ofSeconds(120))\n        .dryRun(true)   // preview changes without writing to disk\n        .build()\n        .run()\n\n    println(\"Changed ${result.changeCount} file(s)\")\n    result.results.forEach { r -\u003e println(r.diff()) }\n}\n```\n\n### Java usage\n\n```java\nimport io.github.skhokhlov.rewriterunner.RewriteRunner;\nimport io.github.skhokhlov.rewriterunner.RunResult;\nimport java.nio.file.Paths;\nimport java.time.Duration;\n\npublic class Example {\n    public static void main(String[] args) {\n        RunResult result = RewriteRunner.builder()\n            .projectDir(Paths.get(\"/path/to/project\"))\n            .activeRecipe(\"org.openrewrite.java.format.AutoFormat\")\n            .recipeArtifact(\"org.openrewrite.recipe:rewrite-static-analysis:LATEST\")\n            .processTimeout(Duration.ofSeconds(120))\n            .dryRun(true)\n            .build()\n            .run();\n\n        System.out.println(\"Changed \" + result.getChangeCount() + \" file(s)\");\n        result.getResults().forEach(r -\u003e System.out.println(r.diff()));\n    }\n}\n```\n\n### Working with results\n\n`RunResult` gives you raw access to everything the recipe produced:\n\n```kotlin\nval result = runner.run()\n\nprintln(\"Changed: ${result.hasChanges}\")         // true/false\nprintln(\"Files changed: ${result.changeCount}\")   // results.size + rawDiffs.size\n\n// Iterate raw OpenRewrite results\nresult.results.forEach { r -\u003e\n    // r.before — source file before the recipe (null for newly created files)\n    // r.after  — source file after the recipe (null for deleted files)\n    println(r.diff())           // unified diff string\n    println(r.after?.sourcePath)  // relative path of the changed file\n}\n\n// Plugin-first raw diffs may be populated alongside results when the\n// Stage 0 specialized ownership pass also changed Docker/HCL/protobuf files.\nresult.rawDiffs.forEach { (path, diff) -\u003e println(\"$path\\n$diff\") }\n\n// changedFiles: paths written to disk (empty in dry-run mode)\nresult.changedFiles.forEach { path -\u003e println(\"Written: $path\") }\n\n// Per-file parse failures (no need to scrape logs)\nresult.executionDiagnostics.parseFailures.forEach { failure -\u003e\n    println(\"${failure.parser} could not handle ${failure.path}: ${failure.reason}\")\n}\n\n// OpenRewrite's estimate of manual effort avoided, or null when unavailable.\nprintln(\"Estimated time saved: ${result.executionDiagnostics.estimatedTimeSaved}\")\n\n// Null means a plugin-only path ran and no in-process LST count was measured.\nprintln(\"Parsed files: ${result.executionDiagnostics.parsedFileCount}\")\n```\n\nPer-file parse failures are collected into `executionDiagnostics.parseFailures`\nrather than aborting the build; callers can surface or ignore them. The one\nintentional exception: a non-URI `MavenParser` throw aborts the LST build by\ndesign (silently downgrading it to `XmlParser` would hide regressions and produce\nmisleading recipe results). URI-class `MavenParser` failures still fall back to\n`XmlParser` and are recorded normally. Fatal `Error`s (e.g. `OutOfMemoryError`)\nalways propagate. See [`docs/library-api.md`](docs/library-api.md#parse-failures)\nfor the full shape.\n\n### Formatted output (ResultFormatter)\n\nFor the same three output modes as the CLI (`diff`, `files`, `report`), use `ResultFormatter`:\n\n```kotlin\nimport io.github.skhokhlov.rewriterunner.output.OutputMode\nimport io.github.skhokhlov.rewriterunner.output.ResultFormatter\n\nval result = runner.run()\n\n// Print unified diffs to stdout\nResultFormatter(OutputMode.DIFF).format(result)\n\n// Print only the paths of changed files (one per line)\nResultFormatter(OutputMode.FILES).format(result)\n\n// Write openrewrite-report.json to the project directory\nResultFormatter(OutputMode.REPORT).format(result)\n```\n\n`OutputMode` values:\n\n| Value | Behaviour |\n|-------|-----------|\n| `DIFF` | Prints a unified diff for each changed file to stdout (default CLI mode) |\n| `FILES` | Prints one changed-file path per line to stdout |\n| `REPORT` | Writes `openrewrite-report.json` to the `reportDir` argument (defaults to `.`) |\n\nLibrary consumers that only need to inspect changes programmatically can skip `ResultFormatter` entirely and work with `RunResult.results` / `RunResult.rawDiffs` directly.\n\n### Logging\n\nBy default the `core` library produces **no log output** — all logging is suppressed via `NoOpRunnerLogger`. To receive pipeline events, implement `RunnerLogger` and pass it to the builder:\n\n```kotlin\nimport io.github.skhokhlov.rewriterunner.RunnerLogger\n\nclass PrintlnLogger : RunnerLogger {\n    override fun lifecycle(message: String) = println(\"[LIFECYCLE] $message\")\n    override fun info(message: String)      = println(\"[INFO]      $message\")\n    override fun debug(message: String)     = println(\"[DEBUG]     $message\")\n    override fun warn(message: String)      = println(\"[WARN]      $message\")\n    override fun error(message: String, cause: Throwable?) {\n        println(\"[ERROR]     $message\")\n        cause?.printStackTrace()\n    }\n}\n\nval result = RewriteRunner.builder()\n    .projectDir(Paths.get(\"/path/to/project\"))\n    .activeRecipe(\"org.openrewrite.java.format.AutoFormat\")\n    .logger(PrintlnLogger())   // wire your logger here\n    .build()\n    .run()\n```\n\n**Log levels** and what they emit:\n\n| Level | What you receive |\n|-------|-----------------|\n| `lifecycle` | Pipeline stage headers and summary results (always relevant) |\n| `info` | Per-language file counts, stage status, artifact resolution progress |\n| `debug` | Per-file version detection, recipe JAR scanning |\n| `warn` | Recoverable problems (e.g. 404 during artifact resolution) |\n| `error` | Fatal failures, always with an optional `cause` |\n\n`NoOpRunnerLogger` (the default) silently discards all messages. The CLI wires its own Logback-backed implementation; `--info` and `--debug` flags control which levels are forwarded to Logback.\n\n---\n\n### Enterprise and private registry setup\n\nWhen Maven Central is unreachable, provide your private registry directly via the builder:\n\n```kotlin\nimport io.github.skhokhlov.rewriterunner.config.RepositoryConfig\n\nval result = RewriteRunner.builder()\n    .projectDir(Paths.get(\"/path/to/project\"))\n    .activeRecipe(\"org.openrewrite.java.format.AutoFormat\")\n    .recipeArtifact(\"org.openrewrite.recipe:rewrite-static-analysis:LATEST\")\n    .artifactRepository(RepositoryConfig(\n        url = \"https://nexus.example.com/repository/maven-public\",\n        username = System.getenv(\"NEXUS_USER\"),\n        password = System.getenv(\"NEXUS_PASS\")\n    ))\n    .includeMavenCentral(false)  // restrict resolution to the Nexus repository only\n    .build()\n    .run()\n```\n\n### Programmatic composite recipes\n\nInstead of writing a `rewrite.yaml` file on disk, you can supply the YAML content as a string directly via `rewriteConfigContent`:\n\n```kotlin\nval recipeYaml = \"\"\"\n    type: specs.openrewrite.org/v1beta/recipe\n    name: com.example.MyMigration\n    displayName: My Custom Migration\n    recipeList:\n      - org.openrewrite.java.migrate.UpgradeToJava21\n      - org.openrewrite.java.format.AutoFormat\n\"\"\".trimIndent()\n\nval result = RewriteRunner.builder()\n    .projectDir(Paths.get(\"/path/to/project\"))\n    .activeRecipe(\"com.example.MyMigration\")\n    .recipeArtifact(\"org.openrewrite.recipe:rewrite-migrate-java:LATEST\")\n    .rewriteConfigContent(recipeYaml)  // no file required\n    .build()\n    .run()\n```\n\n## CLI Reference\n\n```\nUsage: rewrite-runner [-h] [--dry-run] [--skip-plugin-run] [--info] [--debug]\n                          [--no-maven-central]\n                          [--active-recipe=\u003crecipe\u003e]\n                          [--cache-dir=\u003cpath\u003e] [--config=\u003cpath\u003e]\n                          [--artifact-download-threads=\u003cn\u003e]\n                          [--subprocess-run-timeout=\u003cduration\u003e]\n                          [--plugin-run-timeout\u003cduration\u003e]\n                          [--artifact-resolver-connect-timeout=\u003cduration\u003e]\n                          [--artifact-resolver-request-timeout=\u003cduration\u003e]\n                          [--output=\u003cmode\u003e] [--project-dir=\u003cpath\u003e]\n                          [--rewrite-config=\u003cpath\u003e]\n                          [--exclude-paths=\u003cglob\u003e[,\u003cglob\u003e...]]\n                          [--plain-text-masks=\u003cglob\u003e[,\u003cglob\u003e...]]\n                          [--recipe-artifact=\u003ccoord\u003e]...\n```\n\n| Option                                | Description | Default |\n|---------------------------------------|-------------|---------|\n| `--project-dir`, `-p`                 | Project directory to refactor | `.` (current directory) |\n| `--active-recipe`, `-r`               | Fully-qualified recipe name to run | *(required)* |\n| `--recipe-artifact`                   | Maven coordinate of a recipe JAR to load (repeatable) | — |\n| `--rewrite-config`                    | Path to `rewrite.yaml` for custom recipe compositions | `\u003cproject-dir\u003e/rewrite.yaml` |\n| `--output`, `-o`                      | Output mode: `diff`, `files`, or `report` | `diff` |\n| `--cache-dir`                         | Cache root for downloaded recipe JARs (stored under `\u003cpath\u003e/repository`). Project dependencies always resolve from `~/.m2/repository`. | `~/.rewriterunner/cache` |\n| `--config`                            | Path to tool config file (`rewriterunner.yml`) | `\u003cproject-dir\u003e/rewriterunner.yml`, then `~/.rewriterunner/rewriterunner.yml` |\n| `--dry-run`                           | Run recipe but do not write changes to disk | `false` |\n| `--skip-plugin-run`                   | Skip plugin-first execution; use full LST pipeline directly | `false` |\n| `--artifact-download-threads`         | Number of parallel artifact download threads | `5` |\n| `--subprocess-run-timeout`            | Timeout for build-tool subprocesses in the fallback LST pipeline. Accepts `ms`, `s`, `m`, `h`, `d`, or ISO-8601 values. | `120s` |\n| `--plugin-run-timeout`                | Timeout for plugin-first Gradle/Maven invocations. Accepts `ms`, `s`, `m`, `h`, `d`, or ISO-8601 values. | `10m` |\n| `--artifact-resolver-connect-timeout` | TCP connection timeout for Maven Resolver downloads. Accepts `ms`, `s`, `m`, `h`, `d`, or ISO-8601 values. | `30s` |\n| `--artifact-resolver-request-timeout` | Socket read/request timeout for Maven Resolver downloads. Accepts `ms`, `s`, `m`, `h`, `d`, or ISO-8601 values. | `60s` |\n| `--exclude-paths`                     | Comma-separated glob patterns of files to skip (e.g. `**/generated/**,**/*.md`). Forwarded to both the Stage 0 plugin (Maven: `-Drewrite.exclusions=…`; Gradle: `exclusion(...)` DSL) and to the LST fallback pipeline. Stage 0 also receives Docker/HCL/protobuf ownership exclusions. | — |\n| `--plain-text-masks`                  | Comma-separated glob patterns of otherwise-unhandled files to parse as plain text (e.g. `**/CODEOWNERS,**/*.txt`). Replaces the upstream default mask list when specified and is forwarded to both Stage 0 and the LST fallback pipeline. | upstream defaults |\n| `--no-maven-central`                  | Disable Maven Central; use only repositories from the config file | `false` |\n| `--info`                              | Enable INFO-level logging to stderr | `false` |\n| `--debug`                             | Enable DEBUG-level logging to stderr (overrides `--info`) | `false` |\n\n### Output modes\n\n**`--output diff`** (default) — prints unified diffs for each changed file:\n```diff\n--- a/src/main/java/Hello.java\n+++ b/src/main/java/Hello.java\n@@ -1,3 +1,5 @@\n public class Hello {\n-    public static void main(String[]args){System.out.println(\"hi\");}\n+    public static void main(String[] args) {\n+        System.out.println(\"hi\");\n+    }\n }\n```\n\n**`--output files`** — prints only the paths of changed files, one per line.\n\n**`--output report`** — writes `openrewrite-report.json` to the project directory:\n```json\n{\n  \"totalChanged\": 1,\n  \"results\": [\n    {\n      \"filePath\": \"src/main/java/Hello.java\",\n      \"diff\": \"...\",\n      \"isNewFile\": false,\n      \"isDeletedFile\": false\n    }\n  ],\n  \"parsedFileCount\": 1,\n  \"parseFailures\": [\n    {\n      \"path\": \"src/main/java/Broken.java\",\n      \"reason\": \"unterminated comment\",\n      \"parser\": \"JavaParser\"\n    }\n  ]\n}\n```\n\n`parsedFileCount` is the number of successfully parsed source files in the LST path,\nexcluding `ParseError` stubs. It is `null` for plugin-first runs because the in-process\nLST was not built.\n\n`estimatedTimeSaved` is available to library callers as\n`RunResult.executionDiagnostics.estimatedTimeSaved`, but is not serialized into\n`openrewrite-report.json`.\n\n`parseFailures` is empty when every file parsed cleanly. Each entry names the producer\nthat gave up on the entry along with a short reason. Two kinds of producers appear:\n\n- A canonical parser (`JavaParser`, `MavenParser`, `XmlParser`, …) — `path` is the\n  project-relative source file. A file can appear more than once if multiple parsers\n  tried and failed on it (the Maven POM → XML fallback path is the typical case).\n- A classpath-resolution stage (`DependencyResolutionStage`, `BuildFileParseStage`) —\n  `path` is the rejected Maven coordinate string itself (not a file path), and `reason`\n  is `\"illegal Maven coordinate\"`. Malformed coordinates encountered while assembling\n  the LST classpath are skipped rather than aborting the build.\n\n## Recipe Artifacts\n\nSpecify recipe JARs using Maven coordinates. The `--recipe-artifact` flag can be repeated to load multiple recipe modules.\n\n```bash\n# Load a single recipe module\n--recipe-artifact org.openrewrite.recipe:rewrite-spring:LATEST\n\n# Load multiple modules\n--recipe-artifact org.openrewrite.recipe:rewrite-migrate-java:LATEST \\\n--recipe-artifact org.openrewrite.recipe:rewrite-spring:LATEST\n```\n\n`LATEST` resolves to the most recent release. Specific versions (e.g. `2.21.0`) are also accepted.\n\nDownloaded recipe JARs are cached under `~/.rewriterunner/cache/repository` (or `--cache-dir/repository`) and reused on subsequent runs. They are stored separately from the project's own dependencies, which always resolve from `~/.m2/repository`.\n\nOnly compile/runtime JARs are downloaded for recipe artifacts — test-scoped and provided-scoped transitive dependencies of recipes are skipped.\n\n## Custom Recipe Compositions\n\nDefine composite recipes in a `rewrite.yaml` file in your project directory (or pass `--rewrite-config`):\n\n```yaml\n---\ntype: specs.openrewrite.org/v1beta/recipe\nname: com.example.MyMigration\ndisplayName: My Custom Migration\nrecipeList:\n  - org.openrewrite.java.migrate.UpgradeToJava21\n  - org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3\n  - org.openrewrite.java.format.AutoFormat\n```\n\nThen run it:\n\n```bash\njava -jar rewrite-runner-all.jar \\\n  --project-dir /path/to/project \\\n  --active-recipe com.example.MyMigration \\\n  --recipe-artifact org.openrewrite.recipe:rewrite-migrate-java:LATEST \\\n  --recipe-artifact org.openrewrite.recipe:rewrite-spring:LATEST\n```\n\n## Tool Config File\n\nCreate `rewriterunner.yml` to configure repositories and caching for your environment.\n\n**Default locations** (checked in order):\n1. `\u003cproject-dir\u003e/rewriterunner.yml` — project-level config\n2. `~/.rewriterunner/rewriterunner.yml` — global fallback, shared across all projects\n\nFile name matching is case-insensitive (e.g. `RewriteRunner.yml` also works). Override either default with `--config \u003cpath\u003e`.\nDuration values require units such as `30000ms`, `120s`, `10m`, `2h`, `1d`, or ISO-8601 values such as `PT2M`.\n\n```yaml\nrepositories:\n  - url: https://nexus.example.com/repository/maven-public\n    username: ${NEXUS_USER}\n    password: ${NEXUS_PASSWORD}\n\ncacheDir: ~/.rewriterunner/cache\n\ndownloadThreads: 5   # parallel artifact download threads (default: 5)\nprocessTimeout: 120s              # fallback LST build-tool subprocess timeout\npluginTimeout: 10m                # plugin-first rewriteDryRun/rewriteRun timeout\nrewriteGradlePluginVersion: 7.32.1\nrewriteMavenPluginVersion: 6.40.0\nresolverConnectTimeout: 30s       # Maven Resolver TCP connection timeout\nresolverRequestTimeout: 60s       # Maven Resolver socket/request timeout\n\nparse:\n  excludePaths:\n    - \"**/generated/**\"\n    - \"**/*.md\"\n  plainTextMasks:\n    - \"**/CODEOWNERS\"\n    - \"**/*.txt\"\n```\n\nEnvironment variable placeholders (`${VAR_NAME}`) are expanded at runtime.\n\n## Plugin-First Execution\n\nBefore building its own LST, the tool first attempts to apply the recipe through the official OpenRewrite plugin for the project's build tool:\n\n- **Gradle**: injects a temporary init script that applies the `org.openrewrite.rewrite` plugin, then runs `rewriteDryRun` and, when not in `--dry-run` mode, `rewriteRun`\n- **Maven**: invokes `org.openrewrite.maven:rewrite-maven-plugin` directly via `./mvnw`, `mvnw.cmd`, or system `mvn`\n\nThe dry-run goal always runs first so the tool can capture generated `rewrite.patch` files and format output in `diff`, `files`, or `report` mode. Gradle patches are read from `build/reports/rewrite/rewrite.patch`; Maven patches are pinned to a private report directory through `-DreportOutputDirectory`. All plugin patch paths are reported relative to the project root. If no patches contain changes, the run short-circuits with no changes. If plugin execution succeeds with changes, the in-process LST pipeline is skipped entirely.\n\nStage 0 also exposes `ExecutionDiagnostics.estimatedTimeSaved`. It requests OpenRewrite data-table export and reads the latest `SourcesFileResults` table when present; current Maven/Gradle plugin versions may only emit the same OpenRewrite-computed value in the `Estimate time saved` output line, so rewrite-runner falls back to that line. It never estimates the value from changed-file count.\n\nIf the plugin path fails for any reason (no build tool, plugin resolution failure, build error, recipe unavailable, timeout), the tool falls through silently to the fallback pipeline below.\n\nThis plugin-first stage is enabled by default. Existing callers that need the previous direct LST pipeline behavior should pass `--skip-plugin-run` or set `skipPluginRun(true)`.\n\nUse `--skip-plugin-run` to bypass this stage.\n\n## Resilient Parsing Pipeline\n\nThe tool uses a four-stage fallback pipeline to build the LST, ensuring recipes run even on projects with broken builds.\n\n### Stage 1 — Build tool classpath extraction\nInvokes the project's own build tool as a subprocess to extract the full compile classpath:\n- **Maven**: `mvn dependency:build-classpath -DincludeScope=compile`\n- **Gradle**: injects a temporary init script that prints `runtimeClasspath` file paths\n\nIf successful, the resulting JAR list is passed to `JavaParser` for full type attribution (resolves imports, method signatures, type hierarchies).\n\n### Stage 2 — Direct dependency resolution\nIf Stage 1 fails (broken build, no wrapper, timeout), the tool resolves dependencies without running the full build:\n- **Maven**: parses `pom.xml` using `maven-model`. Includes `compile`, `provided`, and `test` scopes; excludes `runtime` and `system` scopes — provided and test dependencies are included to support compile-time and test-source type resolution while runtime-only artifacts are skipped.\n- **Gradle**: runs `gradle dependencies` for the root project **and all declared subprojects** (discovered from `settings.gradle` / `settings.gradle.kts`), parsing the resolved dependency tree to get accurately resolved versions; falls back to best-effort static regex parsing of `build.gradle` / `build.gradle.kts` if Gradle cannot be invoked.\n\n\u003e **Note:** The `gradle dependencies` task only reports dependencies for the project it is applied to. Subprojects are queried explicitly (`:sub:dependencies`) so that multi-module builds are fully covered.\n\n**Direct deps only, no POM traversal.** Stage 2 downloads JARs only for the dependencies explicitly declared in the build file — it does not traverse transitive dependency graphs or fetch transitive POM files. This avoids hundreds of extra HTTP requests on a cold run. Missing transitive types appear as `JavaType.Unknown`, which OpenRewrite handles gracefully.\n\nResolved JARs are cached in `~/.m2/repository` (Maven default), so artifacts already downloaded by the project's own build are reused without re-downloading. Extra repositories from the tool config are also consulted.\n\n### Stage 3 — Static build file parse + POM traversal\nIf Stage 2 fails, the tool statically parses build files without invoking any subprocess, then resolves transitive dependencies via Maven Resolver POM traversal:\n- **Maven**: discovers all modules via `pom.xml` module declarations and directory walk, then resolves the full transitive dependency graph.\n- **Gradle**: statically parses `build.gradle(.kts)` files and version catalogs (`gradle/*.versions.toml`) using regex extraction, then resolves transitives via Maven Resolver POM traversal.\n\nThis stage provides full transitive dependency resolution without requiring a working build tool installation.\n\n### Stage 4 — Local cache scan\nIf all previous stages fail, the tool scans local Maven and Gradle caches:\n- `~/.m2/repository` for Maven-cached JARs\n- `~/.gradle/caches/modules-*/files-*/` for Gradle-cached JARs\n\nUnresolved types appear as `JavaType.Unknown` in the LST, but all structural, text-based, YAML, XML, and search recipes continue to work correctly.\n\n## Supported File Types\n\n| Extension | Parser |\n|-----------|--------|\n| `.java` | `JavaParser` (with classpath from fallback pipeline) |\n| `.kt`, `.kts` | `KotlinParser` (`.kts` augmented with Gradle DSL classpath) |\n| `.groovy`, `.gradle` | `GroovyParser` (`.gradle` augmented with Gradle DSL classpath) |\n| `.yaml`, `.yml` | `YamlParser` |\n| `.json` | `JsonParser` |\n| `pom.xml` | `MavenParser` (fully resolved — parent POMs, property interpolation, BOM imports; enables full `rewrite-maven` recipe catalog) |\n| `*.xml` (other) | `XmlParser` |\n| `.properties` | `PropertiesParser` |\n| `.toml` | `TomlParser` |\n| `.hcl`, `.tf`, `.tfvars` | `HclParser` |\n| `.proto` | `ProtoParser` |\n| `.dockerfile`, `.containerfile`, `Dockerfile*`, `Containerfile*` | `DockerParser` (matched both by extension and by filename prefix) |\n| Plain text mask matches, e.g. `CODEOWNERS`, `*.md`, `*.sh`, `*.txt` | `PlainTextParser` |\n\nAll supported extensions and the upstream default plain-text masks are parsed by default. Use `--exclude-paths` (CLI), `parse.excludePaths` (YAML), or `Builder.excludePaths(...)` (library) to skip specific paths via glob patterns. Use `--plain-text-masks`, `parse.plainTextMasks`, or `Builder.plainTextMasks(...)` to replace the plain-text mask list. Exclusions win, and specialized parsers take precedence over plain-text masks on the LST path. The resolved values are forwarded to the Stage 0 plugin invocation and to the LST fallback pipeline. On Stage 0 success, Docker/HCL/protobuf files are excluded from the plugin and handled by rewrite-runner's restricted specialized parser pass.\n\n### Automatically excluded directories\n\nThe following directories are always skipped during the file-system walk, regardless of configuration:\n\n`.git`, `build`, `target`, `node_modules`, `.gradle`, `.idea`, `out`, `dist`\n\nUse `parse.excludePaths` in `rewriterunner.yml`, `--exclude-paths` on the CLI, or `Builder.excludePaths(...)` in the library to skip additional paths.\n\n## Development\n\n```bash\n# Run all tests\n./gradlew test\n\n# Run a specific test class\n./gradlew test --tests \"io.github.skhokhlov.rewriterunner.output.ResultFormatterTest\"\n\n# Build and run locally\n./gradlew shadowJar\njava -jar cli/build/libs/cli-1.0-SNAPSHOT-all.jar --help\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskhokhlov%2Frewrite-runner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskhokhlov%2Frewrite-runner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskhokhlov%2Frewrite-runner/lists"}