Universal volatile state manager — persistent, toggleable patch layers for any file
patch-buddy is a local-first "volatile state" manager. It treats patches as first-class, toggleable layers that sit on top of your real files. Use it to persist hacky changes, temporary configs, or library patches that need to survive npm install or git pull.
Patches are stored as unified diffs in a .patches/ directory and re-applied automatically any time a watched file changes on disk.
- Patch Any File — not just npm dependencies, any file type on your filesystem
- Toggleable Layers — stack multiple patches per file, enable/disable each one independently
- Automatic Rehydration — the watcher re-applies patches whenever files change externally
- No Config File — everything via CLI flags, patches stored in
.patches/ - IDE-Friendly Conflicts — conflicts are flagged with a status marker and optionally written as git-style conflict blocks for resolution in your preferred editor
# Install globally from npm
npm install -g @yaos-git/patch-buddy
# Or install as a dev dependency
npm install -D @yaos-git/patch-buddy# Clone the repository
git clone https://github.com/YAOSGit/patch-buddy.git
cd patch-buddy
# Install dependencies
npm install
# Build the project
npm run build
# Link globally (optional)
npm link# 1. Save current file content as baseline
patch-buddy init src/config/db.ts
# 2. Make your hacky changes in your editor...
# 3. Snapshot the diff
patch-buddy snap src/config/db.ts -n "fix-port"
# Start the watcher to auto-reapply after npm install / git pull
patch-buddy watchNote: Patch files are plain unified diffs committed to
.patches/. Add.patches/to version control to share volatile state across machines, or add it to.gitignoreto keep it local.
patch-buddy init <file> Save current file content as baseline
patch-buddy snap <file> -n <name> Create a patch from current changes
patch-buddy list [file] Show all patches, optionally filtered
patch-buddy toggle <id> Toggle a patch enabled/disabled
patch-buddy drop <id> Delete a patch
patch-buddy apply [file] Apply all enabled patches once
patch-buddy revert [file] Restore file(s) to unpatched state
patch-buddy watch Start foreground rehydration watcher
patch-buddy --help, -h Show help message
patch-buddy --version, -v Show version information
| Command | Description |
|---|---|
init <file> |
Save current file content as baseline |
snap <file> -n <name> |
Create a patch from current changes |
snap <file> -n <name> --git |
Use git HEAD as baseline instead of stored baseline |
snap <file> -n <name> --base <path> |
Use an arbitrary file as baseline |
list [file] |
Show all patches, optionally filtered by target file |
toggle <id> |
Toggle a patch between enabled and disabled |
drop <id> |
Delete a patch and its diff file |
apply [file] |
Apply all enabled patches once without watching |
revert [file] |
Restore file(s) to their unpatched state |
watch |
Start the foreground rehydration watcher |
watch --conflict-markers |
Write git-style conflict markers on failed patches |
# Baseline + snapshot workflow
patch-buddy init node_modules/some-lib/index.js
# edit the file...
patch-buddy snap node_modules/some-lib/index.js -n "fix-memory-leak"
# Use git HEAD as baseline (skip storing a separate baseline file)
patch-buddy snap src/app.ts -n "debug-logging" --git
# List all tracked patches
patch-buddy list
# List patches for a specific file
patch-buddy list src/config/db.ts
# Disable a patch temporarily
patch-buddy toggle 001
# Re-enable it
patch-buddy toggle 001
# Apply all enabled patches in one shot (no watcher)
patch-buddy apply
# Apply patches for one file only
patch-buddy apply src/config/db.ts
# Revert a file to its unpatched content
patch-buddy revert src/config/db.ts
# Start the watcher (Ctrl+C to stop)
patch-buddy watch
# Start the watcher and write conflict markers on failure
patch-buddy watch --conflict-markerspatch-buddy stores everything inside a .patches/ directory at the project root.
.patches/
manifest.json # Tracks all patches and their metadata
src-config-db.ts/ # Slugified target file path
001-fix-port.patch # Unified diff — prefix is stack position
002-add-logging.patch
node_modules-some-lib-index.js/
001-fix-memory-leak.patch
The slug is derived by replacing path separators and dots with hyphens, making the directory name safe on all platforms.
The manifest is the single source of truth for patch metadata. It is validated against a Zod schema on every read.
{
"files": {
"src-config-db.ts": {
"targetPath": "src/config/db.ts",
"patches": [
{
"id": "001",
"name": "fix-port",
"enabled": true,
"status": "active",
"originalHash": "a1b2c3d4...",
"createdAt": "2026-03-28T10:00:00.000Z"
}
]
}
}
}| Field | Type | Description |
|---|---|---|
id |
string |
Zero-padded numeric ID matching the patch filename prefix |
name |
string |
Human-readable name supplied via -n |
enabled |
boolean |
Whether the patch participates in application |
status |
"active" | "disabled" | "conflicted" |
Current patch state |
originalHash |
string |
SHA-256 of the file content at snapshot time |
createdAt |
string |
ISO 8601 timestamp |
The watch command monitors all files tracked in manifest.json using chokidar. When a file changes on disk the following sequence runs:
- Detect external change — compute the file's hash and compare it against the last hash written by patch-buddy. If patch-buddy itself wrote the change, skip it to avoid re-applying in a loop.
- Forward-apply the stack — read every enabled patch in stack order and attempt to apply each unified diff to the new file content.
- Success — write the patched result back to disk and log the number of patches applied.
- Conflict — if any patch fails to apply, mark it as
conflictedin the manifest and log the failing patch ID. With--conflict-markers, git-style<<<<<<</=======/>>>>>>>blocks are written to the file for manual resolution.
The loop is designed to be idempotent: running apply or restarting watch always produces the same output given the same manifest state.
Launch the TUI with the patch-buddy-tui binary.
patch-buddy-tuiThe dashboard is a full-terminal Ink application that embeds the rehydration watcher automatically. It provides:
- Header — displays the project name and count of tracked files
- Patch List — all patches grouped by target file; each row shows the patch ID, name, and status indicator
- Diff Viewer — press
Enteron any patch to expand the unified diff inline - Watcher Log — a live feed of rehydration events at the bottom of the screen
- Snap Overlay — press
nto open an inline form for creating a new snapshot without leaving the TUI
The embedded watcher starts when the TUI opens and stops when you quit. There is no need to run patch-buddy watch separately while the TUI is open.
| Key | Action |
|---|---|
q |
Quit |
h |
Help |
Up / Down |
Navigate patch list |
Enter |
Expand / collapse patch diff |
t |
Toggle selected patch on/off |
d |
Drop selected patch (with confirmation) |
n |
New snapshot — opens inline snap form |
| Script | Description |
|---|---|
npm run dev |
Run TypeScript checking + test watcher concurrently |
npm run dev:typescript |
Run TypeScript type checking in watch mode |
npm run dev:test |
Run Vitest in watch mode |
| Script | Description |
|---|---|
npm run build |
Bundle the CLI and TUI with esbuild |
| Script | Description |
|---|---|
npm run lint |
Run type checking, linting, formatting, and audit |
npm run lint:check |
Check code for linting issues with Biome |
npm run lint:fix |
Check and fix linting issues with Biome |
npm run lint:format |
Format all files with Biome |
npm run lint:types |
Run TypeScript type checking only |
npm run lint:audit |
Run npm audit |
| Script | Description |
|---|---|
npm test |
Run all tests (unit, react, types, e2e) |
npm run test:unit |
Run unit tests |
npm run test:react |
Run React component tests |
npm run test:types |
Run type-level tests |
npm run test:e2e |
Run end-to-end tests |
- TypeScript 5 — Type-safe JavaScript
- Chalk — Terminal string styling
- Commander — CLI argument parsing
- chokidar — Cross-platform file system watcher
- Zod — Runtime schema validation for the manifest
patch-buddy/
├── src/
│ ├── app/ # Application entry points
│ │ ├── cli.ts # CLI entry point (patch-buddy)
│ │ ├── tui.tsx # TUI entry point (patch-buddy-tui)
│ │ ├── app.tsx # Main TUI application shell
│ │ ├── index.tsx # React app root
│ │ └── providers.tsx # Provider wrapper
│ ├── commands/ # CLI command implementations
│ │ ├── apply.ts # Apply all enabled patches once
│ │ ├── drop.ts # Delete a patch
│ │ ├── init.ts # Save file baseline
│ │ ├── list.ts # List patches
│ │ ├── revert.ts # Restore unpatched state
│ │ ├── snap.ts # Create a patch from current changes
│ │ ├── toggle.ts # Toggle patch enabled/disabled
│ │ └── watch.ts # Foreground rehydration watcher
│ ├── core/ # Core domain modules
│ │ ├── Hash/ # File content hashing
│ │ ├── Manifest/ # manifest.json read/write/mutate
│ │ ├── Patcher/ # Unified diff apply stack
│ │ └── Watcher/ # chokidar wrapper
│ ├── tui/ # TUI-specific code
│ │ ├── commands/ # Keyboard command definitions
│ │ ├── components/ # React (Ink) components
│ │ │ ├── PatchDetail/ # Expanded diff viewer
│ │ │ ├── PatchList/ # Patch list with status indicators
│ │ │ ├── SnapOverlay/ # Inline snapshot form
│ │ │ └── WatcherLog/ # Live rehydration event log
│ │ ├── hooks/ # React hooks
│ │ │ ├── useManifest/ # Manifest state and mutations
│ │ │ ├── useUIState/ # Selected/expanded patch IDs
│ │ │ └── useWatcher/ # Embedded watcher events
│ │ └── providers/ # React context providers
│ │ ├── ManifestProvider/ # Manifest context
│ │ ├── UIStateProvider/ # UI state context
│ │ └── WatcherProvider/ # Watcher context
│ ├── types/ # Shared TypeScript type definitions
│ │ └── Manifest/ # Manifest, FileEntry, PatchEntry types
│ └── utils/ # Pure utility functions
│ ├── diff/ # createPatch / applyPatch wrappers
│ └── slug/ # File path → directory slug
├── e2e/ # End-to-end tests
├── biome.json # Biome configuration
├── tsconfig.json # TypeScript configuration
├── vitest.unit.config.ts # Unit test configuration
├── vitest.react.config.ts # React test configuration
├── vitest.type.config.ts # Type test configuration
├── vitest.e2e.config.ts # E2E test configuration
├── esbuild.config.js # esbuild bundler configuration
└── package.json
This project uses a custom versioning scheme: MAJORYY.MINOR.PATCH
| Part | Description | Example |
|---|---|---|
MAJOR |
Major version number | 1 |
YY |
Year (last 2 digits) | 26 for 2026 |
MINOR |
Minor version | 0 |
PATCH |
Patch version | 0 |
Example: 126.0.0 = Major version 1, released in 2026, minor 0, patch 0
Conventions for contributing to this project. All rules are enforced by code review; Biome handles formatting and lint.
- Named exports only — no
export default. Every module usesexport function,export const, orexport type. import type— always useimport typefor type-only imports..jsextensions — all relative imports use explicit.jsextensions (ESM requirement).
src/
├── app/ # Entry points and root component
├── commands/ # CLI command implementations (camelCase files)
├── core/ # Domain primitives (PascalCase directories)
│ └── MyModule/
│ ├── index.ts
│ └── index.test.ts
├── tui/
│ ├── commands/ # Keyboard bindings and command registry
│ ├── components/ # React components (PascalCase directories)
│ │ └── MyComponent/
│ │ ├── index.tsx
│ │ └── index.test.tsx
│ ├── hooks/ # React hooks (camelCase directories)
│ └── providers/ # React context providers (PascalCase directories)
├── types/ # Shared type definitions (PascalCase directories)
│ └── MyType/
│ ├── index.ts
│ └── index.test-d.ts
└── utils/ # Pure utility functions (camelCase directories)
└── myUtil/
├── index.ts
└── index.test.ts
- Components use
functiondeclarations:export function MyComponent(props: MyComponentProps) {} - Providers use
React.FCarrow syntax:export const MyProvider: React.FC<Props> = ({ children }) => {} - Props are defined in a co-located
.types.tsfile using thetypekeyword. - Components receive data via props — never read
process.stdoutor global state directly.
- Use
typefor all type definitions — neverinterface. - Shared types live in
src/types/TypeName/index.tswith a co-locatedTypeName.test-d.ts. - Local types live in co-located
.types.tsfiles — never inline in implementation files. - No duplicate type definitions — import from the canonical source.
- Named constants go in
.consts.tsfiles. - No magic numbers in implementation files — extract to named constants.
- Every module has a co-located test file.
- Components:
index.test.tsx - Hooks:
index.test.tsx - Utils:
index.test.ts - Types:
index.test-d.ts(type-level tests usingexpectTypeOf/assertType)
ISC