
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
Plan a directory reorganization in your browser, then apply it with an undo script.
Reorganize a messy directory by dragging it into shape, then apply the plan with one command – and an undo script.
npx reorg-cli ~/Downloads
That scans the directory, opens a planner in your browser, and prints a URL. Drag folders around, rename things, mark junk for trash. Nothing touches disk until you say so.

No runtime dependencies, no build step, no config file. One Node script and a page.
Cleaning up a directory is two jobs that get tangled together: deciding what the shape should be, and executing a pile of mv commands without breaking anything. Doing both at once in a terminal means you lose the plan halfway through, or you discover the conflict after the third move.
reorg splits them. You do all the deciding in a view where the whole tree is visible and every change is reversible with a keystroke. When the shape looks right, the tool works out the operations – in a correct order, with collisions caught up front – and executes them as one checked batch that it can also undo.
It is useful for a ~/Downloads that got away from you, a repo whose layout no longer matches how you think about it, or a scratch directory you have been meaning to triage for a year.
Nothing to install: npx reorg-cli <dir> runs it, and installs the command as reorg. Or clone and link it:
git clone https://github.com/j-256/reorg
cd reorg && npm link # then: reorg <dir>
Requires Node 22 or newer – the oldest release still receiving security updates. There are no runtime dependencies: package.json has an empty dependencies block and that is deliberate.
npm test # no install needed -- runs on a bare checkout
npm run test:ui # browser tests; needs `npm ci` and a Chromium
npm run test:all
npm test needs nothing installed, deliberately: it covers the scanner, the plan resolver, the apply engine and its undo scripts, the triage signals, the server's access control, and the browser-side plan model, all under node --test with nothing fetched.
npm run test:ui drives the real page in a real Chromium, which is the only way to cover what a fake DOM cannot: drop-zone geometry is computed from the pointer's position against a row's box, and jsdom reports every box as zero-sized. It also runs a WCAG AA contrast sweep over both themes. Playwright is the one dependency in the repo and it is confined to devDependencies:
npm ci && npx playwright install chromium
| Command | What it does |
|---|---|
reorg [dir] | scan, serve the planner, open a browser |
reorg [dir] --static | write and open a self-contained planner with no server |
reorg plan [dir] | print the current plan as operations |
reorg apply [dir] | dry run: print what would happen, change nothing |
reorg apply [dir] --yes | apply, writing an undo script first |
reorg apply --plan FILE | dry-run a plan exported by a static planner |
reorg undo [dir] | run the most recent undo script |
reorg status [dir] | what is planned, applied, and undoable |
reorg summarize [dir] | one-line AI description per file (see below) |
reorg triage [dir] | rank likely-disposable entries and say why |
Short aliases are -s for --static, -o for --output, -p for --port, -y for --yes, and -h for --help. --plan stays long-only so -p is unambiguously the port.
In the planner:
F2 to rename in place.Delete marks for trash. Folders take their contents with them.n makes a new folder inside the selection; N attaches a note.Space previews a file (first 100 lines, read on demand)./ jumps to the filter box; wrap the query in slashes for a regex (/\.log$/).? lists every key.Toolbar toggles persist in the plan, so the view you set up is the view you come back to: git tints rows by git status, heat draws a size bar on each row, and theme cycles auto (follow the system) through forced dark and light.
Sibling order is derived (folders first, then natural sort), never stored. Dragging something out of a folder and back is a genuine no-op rather than a phantom "reordered" change.
Use --static when the planner needs to work without a local HTTP server:
reorg ~/Downloads -s
reorg ~/Downloads -s -o downloads-plan.html
Without --output, reorg writes a temporary HTML file and opens it. The page is self-contained: the tree, cleanup candidates, bounded file previews, styles, and browser code are all embedded, so it can be moved and opened directly with a file:// URL. An explicit output path is never overwritten.
The tradeoff is that a static page cannot rescan the directory, autosave to .reorg/plan.json, check the live filesystem, or apply anything. Edits stay in the page until Review exports reorg-plan.json by download or clipboard. Feed that export back to the CLI:
reorg apply --plan ~/Downloads/reorg-plan.json # drift-checked dry run
reorg apply --plan ~/Downloads/reorg-plan.json --yes # write undo script, then apply
The export carries both the plan and the scan it was drawn against, including the source root. That lets the CLI preserve every intended operation and refuse the batch if a source disappeared or a destination became occupied. Pass an explicit directory after apply to use that directory instead of the embedded root. --plan - reads the same JSON from standard input.
A static page contains filenames, metadata, summaries, and the embedded file previews. Treat it like the directory data it captures when copying or sharing it.
Applying a reorganization is the part that can ruin your afternoon, so the plan is always shown as the exact operations it resolves to, in order, before anything runs:

