npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

finder-selection-sync

v2.2.0

Published

Sync Finder tags, clean files by reference extension, and keep or delete files by list.

Readme

finder-selection-sync

Sync macOS Finder Tags across original and processed file folders, clean files by a reference extension, and keep or delete files using a text list.

Photo and list cleanup (2.2.0)

These subcommands integrate the workflows previously provided by photome and deletebutme.

# Mixed folder: keep NEF and all matching filenames; preview orphan files of other extensions
finder-selection-sync clean-orphans '/path/to/photos' --reference-ext nef

# Any one extension can be the reference: CR3, JPG, ARW, DNG, etc.
finder-selection-sync clean-orphans '/path/to/photos' --reference-ext jpg

# Separate folders: base supplies reference filenames, targets are cleaned
finder-selection-sync clean-orphans --base '/path/to/NEF' --target '/path/to/exports' --reference-ext nef

# Keep filenames listed in a UTF-8 text file; preview everything else
finder-selection-sync clean-list '/path/to/target' --list '/path/to/keep.txt' --mode keep

# Delete only listed filenames; preview
finder-selection-sync clean-list '/path/to/target' --list '/path/to/delete.txt' --mode delete

New commands are dry runs by default. Add --apply to move selected files to macOS Trash, or --apply --permanent to permanently delete. Trash uses Finder and may require macOS Automation permission. A failure stops subsequent operations without falling back to permanent deletion. --list-limit 0 shows every candidate.

clean-orphans uses --reference-ext <extension> to keep the specified extension and clean all other extensions that have no matching reference filename. No RAW-format setting or target-extension list is needed. For example:

Input                         With --reference-ext nef
A.NEF                         keep (reference)
A.JPG / A.CR3 / A.XMP / A.TIF   keep (matching reference A.NEF)
B.JPG / B.CR3 / B.XMP / B.PSD   clean (no B.NEF)

This includes orphan sidecars, documents and videos, not just image formats. Files without an extension (including names ending in a bare dot) are preserved. Reference-extension files are never selected, including those in target directories. In separate-folder mode only the base supplies reference keys; target reference files do not expand that set. With an empty reference and explicit --allow-empty, all eligible non-reference extensions are selected.

The extension is case-insensitive, may include a leading dot (nef, NEF, .nef), and must be a single extension. jpg and jpeg are distinct: --reference-ext jpg uses only .jpg files as references; matching .jpeg files are retained as other formats. Matching uses the filename without its last extension, so A.NEF.XMP has stem A.NEF, not A.

Matching is case-insensitive by relative subdirectory and filename stem; use --match-by name to ignore directories. Processing suffixes are not stripped. Separate reference and target roots must not overlap; use the single-folder syntax for mixed photos. Multiple --target folders are supported.

The older --reference cr3 / --reference jpeg options remain available with their original paired CR3/JPEG-only scope. They cannot be combined with --reference-ext. Use the new option for multi-format cleanup.

Lists require explicit --mode keep|delete. Default matching is by full filename, including extension, with exact case and Unicode NFC normalization. Options:

  • --match-by name|relative|stem: full basename (default), relative file path, or basename without extension.
  • --ignore-case: case-insensitive list matching.
  • --include-ext jpg,jpeg: restrict eligible file types.
  • --exclude-ext cr3,xmp: always preserve these types; exclusions take precedence.
  • Multiple positional targets or repeated --target options.

List lines may contain paths in name/stem modes; only the basename is used. Relative mode rejects absolute paths and traversal. A single leading UTF-8 encoding BOM is removed; any subsequent literal BOM character belongs to the filename. Blank lines are ignored, filename whitespace is preserved, and lines are not shell commands or comments. Lists and their hard links are protected from cleanup.

Both commands support --no-recursive or --depth N (0 = top level), reject overlapping cleanup roots, and skip hidden entries and symlinks. Empty references/lists fail unless --allow-empty is explicitly passed. With an empty keep-list, all eligible candidates are selected; an empty delete-list selects nothing. Directories are never deleted. Planned files and directory inventories are rechecked before execution. During orphan cleanup, a newly matching reference stops further deletion; this check uses the same directory, case and Unicode rules as planning. Directory paths are revalidated before descent to detect substituted symlinks. These checks reduce stale-plan risks but do not make cleanup transactional; avoid concurrent changes while it runs.

Use clean-orphans --help or clean-list --help for all options. Before publishing/installing this version, run node ./bin/finder-selection-sync.js from this checkout instead of the global command or npx.

The original commands below remain supported. In particular, legacy --delete-unmatched --apply still permanently deletes files; it now refuses empty reference folders and overlapping roots. Relative base paths literally named clean-orphans or clean-list must use ./clean-orphans / ./clean-list to avoid subcommand parsing.

Finder tag workflows

The CLI treats a base folder as the reference selection and matches its filenames against one or more target folders. It can assign one color tag to a selection or copy every Finder Tag from each base file to its target matches. A single file may contain multiple colors and custom named tags. It works with processing workflows that append bracketed variants such as -[variant-a][small].

Requirements

  • macOS
  • Node.js 18 or newer

Install

Run without installing:

npx finder-selection-sync <base> <target...>

Or install the CLI globally:

npm install --global finder-selection-sync

Quick start

Preview the matching files in two processed target folders:

finder-selection-sync \
  '/path/to/base-selection' \
  '/path/to/processed-target-1' \
  '/path/to/processed-target-2'

All commands are dry runs by default. Add --apply only after reviewing the output:

finder-selection-sync \
  '/path/to/base-selection' \
  '/path/to/processed-target' \
  --apply

Sync existing base tags

Copy all Finder Tags from each tagged base file to every matching target file:

