@the-coded/pstash
v0.1.2
Published
Git-backed personal file stash — persistent, project-categorized, multi-machine
Maintainers
Readme
Like git stash but for any file, on any project, synced to a private remote.
pstash save "planning notes" *.md
pstash list
pstash popTable of Contents
- Table of Contents
- Overview
- Installation
- Quick Start
- Commands
- Configuration
- Stash ID Format
- Data Repo Structure
- Requirements
- License
Overview
pstash solves a common problem: you have files (notes, drafts, WIP configs, snippets) that don't belong in the project's git history, but you want them:
- Persistent — not lost after a branch switch or system reset
- Organized — grouped by project, tagged, searchable
- Multi-machine — synced via a private git repo
Installation
npm install -g @the-coded/pstashRequirements: Node 20+, Git
Quick Start
1. Create a private data repo
Create a new empty private repo on GitHub/GitLab (e.g. my-personal-stash).
2. Initialize pstash
pstash init --remote https://github.com/you/my-personal-stash.gitThis clones the repo to ~/.pstash and creates ~/.pstashrc.
3. Stash some files
# Fully interactive — prompts for a message, then a checkbox picker of
# unstaged/untracked files (with an "➕ Add custom glob pattern..." entry)
pstash save
# Stash all markdown files in current directory
pstash save "planning notes" *.md
# Stash with tags
pstash save -t docs -t wip "API design draft" docs/api.md
# Stash and remove source files
pstash save --rm "temp notes" scratch.md4. List your stashes
pstash list
pstash list --all # all projects
pstash list -t docs # filter by tag5. Restore stashes
pstash pop # interactive selector
pstash pop 0 # newest stash
pstash apply 1 # restore without deletingCommands
Command Summary
| Command | Description |
| ------------------- | --------------------------------------------------------------------- |
| init | Initialize pstash — clone data repo and create ~/.pstashrc |
| save | Stash files with a message, commit and sync |
| update | Overwrite files of an existing stash (keeps the stash ID) |
| list | List stashes for the current project (or all projects) |
| pop | Restore stash files to disk and delete the stash |
| apply | Restore stash files without deleting the stash |
| sync | Manually synchronize the stash repo (pull + push) |
| show | Show details or file contents of a stash entry |
| drop | Delete one or more stash entries without restoring files |
| status | Show stash repository status and per-project summary |
| clean | Bulk-remove old or filtered stash entries |
| diff | Compare two stashes, or a stash against the current working directory |
| config | View or set configuration values |
init
Initialize pstash — clone data repo and create ~/.pstashrc.
pstash init [options]| Option | Description |
| -------------------- | --------------------------------------------- |
| -r, --remote <url> | SSH or HTTPS URL of your data repo |
| -p, --path <path> | Local path to clone to (default: ~/.pstash) |
Examples:
pstash init --remote [email protected]:you/my-stash.git
pstash init -r https://github.com/you/my-stash.git --path ~/stashsave
Stash files with a message. Files are copied to the data repo, committed, and synced (pull before + push after) when autoSync is enabled.
pstash save [options] [message] [files...]| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| -t, --tag <tag> | Add tag (repeatable: -t docs -t wip) |
| -p, --project <name> | Override auto-detected project name |
| --no-sync | Skip auto pull+push for this operation |
| --compress | Compress stash as tar.gz (overrides defaults.compression) |
| --rm | Remove source files after saving |
| --keep | Keep source files (overrides defaults.removeAfterSave=true) |
| --unstaged | Auto-detect unstaged git files (modified + untracked) and stash them — ignores [files...] |
Interactive mode:
If [message] and [files...] are both omitted, save runs interactively:
- Prompts for a message (
"What are you stashing?") - Shows a checkbox picker of your git unstaged + untracked files (space to toggle, enter to confirm)
- Offers an
➕ Add custom glob pattern...entry where you can type space-separated globs (e.g.docs/*.md src/**/*.ts)
Flags still work alongside the prompts — for example pstash save --rm runs interactively and then removes the source files. Passing --unstaged skips the picker and auto-detects the file list.
Examples:
pstash save # fully interactive (prompts message + file picker)
pstash save --rm # interactive + remove source files afterwards
pstash save "planning notes" *.md
pstash save -t api -t draft "openapi spec" openapi.yaml
pstash save --rm "WIP code" src/experiment.ts
pstash save --compress "large archive" *.bin
pstash save --no-sync "quick local save" *.md
pstash save --unstaged "WIP before switch"Project detection order:
--projectflag- Git
originremote → extract repo name - Any other git remote
basename(cwd)
Directory structure preservation:
Files within the current directory are stored with their relative path intact. For example, @todo/PROGRESS.md is stored as @todo/PROGRESS.md inside the stash — not as a flat PROGRESS.md. When restored via pop or apply, subdirectories are recreated automatically. Files outside cwd (e.g. absolute paths) fall back to basename only.
update
Overwrite the files of an existing stash. Keeps the stash's id and original timestamp, and sets updatedAt to now. The stash contents are fully replaced (not merged). Message and tags are preserved unless you pass -m / -t.
pstash update [options] [index] [files...]| Option | Description |
| ---------------------- | --------------------------------------------------------------------------- |
| -m, --message <msg> | Override the stash message (defaults to the existing one) |
| -t, --tag <tag> | Replace tags (repeatable). If any tag is given, existing tags are replaced. |
| -p, --project <name> | Override auto-detected project name |
| --no-sync | Skip auto pull+push for this operation |
| --compress | Compress stash as tar.gz (overrides defaults.compression) |
| --unstaged | Auto-detect unstaged git files and stash them — ignores [files...] |
| --force | Skip confirmation prompt |
| [index] | 0-based index. If omitted: interactive stash selector |
| [files...] | Glob patterns for the new contents. If omitted: interactive file picker |
Examples:
pstash update # interactive: pick stash + pick files
pstash update 0 *.md # update newest stash with current markdown files
pstash update 1 --unstaged # update stash #1 with current unstaged files
pstash update 0 -m "revised notes" notes.md
pstash update 0 -t v2 -t docs *.md # replaces tags with [v2, docs]
pstash update 0 --force *.md # skip confirmationlist
List stashes for the current project (or all projects).
pstash list [options]| Option | Description |
| ---------------------- | ----------------------------------------------- |
| -a, --all | Show stashes from all projects |
| -p, --project <name> | Filter by project name |
| -t, --tag <tag> | Filter by tag |
| --since <timespec> | Show stashes after date (7d, 2w, 1m, ISO) |
| --until <timespec> | Show stashes before date |
| --preview | Show a preview of the first line of each file |
| --json | Output as JSON |
Examples:
pstash list
pstash list --all --tag docs
pstash list --since 7d
pstash list --json | jq '.[0].id'pop
Restore stash files to the current directory and delete the stash. When autoSync is enabled, pulls before and pushes after.
pstash pop [options] [index]| Option | Description |
| ----------------------- | ------------------------------------------------------------ |
| -d, --dest <path> | Destination directory (default: current directory) |
| -f, --files <pattern> | Partial restore — only files matching glob pattern |
| -p, --project <name> | Override auto-detected project name |
| --force | Overwrite existing files |
| [index] | 0-based index (0 = newest). If omitted: interactive selector |
Examples:
pstash pop # interactive selector
pstash pop 0 # newest stash
pstash pop 2 -d /tmp/restore
pstash pop 0 -f "*.md" # partial restoreapply
Restore stash files without deleting the stash (like git stash apply). When autoSync is enabled, pulls before restoring — but does not push (no changes to the stash repo).
pstash apply [options] [index]| Option | Description |
| ----------------------- | ------------------------------------------------------------ |
| -d, --dest <path> | Destination directory (default: current directory) |
| -f, --files <pattern> | Partial restore — only files matching glob pattern |
| -p, --project <name> | Override auto-detected project name |
| --force | Overwrite existing files |
| [index] | 0-based index (0 = newest). If omitted: interactive selector |
Examples:
pstash apply # interactive selector
pstash apply 0 # apply newest stash
pstash apply 1 -d /tmp/restore
pstash apply 0 -f "*.ts"sync
Manually synchronize the stash repo with remote (pull + push). Useful when autoSync=false or to force a sync at any time.
pstash sync [options]| Option | Description |
| -------- | --------------------- |
| --pull | Pull only (skip push) |
| --push | Push only (skip pull) |
Examples:
pstash sync # pull + push
pstash sync --pull # fetch updates from other machines
pstash sync --push # upload local stashesshow
Show details of a specific stash entry — metadata, file list, or file contents.
pstash show [options] [index]| Option | Description |
| ---------------------- | ------------------------------------------------------------- |
| -p, --project <name> | Override auto-detected project name |
| -f, --files | List stashed filenames only (no metadata) |
| -c, --cat [pattern] | Print file contents to stdout (optional glob to filter files) |
| --json | Output metadata as JSON |
| [index] | 0-based index. If omitted: interactive selector |
Examples:
pstash show # interactive selector
pstash show 0 # newest stash
pstash show 0 -f # list filenames only
pstash show 0 -c # print all file contents
pstash show 0 -c "*.md" # print only markdown files
pstash show 0 --json # machine-readable outputdrop
Delete a stash entry without restoring its files.
pstash drop [options] [index]| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| -p, --project <name> | Override auto-detected project name |
| -t, --tag <tag> | Drop all stashes with this tag |
| -a, --all | Drop all stashes in the current project (requires confirmation) |
| --force | Skip confirmation prompt |
| --dry-run | Preview what would be deleted |
| [index] | 0-based index. If omitted: interactive multi-select picker (space to toggle, enter to confirm) |
Examples:
pstash drop # interactive multi-select (space toggles, enter confirms)
pstash drop 0 # drop newest (with confirmation)
pstash drop 0 --force # drop without asking
pstash drop -t wip # drop all WIP stashes
pstash drop --all # drop everything (double-confirm)
pstash drop --all --dry-run # preview onlystatus
Show stash repository status — remote, local info, and per-project summary.
pstash status [options]| Option | Description |
| ----------- | ----------------- |
| -a, --all | Show all projects |
| --json | Output as JSON |
Example output:
Remote: [email protected]:you/my-stash.git
Local path: /Users/you/.pstash
Unpushed: 2 commits
PROJECT STASHES TOTAL SIZE LAST UPDATED
my-project 3 268 KB 2 hours ago
other-proj 1 12 KB 3 days agoclean
Bulk-remove old or filtered stash entries. Requires at least one filter (safety guard).
pstash clean [options]| Option | Description |
| ------------------------- | -------------------------------------------------- |
| --older-than <timespec> | Delete stashes older than this (30d, 2w, 1m) |
| --keep <n> | Keep only the N most recent stashes per project |
| -t, --tag <tag> | Delete only stashes with this tag |
| -a, --all | Clean across all projects |
| -p, --project <name> | Override auto-detected project name |
| --dry-run | Preview what would be deleted |
| --force | Skip confirmation prompt |
Examples:
pstash clean --older-than 30d
pstash clean --keep 5
pstash clean -t wip --dry-run
pstash clean --older-than 7d --forcediff
Compare two stashes, or a stash against the current working directory. Built-in LCS-based diff — no external tools required.
pstash diff [options] [indexA] [indexB]| Option | Description |
| ---------------------- | --------------------------------------------------- |
| -p, --project <name> | Override auto-detected project name |
| --files <pattern> | Limit diff to files matching this glob pattern |
| --stat | Show only changed file names (no inline diff) |
| [indexA] | First stash index. If omitted: interactive selector |
| [indexB] | Second stash index — omit to compare against cwd |
Interactive mode:
If [indexA] is omitted, diff runs interactively:
- Prompts you to pick stash A (the "before" side)
- Prompts you to pick the comparison target — either the current working directory (cwd) or another stash (skipped when only one stash exists)
Examples:
pstash diff # interactive: pick stash A, then cwd or another stash
pstash diff 0 1 # compare two stashes
pstash diff 0 # compare stash 0 with cwd
pstash diff 0 1 --files "*.ts" # limit to TypeScript files
pstash diff 0 --stat # summary onlyconfig
View or set configuration values using dot-notation keys.
pstash config [options] [key] [value]| Option | Description |
| ------------ | -------------------------------------------------- |
| -l, --list | List all config values (default when no key given) |
| --json | Output as JSON |
Examples:
pstash config # list all config
pstash config --json # list as JSON
pstash config autoSync # get value
pstash config autoSync false # set value
pstash config defaults.compression false
pstash config defaults.keepOnPop falseConfiguration
Config is stored at ~/.pstashrc (JSON). Example:
{
"version": "1.0.0",
"remote": "https://github.com/you/my-personal-stash.git",
"localPath": "~/.pstash",
"autoSync": true,
"projects": {
"scena": {
"aliases": ["e2e-gen", "scena-cli"]
}
},
"defaults": {
"keepOnPop": false,
"compression": false,
"removeAfterSave": false
}
}Config Keys
| Key | Type | Default | Description |
| -------------------------- | --------- | ----------- | ----------------------------------------------------------------------- |
| remote | string | — | URL of your data repo (required) |
| localPath | string | ~/.pstash | Local clone path |
| autoSync | boolean | true | Auto pull before reads, pull+push after writes |
| defaults.keepOnPop | boolean | false | If true, pop keeps the stash (behaves like apply) |
| defaults.compression | boolean | false | Compress stashes as tar.gz (use --compress flag to opt-in per save) |
| defaults.removeAfterSave | boolean | false | Delete source files after save |
autoSyncis the master sync switch. When enabled, pstash automatically pulls the latest changes before read operations (list,show,diff,status,apply) and pulls + pushes around write operations (save,pop,drop,clean). Override per-command with--no-sync.
Project Aliases
Map alternative names to a canonical project:
{
"projects": {
"scena": {
"aliases": ["e2e-gen", "scena-cli"]
}
}
}When pstash detects your project as e2e-gen (from git remote), it automatically resolves to scena — so all stashes are stored under one project name.
Stash ID Format
Each stash has a unique ID: YYYY-MM-DD_HH-mm_XXXX
YYYY-MM-DD_HH-mm— timestamp (UTC)XXXX— 4-character nanoid suffix (collision prevention for multi-machine use)
Example: 2026-03-12_01-05_k7x2
Data Repo Structure
my-personal-stash/
scena/
.project.json ← project index
2026-03-12_01-05_k7x2/
.stash.json ← metadata
stash.tar.gz ← compressed files (default)
2026-03-10_14-30_p9qr/
.stash.json
README.md ← uncompressed files
notes.md
other-project/
.project.json
2026-02-28_09-15_mnop/
.stash.json
stash.tar.gz.stash.json format
{
"id": "2026-03-12_01-05_k7x2",
"project": "scena",
"timestamp": "2026-03-12T01:05:00.000Z",
"message": "planning notes for v2",
"tags": ["docs", "planning"],
"branch": "main",
"commit": "abc123def456",
"user": "gab@macmini",
"files": [{ "name": "README.md", "size": 1024, "hash": "sha256:a1b2c3d4e5f6" }],
"totalSize": 1024,
"compressed": true
}Requirements
- Node.js 20+
- Git (must be installed and in PATH)
- A private git repository for your stash data
License
MIT — see LICENSE