reorg apply prints and exits. Only --yes moves anything. The browser cannot apply at all unless you started it with --allow-apply..reorg/trash/<run>/. Emptying that is a separate decision you make yourself.git mv for tracked files, so history follows the move. (Git refuses this on a fully-untracked directory; reorg falls back to a plain rename there.)Your plan lives in .reorg/plan.json, which is a diff against the scan rather than a copy of the tree. .reorg/ git-ignores itself on creation, so planning a repo's layout never dirties that repo.
Half of triage is remembering what a file is. reorg summarize labels each one with a single line – "nightly S3 sync of /var/data", not "a shell script" – and the planner shows it inline next to the filename.
Two ways to get them, and the default needs no API key:
# Agent path: writes a prompt pack, your coding agent fills it in. Free.
reorg summarize ~/Downloads # -> .reorg/summarize.md + summaries.json
# ...point Claude Code (or any agent) at that markdown file...
reorg summarize --ingest ~/Downloads
# API path: calls the Messages API directly. Needs a key.
ANTHROPIC_API_KEY=sk-... reorg summarize ~/Downloads
The API path batches files (about a dozen per request), sends only the first few KB of each, skips binaries and empty files, and defaults to Haiku because this is a classification job. Override with --model. It uses fetch against the documented HTTP API – no SDK dependency.
Summaries are stored in the plan and keyed by path, so they survive a rescan.
reorg triage ranks entries that look like junk and says why, so a directory that has got away from you starts with a shortlist instead of a scroll. The same list is in the planner behind the triage button, where each row has a mark-for-trash button.