finder-selection-sync \
  '/path/to/base' \
  '/path/to/processed-target-1' \
  '/path/to/processed-target-2' \
  --sync-tags

The command above is a dry run. After reviewing each planned tag set, apply the changes:

finder-selection-sync <base> <target...> \
  --sync-tags \
  --apply

By default, the base tag set replaces the complete target tag set. To preserve target-only tags and add the base tags, use --merge-tags:

finder-selection-sync <base> <target...> \
  --sync-tags \
  --merge-tags \
  --apply

Untagged base files are skipped and do not change their target matches. To synchronize the untagged state as well, use --include-clear; matching target tags will then be cleared:

finder-selection-sync <base> <target...> \
  --sync-tags \
  --include-clear \
  --apply

If duplicate base files have the same match key but different tag sets, the CLI reports the conflicting files and makes no changes. Tag order does not create a conflict. Without --include-clear, an untagged duplicate is ignored when another duplicate has tags.

--sync-tags operates on targets and cannot be combined with --scope base|both, --select, --color, --match, or --clear-all.

Positive and inverse selection

The default scope is targets.

| Selection | Option | Result | | --- | --- | --- | | Positive | --select matched | Target files with a matching filename in base | | Inverse | --select unmatched | Target files without a matching filename in base | | All | --select all | Every file in each target, regardless of matching |

Replace the tags on the inverse selection with a green color tag:

finder-selection-sync \
  '/path/to/base-selection' \
  '/path/to/processed-target-1' \
  '/path/to/processed-target-2' \
  --select unmatched \
  --color green \
  --apply

Clear tags

Clear all Finder Tags from every file in the target folders:

finder-selection-sync \
  '/path/to/base-selection' \
  '/path/to/processed-target-1' \
  '/path/to/processed-target-2' \
  --clear-all \
  --apply

--clear-all is a shortcut for --select all --color clear. It follows the selected scope, which is targets by default. Without --apply, the command only previews the files whose tags would be cleared.

To clear tags only from a positive or inverse selection, combine that selection with --color clear:

finder-selection-sync <base> <target...> \
  --select unmatched \
  --color clear \
  --apply

Delete unmatched target files

Preview target files that do not have a matching filename in the base folder:

finder-selection-sync <base> <target...> --delete-unmatched

After reviewing the complete list, permanently delete those files:

finder-selection-sync <base> <target...> \
  --delete-unmatched \
  --list-limit 0 \
  --apply

--delete-unmatched affects target files only. It does not delete base files or directories, and it cannot be combined with tag operations, --select, --color, or --match. Deletion is permanent and does not move files to the Trash.

Filename matching

Matching is case-insensitive. File extensions and the processing suffix beginning with the first -[ are ignored. These files therefore share the match key item_0001:

  • ITEM_0001.DAT
  • ITEM_0001-[variant-a][small].dat
  • ITEM_0001-[variant-a][small][export][web].dat

Files with the same match key are all selected; the implementation does not discard duplicates.

Options

--scope <base|targets|both>    Side on which tag changes are applied (default: targets)
--select <mode>                matched, unmatched, or all (default: matched)
--clear-all                    Clear all Finder tags from the selected scope
--sync-tags                    Copy all base Finder tags to matching target files
--merge-tags                   Merge base and target tags instead of replacing
--include-clear                Also copy the untagged state with --sync-tags
--delete-unmatched             Permanently delete target files not found in base
--match <any|all>              Match base files in any or all targets (default: any)
--color <color>                Finder tag color (default: green)
--apply                        Apply changes; otherwise preview only
--no-recursive                 Do not scan nested folders
--list-limit <number>          Maximum selected paths to print; 0 prints all
-v, --version                  Show the installed version
-h, --help                     Show help

Available colors are clear, gray, green, purple, blue, yellow, red, and orange. Applying a color replaces the selected files' tag sets with that single color tag. clear removes all tags from the selected files.

--match any|all affects selection in base scope. Each target is compared directly with the base folder when the scope is targets.

Base and both scopes

Select base files found in at least one target:

finder-selection-sync <base> <target...> --scope base --match any

Select base files found in every target:

finder-selection-sync <base> <target...> --scope base --match all

Apply the selection to both sides:

finder-selection-sync <base> <target...> --scope both --apply

Safety

  • The CLI never writes tags unless --apply is present.
  • Hidden files and hidden directories are ignored.
  • Existing unrelated com.apple.FinderInfo data is preserved; only Finder's color bits are updated for compatibility.
  • Modern Finder Tags are read from and written to com.apple.metadata:_kMDItemUserTags.
  • --sync-tags skips untagged base files unless --include-clear is present.
  • Conflicting base tag sets stop the entire tag-sync operation before any file is changed.
  • --delete-unmatched is a dry run unless --apply is present, and it never deletes directories.
  • Paths are passed directly to xattr without shell interpolation.

Development and tests

npm test
npm run test:coverage

Tests use temporary files and cover both cleanup directions, lists, filtering, filesystem guards, CLI integration, generated datasets, and Finder metadata. The default suite uses a substitute executor for Trash argument handling and failures and does not alter the user's Trash. An explicit macOS integration test exercises real Finder deletion, filename collisions, Unicode/quotes, list/reference preservation, and stopping when a new matching reference appears:

# From the source checkout only; may request macOS Automation permission
npm run test:finder-trash

This opt-in test creates isolated temporary fixtures, verifies their contents at the exact Trash paths returned by Finder, then restores and removes only those fixtures. It never enumerates or empties Trash. It does not run under npm test or during publishing. If recovery fails, retain the printed fixture directory for investigation; do not empty Trash. Concurrency checks are not a filesystem transaction and cannot eliminate every race between checking a path and modifying it.

License

MIT