jmove/docs/SKILL.md
loki5512344 ad1ad4f999
Some checks are pending
CI / lint (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
feat: SARIF 2.1.0 and Checkstyle XML reports for check/fix
--report FILE on check and fix writes the findings a run already
computed into CI-consumable formats chosen by extension: .sarif
(single run, %SRCROOT%-relative URIs, properties.autoFixable, rule
registry with descriptions) and .xml (Checkstyle, source=jmove.<rule>).
Unknown suffixes fail fast with INVALID_ARGUMENT; stdout and exit codes
are untouched, and clean runs write valid empty documents.

The check handler moved into cli::report (its format siblings); fix
emits the candidate list in dry-run and apply modes. Marketing angle
from the plan: auto-fix for what Checkstyle only reports.
2026-09-15 20:23:20 +02:00

7.4 KiB
Raw Blame History

jmove skill (for AI agents)

What this tool does

Moves or renames source files — or whole directories of them — inside a project and updates every import statement referencing them. Never breaks imports. Supported: TypeScript, JavaScript, Java (package declaration + all importers + the file move are kept in sync); Python/Go on the roadmap. Single binary, no LSP needed.

When to use

  • The user asks to move/rename/reorganize source files or packages.
  • You refactored a directory layout and imports now point at nothing.
  • You need to verify the project has no broken imports (check).

Commands

mv — move a file and rewrite its importers

jmove mv <source> <target> [--root DIR] [--source-root DIR] [--dry-run] [--json] [--no-git]

Always run --dry-run first and confirm the change set looks right. Moving onto an existing path fails with TARGET_EXISTS — choose another target (Phase 1 has no overwrite mode).

Directory moves: mv <dir> <target> relocates every indexed file under <dir> mirrored under <target> in one atomic batch. --json adds moved_files[] (from/to per file) and left_behind[] — real files under <dir> that are not indexable and deliberately stay where they are. Emptied source directories are pruned; a directory with leftovers is not. A source that is nested in its own target fails with PLAN_REJECTED.

Git integration: inside a git repository, a tracked file is renamed with git mv so the rename is staged (history-preserving git log --follow / git diff -M work). Untracked files, non-repositories and --no-git fall back to a plain filesystem rename. The import rewrites land in the working tree unstaged either way — stage or commit them yourself. --json reports the choice as moved_via ("git"/"fs") and, on a dry-run, would_move_via.

tsconfig paths: compilerOptions.paths prefixes (@/*, @cfg exact keys, baseUrl-relative) map bare specifiers into the project graph, so aliased imports participate in mv — the rewrite keeps the alias shape while the new file stays inside the mapped tree and falls back to a relative specifier otherwise. extends chains and 2nd+ candidate lists are not followed (v1); an invalid tsconfig silently means "no aliases".

Non-import references: before writing, mv scans markdown/config/text files and source strings for mentions of moved paths and old specifiers. Such references are never rewritten (each format has its own semantics); they are reported as non_import_refs[] (file, line, token, kind, text) in mv --json — dry-run included — and on stderr for humans. Exit code never changes; surface the list to the user.

check — find broken imports and Java layout errors

jmove check [--root DIR] [--json] [--report FILE]

Run after any move (or any edit) to validate project consistency. Reports broken relative imports and Java files whose single public type is named differently from the file (each finding carries the exact jmove mv that renames it; exit code 2 covers both kinds).

report files — SARIF and Checkstyle for CI/IDE

check and fix accept --report FILE; the format is chosen by the file name suffix: .sarif → SARIF 2.1.0 (single run, %SRCROOT%-relative artifact URIs, properties.autoFixable), .xml → Checkstyle XML (source="jmove.<rule>" with / mapped to .). Unknown suffixes fail before any work with INVALID_ARGUMENT. The report mirrors what the run already computed (fix: candidates with their applied/manual status; dry-run included), stdout and exit codes stay as without --report, and clean runs write valid empty documents. Rule ids: the four fix rules above, plus broken-import and java/class-name-mismatch.

fix — auto-repair import problems

jmove fix [--root DIR] [--rule ID] [--dry-run] [--json] [--report FILE]

Runs the deterministic rules over the whole project and applies the repairs through the same atomic engine as mv (dry-run diff, rollback, exit codes). Current Java rules: java/unused-import (deletes single-type imports whose name is provably unreferenced), java/missing-import (inserts the import of a project class used by simple name — unique FQN candidate required) and java/import-order (Google style: statics first, then single-type, ASCII-sorted, duplicates dropped). TS/JS: ts/unused-import deletes whole statements whose every bound name is unreferenced; mixed statements (one name live) stay untouched because ESM imports carry module side effects, so partial specifier surgery is deliberately off. Unknown --rule fails with INVALID_ARGUMENT and lists the known ids. A candidate the engine cannot prove safe is reported with "applied": false and a candidates array of FQN options — resolve it yourself (pick one, add the import) and re-run; fix never guesses. When two rules want the same bytes (order rewrite vs an unused deletion), the urgent rule applies and the other is deferred ("applied": false, "skipped") — re-run fix until it reports "nothing to change" (converges in 2–3 runs).

Recommended: fix --dry-run --json, inspect files[].fixes[] (rule, line, message, applied, optional candidates), then apply without --dry-run. For Java projects the practical loop is: mv → fix → fix again if anything was "skipped" → check.

Java specifics

  • Moving a .java file rewrites three coordinated edits: its own package declaration, every import <fqn> naming the class (static member imports keep their member suffix) and the physical move.
  • The target must be a .java path under the same source root (src/main/java, src, …) and its directory maps to the new package. Anything else exits 1 with code PLAN_REJECTED.
  • A class in the default package (no package declaration) can only be renamed inside its directory.
  • check never reports unresolved Java imports (jdk, third-party, pkg.*) as broken — they are external by design, like TS bare specifiers.
  • Monorepos with duplicate packages (guava vs android/guava under one --root): the same FQN exists in parallel trees, so resolution picks the sorted-first copy and a move rewrites the wrong files. Pass the global --source-root DIR to index (and fix/move) only that subtree; each tree is then self-contained and correct. Without the flag behavior is unchanged; --source-root on a non-directory exits 1.
  1. jmove mv <src> <target> --dry-run --json — preview.
  2. Inspect affected_files; if unexpected, abort and ask the user.
  3. jmove mv <src> <target> --json — apply (atomic, rolls back on error).
  4. Java moves: jmove fix --json — heal bare same-package references left behind by the move (they break javac otherwise).
  5. jmove check --json — verify nothing is broken.

Output format (--json)

Every response: { "status": "ok" | "dry_run" | "error", "operation": "...", ... }. Errors carry a stable code (TARGET_EXISTS, SOURCE_NOT_FOUND, ...) and a hint describing the next action. On success, mv reports changed_files with line-level old/new import diffs and counters (moved, updated_imports).

Exit codes

  • 0 — success
  • 1 — operation failed (read --json error or stderr)
  • 2 — check found broken imports or Java name mismatches

Rules of use

  • Never run mv without a prior --dry-run in the same session.
  • After every successful mv, run check; treat exit code 2 as a failed refactor.