It ranks names and structure, not age. That is the opposite of the obvious design, and it is deliberate: a name is frequently a direct statement of intent – someone wrote "backup" or "dryrun" because that is what the thing was for – where mtime turns out to predict almost nothing. The signals are:
| Signal | Why |
|---|---|
| an archive beside its unpacked copy | pure duplication; one of the two is redundant |
a name ending in backup/dryrun/tmp/presync/... | the name states it was a safety copy |
installer (.dmg, .pkg, .msi) | re-downloadable, dead weight once installed |
scratch extension (.log, .bak, .crdownload) | a byproduct, not something authored |
a bare .git clone | a mirror kept as a one-off safety copy |
| bulky | worth a decision purely for what it costs to keep |
Position matters, because the same word can name a subject rather than a status. Only trailing markers count, and the screenshot above shows both sides of that: project-backup-20260415 is flagged, while backup-strategy-notes.md and all-mail-including-spam-and-trash.mbox sit in the same tree untouched – one is a document about backups, the other is 2.9 GB of actual mail. Both of those were real false positives before the position rule went in, and flagging 3 GB of someone's mail as trash is how a suggestion list loses its reader.
Emptiness is deliberately not a signal: empty directories are often intentional (mount points, placeholders) and cost nothing to keep.
Look at the ages the triage panel prints above: every flagged candidate is recent – 3, 12, 34 days. Nothing there is old, because in a directory that got away from you the junk is usually the newest thing in it. Sorting by mtime would push all six candidates to the bottom.
That inversion is why the ranking works the way it does, and it is not invented for the screenshot. Measured against a real long-neglected scratch directory:
-backup-20260425 directory, a .zip still sitting beside its unpacked copy, a downloaded .dmg – had ages spanning 12 to 586 days, so age separated them from nothing. Seven of eight -backup-/-dryrun- directories were all 12 days old: an age sort would have called them active work.Age is still shown on every row, for context. It is just never what sorts them.
Worth knowing, because it explains why the output looks the way it does.
Every entry has a stable id (its path at scan time) and two positions: the frozen original and the live one you edit. The diff between them is the plan. Resolving it produces operations in dependency order:
mkdir for folders you invented, shallowest first.mv, but only for entries whose own position changed. Moving a/ to b/a/ relocates everything inside it implicitly – emitting a second move for a/x would fail, because by then its source is gone. Destinations are final-tree paths, so each entry moves once.trash last, at each entry's post-move location.Move ordering is a topological sort over two constraints: vacate before occupy (if X lands where Y still is, Y goes first), and parent before child (if X lands inside where Y is going, Y arrives first). A cycle between them means no order works, which is when staging kicks in.
reorg plan prints exactly this list, and the planner's review panel shows the same thing before you apply.
bin/reorg CLI: scan, serve or build static, plan, apply, undo, summarize, status
src/scan.js walk a directory, tag git status, summarize collapsed dirs
src/plan.js pure resolver: plan -> ordered operations (no fs, no exec)
src/apply.js execute, with drift checks, git mv, trash, undo script
src/summarize.js Messages API batching + the no-key agent prompt pack
src/signals.js cleanup signals: what looks disposable, and why (name, not age)
src/server.js stdlib http server, token-gated JSON API
src/static.js build a self-contained planner with an embedded read-only API
src/state.js .reorg/plan.json load, save, self-ignore
web/ the planner: tree, drag and drop, preview, review
test/ unit tests for the resolver, integration tests on real temp dirs
src/plan.js is deliberately pure so the risky decisions are testable without a filesystem. The integration tests do the opposite – real directories, real git repos, real bash undo-*.sh round trips – because a resolver that is right on paper and wrong on disk is worthless.
npm test
reorg serves on loopback with a per-run token in the URL. That is not theatre: the API can read file contents and, with --allow-apply, move files. A token means another process on the machine, or a stray browser tab, cannot drive it. Path parameters are confined to the scan root, so ../../.ssh/id_rsa is rejected rather than served.
npm version patch # or minor, major
That runs a guard (on main, in sync with origin/main), runs the dependency-free suite, bumps the version, commits, tags, and pushes. The tag push is what triggers the release: CI re-checks that the tag sits on main and matches package.json, runs the browser suite as a formal publishing prerequisite, re-runs the dependency-free suite and packed-install check, publishes to npm with a provenance attestation, installs the result from the registry to confirm it is really there, and opens a GitHub release with the tarball attached.
To exercise the pipeline without publishing, dispatch it manually – dry_run defaults to true:
gh workflow run release.yml
A dry run proves that the package builds, packs, installs, and passes its tests. It skips the registry-facing publish step entirely, so credentials, provenance, and registry acceptance are only exercised by a real release.
Unchecking dry_run on a manual dispatch is a real publish, and not the way to cut a release: dispatching has no tag, so both tag checks are skipped and no GitHub release is created – npm gets the version, the repo does not. Release by pushing a tag.
If CI fails before the registry accepts the upload – a bad tag, a failed test, a provenance error – the version number is untouched and you can move the tag onto the fix:
npm run retag
That moves the tag to whatever main currently points at, so push the fix first. retag runs the same guard as npm version and refuses from a feature branch or an unpushed main, since either would tag a commit the release then could not verify.
retag only works when the tag actually moves. If the tag already points at the commit you want – a run that failed for a reason outside the repo, say a registry outage – the force-push is a no-op, git prints Everything up-to-date, and no workflow runs: Actions fires on a ref change, and nothing changed. Re-run the same commit by dispatching against the tag instead, which takes the tag's tree and skips nothing:
gh workflow run release.yml --ref v0.1.0 -f dry_run=false
The dry_run=false is required – dispatch defaults to a dry run, which publishes nothing. Unlike a dispatch from a branch, this one has a tag, so both tag checks run and the GitHub release is still cut.
Once a version is on the registry it is spent; npm does not allow republishing it. A failure after that point means the package is live and the fix is the next patch, not a retag. The GitHub release is therefore cut whenever the publish succeeded, even if the registry smoke test then fails – a slow-propagating registry should not also cost you the release.
A publish sends this README to the registry as plaintext, on every release rather than only the first, and a WAF sits in front of registry.npmjs.org that rejects request bodies matching attack signatures. So prose here can fail a release: a path-traversal example was once enough. npm reports the block as E403 with boilerplate about "your security policy", naming neither the WAF nor the cause, so it reads like a credential problem. To tell them apart, PUT the same document with no credentials – the WAF answers before npm authenticates, so an HTML 403 indicts the payload while JSON clears it, and an unauthenticated request cannot publish.
Publishing carries no credential at all. npm knows this repository, release.yml, and the prd environment as a trusted publisher, so it exchanges the workflow's OIDC token for a short-lived publish token – nothing long-lived to leak, rotate, or forget. A fork cannot publish, because the claim names this repository.
Those three values have to agree with the package's settings on npmjs.com exactly, and all three are easy to break by accident: renaming this workflow file, or renaming the job's environment:, or dropping it. GitHub only puts an environment claim in the token when the job declares one, so environment: prd in release.yml is load-bearing for authentication rather than deployment bookkeeping. npm does not validate a trusted publisher when you save it, and a mismatch fails silently at publish time: the exchange is skipped, npm publish runs unauthenticated, and the registry answers E404 ... you do not have permission, which reads like a missing package. Ask the exchange endpoint directly for the real message:
curl -X POST -H "Authorization: Bearer $ID_TOKEN" \
https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/reorg-cli
Trusted publishing cannot perform a package's first publish, though – npmjs.com only exposes the setting for a package that already exists – so 0.1.0 went out with a short-lived granular token, which was then revoked. Anyone bootstrapping a new package from this workflow has to do the same: publish once with an NPM_TOKEN secret and NODE_AUTH_TOKEN set on the publish step, then register the publisher and remove both.
AGPL-3.0-only. See LICENSE.
FAQs
Plan directory reorganizations yourself or with an AI agent, collaborate through a live browser plan, and safely apply changes with generated undo scripts.
The npm package reorg-cli receives a total of 22 weekly downloads. As such, reorg-cli popularity was classified as not popular.
We found that reorg-cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.