| name | batch-refactor |
| description | Batch operations across files with confidence-based auto-apply. Use for renaming, search-replace, refactoring code, updating text/markdown, migrating terminology, and API migrations. Run `bun tools/refactor.ts --help` for detailed command reference. |
| allowed-tools | Bash, Read, Edit, Grep, Glob, AskUserQuestion |
Batch Operations Skill
Quick start: Run bun vendor/bearly/tools/refactor.ts --help for command reference and examples.
Use this skill when the user wants to make changes across multiple files:
- Code refactoring: rename functions, variables, types
- Text/markdown updates: change terminology, update docs
- File operations: batch rename files with import path updates
- Terminology migrations: widget→gadget, vault→repo
- API migrations: old API patterns → new API patterns (LLM-powered)
Trigger phrases:
- "rename X to Y everywhere"
- "change all X to Y"
- "refactor X across the codebase"
- "batch replace"
- "update terminology"
- "migrate from X to Y"
- "rename function/variable everywhere"
- "change wording in all files"
- "migrate API" / "update API usage"
- "change how we call X"
- "update all uses of X to new pattern"
- "convert old pattern to new pattern"
Why Use Batch Refactor (Not Manual Edit)
⛔ NEVER use these for batch changes: sed, awk, perl, python -c, or manual Edit loops.
These tools lack checksums, miss edge cases, and can corrupt files. See "Tips & Tricks → STOP: Manual Edit Tools" below.
ALWAYS use this tool instead of manual editing when:
| Situation | Manual Edits | Batch Refactor |
|---|
| Rename a function used in 47 files | 47 separate Edit calls | 1 command |
| Rename parameter in destructuring patterns | Miss edge cases | Catches all patterns |
| Update terminology across codebase | Hours of work | Minutes |
| Rename file + update all imports | Break builds | Atomic, safe |
Example: Why Manual Editing Fails
Task: Rename createWidget to createGadget
Manual approach (WRONG):
const { createWidget } = api
const init = createWidget
type Factory = typeof createWidget
async ({ createWidget }: Deps) => {}
Batch refactor approach (CORRECT):
bun tools/refactor.ts rename.batch --pattern '/^createWidget$/' --replace createGadget --output edits.json
bun tools/refactor.ts editset.apply edits.json
Regex Patterns — Case Handling Is Explicit
CRITICAL: all --pattern and --from arguments must be regex literals in the form /pattern/flags.
This is a forcing function: plain strings hide a decision about case-sensitivity, and the wrong
default silently corrupts camelCase identifiers. The regex form makes you choose:
| Use case | Flag | Behavior |
|---|
| Code identifier rename | no /i | exact match, literal replacement — preserves the exact casing you typed |
| Prose terminology migration | /i | case-insensitive match, case-preserving replacement (widget→Gadget, WIDGET→GADGET) |
bun tools/refactor.ts pattern.replace --pattern '/screenRect/' --replace 'scrollRect'
bun tools/refactor.ts rename.batch --pattern '/^useContentRect$/' --replace 'useBoxRect'
bun tools/refactor.ts migrate --from '/contentRect/' --to 'boxRect' --dry-run
bun tools/refactor.ts pattern.replace --pattern '/widget/i' --replace 'gadget'
bun tools/refactor.ts pattern.replace --pattern '/\buseLayout\b/' --replace 'useBoxRect'
Why regex-first: every --pattern invocation becomes a moment where you look at the flags
and consciously pick. Without this, you write --pattern screenRect and the tool silently
downcases your replacement to scrollrect. The extra characters (/ and optional /i) are a
small cost for eliminating a whole class of bugs.
The g flag is implicit (always multi-match within a file).
Quick Reference: When to Use Each Command
| You want to... | Command | Example |
|---|
| Rename TypeScript function/variable | rename.batch | --pattern '/^createWidget$/' --replace createGadget |
| Rename TypeScript type/interface | rename.batch | --pattern '/^WidgetConfig$/' --replace GadgetConfig |
| Rename files | file.rename | --pattern widget --replace repo --glob "**/*.ts" |
| Update text in markdown | pattern.replace --backend ripgrep | --pattern '/widget/i' --replace repo --glob "**/*.md" |
| Full terminology migration | migrate | --from '/widget/i' --to repo |
| API migration (complex patterns) | pattern.migrate | --patterns "oldApi()" --prompt "migrate to newApi" |
Comprehensive Migration Workflow
For large terminology migrations (e.g., "rename widget to repo"), follow this phased approach:
Phase 1: Conflict Analysis (BEFORE any changes)
CRITICAL: Analyze ALL conflicts before making ANY changes.
bun tools/refactor.ts file.rename --pattern widget --replace repo --glob "**/*.ts" --check-conflicts
bun tools/refactor.ts rename.batch --pattern '/widget/i' --replace repo --check-conflicts
ls **/repo*.ts 2>/dev/null || echo "No existing repo files"
For each conflict, document resolution:
| Conflict | Resolution |
|---|
widget.ts → repo.ts (exists) | Merge and delete |
createWidget → createRepo (exists) | Update references, keep new |
Never use --skip without explicit user approval.
Phase 2: File Renames
Rename files FIRST (before symbol renames) because:
- Import paths need to be valid for ts-morph to work
- File renames update import paths automatically
bun tools/refactor.ts file.rename --pattern widget --replace repo \
--glob "**/*.{ts,tsx}" \
--output file-editset.json
bun tools/refactor.ts file.apply file-editset.json --dry-run
bun tools/refactor.ts file.apply file-editset.json
Phase 3: Symbol Renames (TypeScript)
After files are renamed, rename symbols:
bun tools/refactor.ts rename.batch --pattern '/widget/i' --replace repo \
--output symbol-editset.json
bun tools/refactor.ts editset.apply symbol-editset.json --dry-run
bun tools/refactor.ts editset.apply symbol-editset.json
Phase 4: Text/Comment Renames
Rename remaining mentions in comments, strings, markdown:
bun tools/refactor.ts pattern.replace --pattern '/widget/i' --replace repo \
--glob "**/*.{ts,tsx}" \
--backend ripgrep \
--output text-editset.json
bun tools/refactor.ts pattern.replace --pattern '/widget/i' --replace repo \
--glob "**/*.md" \
--backend ripgrep \
--output docs-editset.json
bun tools/refactor.ts editset.apply text-editset.json --dry-run
bun tools/refactor.ts editset.apply text-editset.json
Phase 5: Vendor Submodules
For changes in git submodules:
cd vendor/<submodule>
bun ../tools/plugins/batch/tools/refactor.ts rename.batch \
--pattern '/widget/i' --replace repo --check-conflicts
git add -A
git commit -m "refactor: rename widget → repo"
git push
cd ../..
git add vendor/<submodule>
git commit -m "chore(vendor): update <submodule> with repo terminology"
Phase 6: Verification
grep -ri widget . --include="*.ts" --include="*.tsx" | grep -v node_modules | wc -l
bun tsc --noEmit
bun fix
bun run test:all
Tool Selection
| What you're changing | File Type | Backend | Command |
|---|
| File names | any | file-ops | file.rename |
| TypeScript/JS identifiers | .ts, .tsx, .js, .jsx | ts-morph | rename.batch |
| API patterns (complex) | .ts, .tsx | LLM | pattern.migrate |
| Go, Rust, Python structural patterns | .go, .rs, .py | ast-grep | pattern.replace |
| JSON/YAML values | .json, .yaml | ast-grep | pattern.replace |
| Text/markdown/comments | .md, .txt, any | ripgrep | pattern.replace |
| Wiki links only | .md | wikilink | wikilink.rename |
| package.json paths | package.json | package-json | package.rename |
| tsconfig.json paths | tsconfig*.json | tsconfig-json | tsconfig.rename |
file.rename auto-detects file type and updates references:
.ts/.tsx/.js/.jsx → updates import paths
.md/.markdown/.mdx → updates [[wikilinks]] (Obsidian, Foam, etc.)
package.json → updates exports, main, types, bin paths
tsconfig.json → updates paths mappings, includes, references
CRITICAL for TypeScript: Always use ts-morph (via rename.batch) for identifiers. It handles destructuring, arrow function params, and nested scopes that text-based tools miss.
Dependencies:
- ts-morph: bundled (no external CLI)
- ast-grep: requires
sg CLI (brew install ast-grep)
- ripgrep: requires
rg CLI (usually pre-installed)
CLI Reference
File Operations
| Command | Purpose |
|---|
file.find --pattern <p> --replace <r> [--glob] | Find files to rename |
file.rename --pattern <p> --replace <r> [--glob] [--output] [--check-conflicts] | Create file rename proposal |
dir.move --old <prefix> --new <prefix> [--glob] [--exclude-glob]... [--check-conflicts] | Move a directory/prefix + update refs |
file.verify <file> | Verify file editset can be applied |
file.apply <file> [--dry-run] | Apply file renames OR a dir move |
dir.move (directory/prefix move). Moves every file under --old to --new and rewrites all
references (imports, wikilinks, package.json, tsconfig), with link edits deduped. --exclude-glob
skips rewriting links inside matching files — the bead-safety lever so @km/**/*.md bead nodes
keep their wikilinks verbatim. The output is a file-rename editset; apply it with file.apply.
bun tools/refactor.ts dir.move --old apps/silvercode --new apps/ag \
--exclude-glob '@km/**/*.md' --output /tmp/move.json
bun tools/refactor.ts file.apply /tmp/move.json --dry-run
bun tools/refactor.ts file.apply /tmp/move.json
TypeScript/JavaScript (ts-morph)
| Command | Purpose |
|---|
symbol.at <file> <line> [col] | Find symbol at location |
refs.list <symbolKey> | List all references to a symbol |
symbols.find --pattern <regex> | Find symbols matching pattern |
rename.propose <key> <new> | Single symbol rename proposal |
rename.batch --pattern <p> --replace <r> [--check-conflicts] | Batch rename proposal |
Multi-Language (ast-grep/ripgrep)
| Command | Purpose |
|---|
pattern.find --pattern <p> [--glob]... [--exclude-glob]... [--backend] | Find structural patterns |
pattern.replace --pattern <p> --replace <r> [--glob]... [--exclude-glob]... [--backend] | Pattern replace proposal |
pattern.migrate --patterns <p1,p2> --prompt <text> [--glob] | LLM-powered API migration |
backends.list | List available backends |
Repeatable include/exclude globs (ripgrep backend). --glob (alias --include-glob) is
repeatable, and --exclude-glob excludes files via ripgrep's native ! semantics. This is the
bead-safety lever for the reorg: rewrite code while leaving @km/**/*.md bead nodes untouched.
bun tools/refactor.ts pattern.replace --pattern '/@km\/code/' --replace '@ag/code' \
--backend ripgrep --glob '**/*.ts' --glob '**/*.tsx' --exclude-glob '@km/**/*.md'
bun tools/refactor.ts migrate --from '/@km\/code/' --to '@ag/code' --exclude-glob '@km/**/*.md'
Editset Operations
| Command | Purpose |
|---|
editset.select <file> --include/--exclude | Filter editset refs |
editset.verify <file> | Check editset can be applied |
editset.apply <file> [--dry-run] | Apply with checksum verification |
Package.json Operations
| Command | Purpose |
|---|
package.find --target <file> | Find package.json refs to a file |
package.rename --old <path> --new <path> | Update paths when a file is renamed |
package.dep-rename --from <name> --to <name> | Rename a package identifier (deps + name) |
package.broken | Find broken paths in package.json |
package.rename vs package.dep-rename: package.rename rewrites path fields
(main/module/types/exports/…) when a file moves. package.dep-rename renames a package
name (e.g. @km/code → @ag/code) across the declaring package's name field and every
dependency-map key (dependencies/devDependencies/peerDependencies/optionalDependencies/
peerDependenciesMeta/overrides) — it never touches paths, prose, or version ranges. Use it for
namespace/scope renames.
TSConfig.json Operations
| Command | Purpose |
|---|
tsconfig.find --target <file> | Find tsconfig.json refs to a file |
tsconfig.rename --old <path> --new <path> | Update paths when file renamed |
Concrete Examples by Operation Type
Example 1: Batch File Rename
Scenario: Rename all files containing "user-service" to "account-service"
bun tools/refactor.ts file.find --pattern "user-service" --replace "account-service" --glob "**/*.ts"
bun tools/refactor.ts file.rename --pattern "user-service" --replace "account-service" \
--glob "**/*.ts" --check-conflicts
bun tools/refactor.ts file.rename --pattern "user-service" --replace "account-service" \
--glob "**/*.ts" --output file-renames.json
bun tools/refactor.ts file.apply file-renames.json --dry-run
bun tools/refactor.ts file.apply file-renames.json
Result:
src/user-service.ts → src/account-service.ts
src/testing/mock-user-service.ts → src/testing/mock-account-service.ts
UserServiceConfig.ts → AccountServiceConfig.ts (case preserved)
Example 2: Import Path Updates (after file renames)
Scenario: Update all imports that reference renamed files
bun tools/refactor.ts pattern.replace \
--pattern "user-service" \
--replace "account-service" \
--glob "**/*.ts" \
--backend ripgrep \
--output import-updates.json
bun tools/refactor.ts editset.apply import-updates.json --dry-run
bun tools/refactor.ts editset.apply import-updates.json
Before:
import { createUser } from "./user-service"
import { UserService } from "../services/user-service"
After:
import { createUser } from "./account-service"
import { UserService } from "../services/account-service"
Example 3: TypeScript Symbol Rename (ts-morph)
Scenario: Rename function createWidget to createGadget across codebase
bun tools/refactor.ts rename.batch --pattern "createWidget" --replace "createGadget" --check-conflicts
bun tools/refactor.ts rename.batch --pattern "createWidget" --replace "createGadget" \
--output symbol-renames.json
bun tools/refactor.ts editset.apply symbol-renames.json --dry-run
bun tools/refactor.ts editset.apply symbol-renames.json
Handles correctly:
export function createWidget(config: Config) { } → createGadget
const createWidget = (opts) => { } → createGadget
const { createWidget } = api → createGadget
function init({ createWidget }: Deps) { } → createGadget
type Factory = typeof createWidget → createGadget
Example 4: Type/Interface Rename
Scenario: Rename ApiClient to HttpClient across codebase
bun tools/refactor.ts rename.batch --pattern "ApiClient" --replace "HttpClient" --check-conflicts
bun tools/refactor.ts rename.batch --pattern "ApiClient" --replace "HttpClient" \
--output type-renames.json
bun tools/refactor.ts editset.apply type-renames.json
Handles correctly:
export interface ApiClient { } → HttpClient
type Client = ApiClient → HttpClient
const client: ApiClient = ... → HttpClient
function fetch<T extends ApiClient>() → HttpClient
import type { ApiClient } from "./api" → HttpClient
Example 5: Text/Comment/String Replace (ripgrep)
Scenario: Update documentation and comments from "widget" to "gadget"
bun tools/refactor.ts pattern.replace \
--pattern "widget" \
--replace "gadget" \
--glob "**/*.md" \
--backend ripgrep \
--output docs-updates.json
bun tools/refactor.ts pattern.replace \
--pattern "widget" \
--replace "gadget" \
--glob "**/*.ts" \
--backend ripgrep \
--output comment-updates.json
bun tools/refactor.ts editset.apply docs-updates.json --dry-run
bun tools/refactor.ts editset.apply docs-updates.json
Updates:
const msg = "Widget not found" → "Gadget not found"
→ Gadget factory
# Widget Guide → Gadget Guide
Create a widget with... → Create a gadget with...
Example 6: Structural Pattern Replace (ast-grep)
Scenario: Migrate Go logging from fmt.Println to log.Info
bun tools/refactor.ts pattern.replace \
--pattern 'fmt.Println($MSG)' \
--replace 'log.Info($MSG)' \
--glob "**/*.go" \
--backend ast-grep \
--output go-logging.json
bun tools/refactor.ts editset.apply go-logging.json
Before:
fmt.Println("Starting server")
fmt.Println(err.Error())
After:
log.Info("Starting server")
log.Info(err.Error())
Example 7: Complete Terminology Migration
Scenario: Migrate entire codebase from "user" terminology to "account"
bun tools/refactor.ts file.rename --pattern "user" --replace "account" --check-conflicts
bun tools/refactor.ts rename.batch --pattern "user" --replace "account" --check-conflicts
bun tools/refactor.ts file.rename --pattern "user" --replace "account" \
--glob "**/*.ts" --output phase2-files.json
bun tools/refactor.ts file.apply phase2-files.json
bun tools/refactor.ts rename.batch --pattern "user" --replace "account" \
--output phase3-symbols.json
bun tools/refactor.ts editset.apply phase3-symbols.json
bun tools/refactor.ts pattern.replace --pattern "user" --replace "account" \
--glob "**/*.ts" --backend ripgrep --output phase4-text.json
bun tools/refactor.ts editset.apply phase4-text.json
bun tools/refactor.ts pattern.replace --pattern "user" --replace "account" \
--glob "**/*.md" --backend ripgrep --output phase5-docs.json
bun tools/refactor.ts editset.apply phase5-docs.json
grep -ri user . --include="*.ts" | wc -l
bun tsc --noEmit
bun fix
bun run test:all
Example 8: Selective Rename with Filtering
Scenario: Rename only specific occurrences, not all
bun tools/refactor.ts rename.batch --pattern "config" --replace "options" \
--output full-editset.json
bun tools/refactor.ts editset.select full-editset.json \
--include "src/core/**" \
--exclude "src/core/legacy/**" \
--output filtered-editset.json
bun tools/refactor.ts editset.apply filtered-editset.json
Case Preservation
The tool preserves case during renames:
| Original | Pattern | Replacement | Result |
|---|
widget | widget | repo | repo |
Repo | widget | repo | Repo |
REPO | widget | repo | REPO |
widgetPath | widget | repo | repoPath |
WidgetConfig.ts | widget | repo | GadgetConfig.ts |
Conflict Resolution
Never skip conflicts without understanding them.
File Conflicts
| Conflict Type | Resolution Strategy |
|---|
| Target exists (duplicate) | Merge content, delete source |
| Target exists (different) | Rename to avoid collision |
| Same path (no-op) | Skip (no change needed) |
Symbol Conflicts
| Conflict Type | Resolution Strategy |
|---|
| Target name exists | Check if same symbol (safe to merge) or different (needs rename) |
| Multiple symbols same name | May be scoped (function-local vs module) - often safe |
Process:
- Run
--check-conflicts first
- Document each conflict and its resolution
- Get user approval on resolution strategy
- Execute with explicit handling (no blind --skip)
Safety Checks
Before making batch changes:
git rev-parse --is-inside-work-tree 2>/dev/null
git status --porcelain
| Situation | Action |
|---|
| Git repo, clean working tree | ✅ Proceed |
| Git repo, uncommitted changes | ⚠️ Ask user to commit first |
| Not a git repo | ⚠️ Warn: no undo available |
Context Gathering
Before making changes, gather project context:
-
Read CLAUDE.md - look for:
- Mentioned migrations or refactoring plans
- ADR references
- Terminology notes
-
Check for migration scripts (optional):
scripts/check-migration.ts or similar
- May have ALLOWED_PATTERNS for exclusions
-
Understand scope:
grep -ri <pattern> . --include="*.ts" | wc -l
find . -name "*<pattern>*" -not -path "./node_modules/*"
Confidence Philosophy
Be aggressive. Tests catch mistakes.
| Context | Confidence |
|---|
Our code (const widgetRoot) | HIGH |
Our compound identifier (widgetHelper) | HIGH |
Our error message ("widget not found") | HIGH |
External reference ("Obsidian widget") | LOW - may need to keep |
URL/path (widget.example.com) | LOW |
Default to HIGH unless clearly external.
Why ast-grep Fails for TypeScript Identifiers
ast-grep misses TypeScript-specific patterns:
const widgetDir = "/path"
interface TestEnv { widgetDir: string }
({ widgetDir }) => { ... }
Rule: If it shows up in "Find All References" in your IDE, use ts-morph.
Example: Complete Migration
User request: "rename widget to repo everywhere"
Claude's plan:
-
Analyze scope
grep -ri widget . --include="*.ts" | wc -l
find . -name "*widget*" -not -path "./node_modules/*"
-
Check ALL conflicts
bun tools/refactor.ts file.rename --pattern widget --replace repo --check-conflicts
bun tools/refactor.ts rename.batch --pattern '/widget/i' --replace repo --check-conflicts
-
Document conflict resolutions (ask user if unclear)
-
Execute in phases:
- Phase 2: File renames
- Phase 3: Symbol renames
- Phase 4: Text/comment renames
- Phase 5: Vendor submodules
-
Verify:
grep -ri widget . --include="*.ts" | wc -l
bun tsc --noEmit
bun fix
bun run test:all
Important Rules
- Check conflicts FIRST - never blind rename
- File renames BEFORE symbol renames - import paths must be valid
- Use editset workflow for TypeScript identifiers
- Always run tsc after batch changes
- Preview with --dry-run before applying
- Trust checksums - editset won't apply to modified files
- Vendor submodules - commit and push separately, then update reference
- Be aggressive - apply all matches, let tests catch mistakes
LLM Patch Workflow
Editsets now include enriched context fields that allow an LLM to review and selectively modify replacements before applying. This enables intelligent, context-aware refactoring decisions.
Enriched Reference Fields
Each reference in an editset includes:
| Field | Type | Description |
|---|
kind | "call" | "decl" | "type" | "string" | "comment" | What kind of reference this is |
scope | string | null | Enclosing function/class name, or null for module-level |
ctx | string[] | Array of context lines with ► marker on the matching line |
replace | string | null | null to skip this reference, string for custom replacement |
Workflow
bun tools/refactor.ts rename.batch --pattern '/widget/i' --replace repo -o editset.json
bun tools/refactor.ts editset.patch editset.json <<'EOF'
{ "b2c3": "Repository", "c3d4": null }
EOF
bun tools/refactor.ts editset.apply editset.json
Example: Selective Replacement
Given an editset with these references:
{
"refs": [
{
"id": "a1b2",
"kind": "decl",
"scope": "createWidget",
"replace": "repo"
},
{ "id": "b2c3", "kind": "type", "scope": null, "replace": "repo" },
{ "id": "c3d4", "kind": "comment", "scope": null, "replace": "repo" }
]
}
An LLM might decide:
- Keep
a1b2 as-is (standard replacement)
- Change
b2c3 to "Repository" (capitalize for type name)
- Set
c3d4 to null (skip comment, it refers to external Obsidian widget)
bun tools/refactor.ts editset.patch editset.json <<'EOF'
{ "b2c3": "Repository", "c3d4": null }
EOF
Context Lines
The ctx field shows surrounding lines with the match marked:
{
"ctx": [" // Create a new widget for the user", "► const vault = createVault(config)", " return widget"]
}
This helps LLMs understand whether a reference is:
- Internal code (safe to rename)
- External reference (may need to preserve)
- Documentation (may need different wording)
Migrate Command
The migrate command orchestrates a full terminology migration in phases:
bun tools/refactor.ts migrate --from '/widget/i' --to repo --dry-run
bun tools/refactor.ts migrate --from '/widget/i' --to repo
bun tools/refactor.ts migrate --from '/widget/i' --to repo --output ./my-editsets
bun tools/refactor.ts migrate --from '/widget/i' --to repo --glob "**/*.{ts,tsx,md}"
What Migrate Does
| Phase | Description | Output File |
|---|
| 1. File renames | Rename files containing pattern | 01-file-renames.json |
| 2. Symbol renames | TypeScript identifiers via ts-morph | 02-symbol-renames.json |
| 3. Text patterns | Comments, strings via ripgrep | 03-text-patterns.json |
After running, review each editset and apply:
bun tools/refactor.ts file.apply .editsets/01-file-renames.json --dry-run
bun tools/refactor.ts file.apply .editsets/01-file-renames.json
bun tools/refactor.ts editset.apply .editsets/02-symbol-renames.json --dry-run
bun tools/refactor.ts editset.apply .editsets/02-symbol-renames.json
bun tools/refactor.ts editset.apply .editsets/03-text-patterns.json --dry-run
bun tools/refactor.ts editset.apply .editsets/03-text-patterns.json
Migrate Command Flags
| Flag | Description | Default |
|---|
--from <pattern> | Pattern to match (required) | - |
--to <replacement> | Replacement string (required) | - |
--glob <glob> | File glob filter | **/*.{ts,tsx} |
--dry-run | Preview without creating editsets | false |
--output <dir> | Output directory for editsets | .editsets |
LLM-Powered API Migration (pattern.migrate)
For complex API migrations where simple find/replace isn't enough, use pattern.migrate. This command:
- Searches for patterns using ripgrep
- Gathers context around each match
- Sends to LLM in one call to determine correct replacements
- Generates editset for review and application
When to Use pattern.migrate
Use pattern.migrate instead of pattern.replace when:
- Transformations require understanding context (e.g., adding
await, changing variable names)
- Multiple related patterns need coordinated changes
- Replacements involve value mapping (e.g., ANSI codes → key names)
- The old and new patterns have different structures (destructuring → single variable)
Example: Test Framework API Migration
bun tools/refactor.ts pattern.migrate \
--patterns "lastFrame(),stdin.write,= render(" \
--glob "**/*.test.tsx" \
--prompt "Migrate old render() API to new App API:
- const { lastFrame, stdin } = render(...) → const app = render(...)
- lastFrame() → app.ansi
- stripAnsi(lastFrame()) → app.text
- stdin.write('\\x1b[A') → await app.press('ArrowUp')
- stdin.write('\\x1b[B') → await app.press('ArrowDown')" \
--output /tmp/migrate.json
jq '.refs[:5]' /tmp/migrate.json
bun tools/refactor.ts editset.apply /tmp/migrate.json
Command Flags
| Flag | Description | Default |
|---|
--patterns <p1,p2,...> | Comma-separated search patterns (required) | - |
--prompt <text> | Migration instructions for LLM (required) | - |
--glob <glob> | File filter | **/*.{ts,tsx} |
--output <file> | Editset output file | /tmp/migrate.json |
--model <model> | Override LLM model | best available |
--dry-run | Preview prompt without calling LLM | false |
Workflow
bun tools/refactor.ts pattern.migrate \
--patterns "oldPattern" \
--glob "**/*.ts" \
--prompt "Migrate to new pattern" \
--dry-run
bun tools/refactor.ts pattern.migrate \
--patterns "oldPattern" \
--glob "**/*.ts" \
--prompt "Migrate to new pattern" \
--output /tmp/migrate.json
jq '.refs | length' /tmp/migrate.json
jq '.refs[:3]' /tmp/migrate.json
bun tools/refactor.ts editset.apply /tmp/migrate.json
bun tsc --noEmit && bun run test:fast
Writing Good Migration Prompts
The LLM sees each match with ~3 lines of context. Write prompts that:
- List all transformation rules clearly with before → after
- Include edge cases (e.g., what to do with
stripAnsi() wrappers)
- Specify async handling if needed (when to add
await)
- Include value mappings if applicable (ANSI codes to key names)
--prompt "Migrate test API:
- const { lastFrame, stdin } = render(...) → const app = render(...)
- lastFrame() → app.ansi
- stripAnsi(lastFrame()) → app.text (remove stripAnsi wrapper)
- stdin.write('x') → await app.press('x')
- stdin.write('\\x1b[A') → await app.press('ArrowUp')
- stdin.write('\\x1b[B') → await app.press('ArrowDown')
- stdin.write('\\x1b[C') → await app.press('ArrowRight')
- stdin.write('\\x1b[D') → await app.press('ArrowLeft')
- stdin.write('\\r') → await app.press('Enter')"
Enriched Editset JSON Format
When an LLM reviews an editset, it sees enriched context for each reference:
{
"id": "rename-widget-to-repo-1706123456789",
"operation": "rename",
"from": "widget",
"to": "repo",
"refs": [
{
"refId": "a1b2c3d4",
"file": "src/storage.ts",
"line": 45,
"range": [45, 12, 45, 17],
"kind": "call",
"scope": "initStorage",
"ctx": [" function initStorage() {", "► const root = createVault(config);", " return root;"],
"replace": "repo",
"preview": "const root = createWidget(config);",
"checksum": "abc123...",
"selected": true
},
{
"refId": "b2c3d4e5",
"file": "src/errors.ts",
"line": 12,
"range": [12, 25, 12, 30],
"kind": "string",
"scope": "errorHandler",
"ctx": ["► throw new Error(\"vault not found\");"],
"replace": "repo",
"preview": "throw new Error(\"widget not found\");",
"checksum": "def456...",
"selected": true
}
],
"edits": [
],
"createdAt": "2024-01-24T12:00:00.000Z"
}
Field Reference
| Field | Type | Description |
|---|
refId | string | Stable identifier for this reference |
kind | "call" | "decl" | "type" | "string" | "comment" | Semantic kind |
scope | string | null | Enclosing function/class, or null for module-level |
ctx | string[] | Context lines with ► marker on match line |
replace | string | null | Replacement text, or null to skip |
line | number | 1-indexed line number |
range | [number, number, number, number] | [startLine, startCol, endLine, endCol] |
editset.patch Command
Apply LLM-generated patches to editsets via stdin (heredoc):
bun tools/refactor.ts editset.patch editset.json <<'EOF'
{
"b2c3d4e5": "repository",
"c3d4e5f6": null
}
EOF
cat my-patch.json | bun tools/refactor.ts editset.patch editset.json
bun tools/refactor.ts editset.patch editset.json --output patched.json <<'EOF'
{ "b2c3": null }
EOF
Patch Format
The patch is a simple JSON object mapping refIds to actions:
{
"refId1": "custom replacement",
"refId2": null
}
| Patch Value | Action |
|---|
"string" | Use this replacement text |
null | Skip this reference (don't apply) |
| (not in patch) | Apply with default to replacement |
Full Editset Patch
You can also pass a full editset with modified replace fields:
{
"refs": [
{ "refId": "a1b2", "replace": "Repository" },
{ "refId": "b2c3", "replace": null }
]
}
The patch command extracts refId and replace from each ref.
More Examples: Edge Cases Manual Editing Misses
Example 9: Destructuring Parameters (ts-morph REQUIRED)
Scenario: Rename widgetPath to repoPath — appears in destructuring patterns
Manual grep would find:
const widgetPath = "/path/to/widget"
But MISS these (ts-morph catches them):
export function init({ widgetPath, config }: Options) {
return load(widgetPath)
}
const {
paths: { widgetPath },
} = config
const handler = ({ widgetPath }: Ctx) => widgetPath
return { widgetPath }
Command:
bun tools/refactor.ts rename.batch --pattern '/^widgetPath$/' --replace repoPath --output edits.json
bun tools/refactor.ts editset.apply edits.json
Example 10: Re-exports and Barrel Files (ts-morph REQUIRED)
Scenario: Rename Widget to Gadget — used in index.ts re-exports
Manual editing misses:
export { Widget } from "./widget"
export type { Widget, WidgetConfig } from "./widget"
export * from "./Widget"
export { Widget as DefaultWidget } from "./Widget"
ts-morph finds ALL of these:
bun tools/refactor.ts rename.batch --pattern '/^Widget$/' --replace Gadget --output edits.json
Example 11: Type-Only Imports
Scenario: Rename UserService interface — used in type-only imports
Manual editing risks:
import type { UserService } from "./services"
const fn = (service: import("./services").UserService) => {}
function process<T extends UserService>(svc: T) {}
ts-morph finds all patterns:
bun tools/refactor.ts rename.batch --pattern '/^UserService$/' --replace AccountService --check-conflicts
bun tools/refactor.ts rename.batch --pattern '/^UserService$/' --replace AccountService --output edits.json
Example 12: JSDoc References
Scenario: Rename parseConfig — referenced in JSDoc comments
function initApp(config) {}
ripgrep backend catches JSDoc:
bun tools/refactor.ts pattern.replace \
--pattern '/^parseConfig$/' \
--replace loadConfig \
--glob "**/*.{ts,js}" \
--backend ripgrep \
--output jsdoc-updates.json
Example 13: Dynamic Imports
Scenario: Rename file user-api.ts to account-api.ts — dynamic imports exist
import { getUser } from "./user-api"
const api = await import("./user-api")
const { handler } = await import(`./user-api`)
Two-step approach:
bun tools/refactor.ts file.rename --pattern user-api --replace account-api --output files.json
bun tools/refactor.ts file.apply files.json
bun tools/refactor.ts pattern.replace \
--pattern "user-api" \
--replace "account-api" \
--glob "**/*.ts" \
--backend ripgrep \
--output dynamic.json
bun tools/refactor.ts editset.apply dynamic.json
Example 14: Test Mocks and Fixtures
Scenario: Rename createWidget — but test mocks use it too
export function createWidget() {}
vi.mock("../widget", () => ({
createWidget: vi.fn(),
}))
export const mockCreateWidget = () => {}
ts-morph renames ALL including mocks:
bun tools/refactor.ts rename.batch --pattern '/^createWidget$/' --replace createGadget --output edits.json
Example 15: Partial Word Match with Case Preservation
Scenario: Rename all widget occurrences — different casings exist
const widget = {}
const Repo = {}
const REPO_PATH = ""
const widgetConfig = {}
class RepoManager {}
const REPO_OPTIONS = {}
Automatic case preservation:
bun tools/refactor.ts rename.batch --pattern '/widget/i' --replace repo --output edits.json
Result:
const repo = {}
const Repo = {}
const REPO_PATH = ""
const repoConfig = {}
class RepoManager {}
const REPO_OPTIONS = {}
Example 16: Multi-Package Monorepo
Scenario: Rename across multiple packages in a monorepo
packages/
core/src/widget.ts
cli/src/commands/widget.ts
tui/src/views/widget-view.tsx
storage/src/widget-loader.ts
Single command handles all:
bun tools/refactor.ts migrate --from '/widget/i' --to repo --glob "packages/**/*.{ts,tsx}"
Example 17: Rollback Safety with Checksums
Scenario: Someone edited a file after you created the editset
bun tools/refactor.ts rename.batch --pattern '/^Widget$/' --replace Gadget --output edits.json
bun tools/refactor.ts editset.apply edits.json
The editset NEVER corrupts files — if the file changed, it skips that file.
Decision Tree: Which Command to Use
Is this a terminology migration (file names + code + docs)?
├── YES → `migrate --from /X/ --to Y`
└── NO
├── Is this an API migration (complex pattern changes)?
│ └── YES → `pattern.migrate --patterns X --prompt "..."` (LLM-powered)
├── Renaming files?
│ └── YES → `file.rename --pattern X --replace Y`
├── Renaming TypeScript identifiers?
│ └── YES → `rename.batch --pattern /X/ --replace Y`
├── Updating Go/Rust/Python structural patterns?
│ └── YES → `pattern.replace --backend ast-grep`
└── Updating text/markdown/comments?
└── YES → `pattern.replace --backend ripgrep`
Use pattern.migrate when:
- Old → new patterns have different structures (not just name changes)
- Transformations need context awareness (adding
await, changing variable scope)
- Multiple related patterns need coordinated changes
- Value mapping is required (e.g., escape codes → named keys)
Performance Comparison
| Task | Manual Edits | Batch Refactor |
|---|
| Rename function (50 refs) | ~50 Edit calls | 2 commands |
| Rename file + imports | Risk broken build | Atomic update |
| Full terminology migration | Hours | Minutes |
| Rollback on error | Manual git restore | Automatic (checksums) |
Rule of thumb: If you'd make more than 5 edits, use batch refactor
Tips & Tricks
⛔ STOP: Manual Edit Tools
IMMEDIATELY STOP AND THINK if you're about to use any of these:
| Tool | What it does | Why you should stop |
|---|
sed | Stream editing | batch-refactor does this better with checksums |
awk | Pattern processing | batch-refactor handles this |
perl -pe | Regex replacement | batch-refactor does this safely |
python -c | Quick scripts | batch-refactor is purpose-built for this |
Manual Edit tool in loop | Many small edits | batch-refactor does this atomically |
Before using these, ask yourself:
- Can batch-refactor do this? → Use
pattern.replace or rename.batch
- Is this a text pattern? → Use
pattern.replace --backend ripgrep
- Is this a TypeScript symbol? → Use
rename.batch (ts-morph)
- Is this a structural pattern? → Use
pattern.replace --backend ast-grep
If batch-refactor CAN'T do what you need:
- File a feature request as a bead:
bd create --title "batch-refactor: support X" --type=task
- Document the gap in the bead description
- Only then use the manual tool as a workaround
The goal: Every manual edit is a signal that batch-refactor is missing a feature.
1. Store editsets in /tmp
Storing editsets outside the repo avoids cluttering your working directory and needing grep -v .editsets when verifying:
bun tools/refactor.ts migrate --from '/widget/i' --to repo -o .editsets
rg -l widget -g "*.ts" | grep -v .editsets | wc -l
bun tools/refactor.ts migrate --from '/widget/i' --to repo -o /tmp/editsets
rg -l widget -g "*.ts" | wc -l
2. Preview editset contents
Before applying, inspect what an editset will do:
cat /tmp/editset.json | head -50
cat /tmp/editset.json | jq '{refs: .refs | length, edits: .edits | length}'
bun tools/refactor.ts editset.apply /tmp/editset.json --dry-run
3. Use rg directly for exploration
Before running batch operations, verify your patterns find what you expect:
rg -l "useRepo" -g "*.ts"
rg -n "useRepo" -g "*.ts"
rg -c "widget" -g "*.ts" | head -20
rg -C 2 "createWidget" -g "*.ts"
4. Use rg for file discovery too
rg --files lists files matching globs, respecting .gitignore. One tool for everything:
find . -name "*widget*" -not -path "./node_modules/*"
rg --files -g "*widget*"
rg --files -g "*.ts" | head -10
rg --files -g "*widget*.ts"
rg --files packages/
5. Avoid slow shell loops
Shell loops with rg are slow. Use rg's built-in features instead:
rg -l "widget" | while read f; do
echo "=== $f ==="
rg -n "widget" "$f"
done
rg -n "widget" -g "*.ts"
rg --heading -n "widget" -g "*.ts"
rg --vimgrep "widget" -g "*.ts"
6. Verify glob patterns work
Test glob patterns with rg directly before using them in batch commands:
rg --files -g "**/*.ts" | head -10
rg --files -g "apps/**/*.tsx" | head -10
bun tools/refactor.ts pattern.replace --pattern '/widget/i' --replace 'gadget' --glob "apps/**/*.tsx" -o /tmp/edits.json
6. Check remaining mentions efficiently
After migration, verify zero remaining mentions:
rg -c "widget" -g "*.ts" --stats
rg -l "widget" -g "*.ts" -g "*.tsx"
rg -l "widget" -g "*.ts" -g "!vendor/**" -g "!node_modules/**"
7. Debug batch-refactor commands
If a batch command returns 0 results unexpectedly:
rg -l "pattern" -g "*.ts"
DEBUG=* bun tools/refactor.ts pattern.replace --pattern '/widget/i' --replace 'gadget' --glob "**/*.ts"