linkany is a safe symlink manager for macOS/Linux. It manages a set of source ↔ target symlink mappings via a manifest file (or an in-memory manifest), with a strong focus on safety, traceability, and refusing risky operations by default.
linkany ships both a library API and a CLI. The CLI supports setting a global default manifest so you don’t have to pass --manifest every time.
- Set default manifest (written to global config; path is resolved to an absolute path):
linkany manifest set ./linkany.manifest.json
- Show current default manifest:
linkany manifest show
- Clear default manifest:
linkany manifest clear
- Priority:
-m/--manifest <path>(one-off override) > global default manifest - Examples:
- Use default manifest:
linkany install - Override once:
linkany install -m ./other.manifest.json
- Use default manifest:
linkany add --source <path> --target <path> [--kind file|dir] [--atomic|--no-atomic] [-m <manifest>] [--dry-run] [--plan] [--no-audit]linkany remove <key> [--keep-link] [-m <manifest>] [--dry-run] [--plan] [--no-audit]linkany install [-m <manifest>] [--dry-run] [--plan] [--no-audit]linkany uninstall [-m <manifest>] [--dry-run] [--plan] [--no-audit]
- If
XDG_CONFIG_HOMEis set:$XDG_CONFIG_HOME/linkany/config.json - Otherwise:
~/.config/linkany/config.json - Format:
{ "manifestPath": "/abs/path/to/manifest.json" }- Symlink-only: if symlink creation fails (permissions/filesystem), it errors; no “copy fallback”.
- Files & directories: supports both.
- Safety rules:
add: refuses whensourceandtargetboth exist andtargetis not already a symlink tosource.remove/uninstall: only removes the target symlink; never deletes sources.install: if anytargetexists but is not a symlink, it aborts the entire run to avoid harming real files/dirs.
- Atomic (best-effort):
- Creates/replaces symlinks via
target.tmp.<rand>thenrenameinto place. - When replacing an existing symlink, it first moves the old target to
target.bak.<timestamp>.<rand>(easier recovery).
- Creates/replaces symlinks via
- Audit log (optional): appends each operation’s
Resultto a JSONL file (default${manifestPath}.log.jsonl), unless disabled. - dry-run / plan:
opts.dryRun=trueperforms no filesystem writes.opts.includePlanText=truereturns a human-readable plan inResult.planText.
- Rollback protocol (best-effort):
Result.rollbackStepscontains a best-effort reverse plan (protocol/data only; no one-shot rollback API yet).
{
"version": 1,
"installs": [
{
"id": "optional-stable-id",
"source": "path/to/source",
"target": "path/to/target",
"kind": "file",
"atomic": true
}
]
}Notes:
source/targetcan be absolute or relative; relative paths are resolved against the manifest file directory.idis optional. If absent,targetis used as the entry identity (e.g., forremove).kindis optional:file | dir. If omitted,addtries to infer;installinfers from the actual source.atomicdefaults totrue.- Extra fields are allowed and preserved on write-back.
All 4 core APIs accept manifest in two forms:
- File mode:
manifestis a manifest file path (string) - In-memory mode:
manifestis a manifest JSON/object
All 4 core APIs return:
{ result, manifest }whereresultis the operation result andmanifestis the updated manifest object.
Writes/updates a mapping in the manifest and converges target to symlink(source).
Key semantics:
- If
sourceis missing: creates an empty source (file: empty file; dir: empty dir). - If
targetexists and is not a symlink whilesourceis missing: performs a safe migration:- copy
target -> source - move original
targettotarget.bak.<timestamp>.<rand> - then make
targeta symlink tosource
- copy
- If
sourceandtargetboth exist: refuses with an error (requires manual conflict resolution).
Removes a mapping from the manifest and by default deletes the target symlink.
key: matchesidfirst, thentarget.opts.keepLink=trueremoves the manifest entry only (does not delete the symlink).- Never deletes sources.
Applies all entries: ensures each target is a symlink pointing to source.
Safety:
- Aborts without changes if any:
- source is missing
- target exists but is not a symlink
Removes all target symlinks listed in the manifest; never deletes sources.
When manifest is a JSON/object, opts also supports:
baseDir?: string(default:process.cwd()) for resolving relative pathsmanifestPath?: stringforResult.manifestPathand audit default-path derivation only (does not read/write manifest files)
- Default path:
${manifestPath}.log.jsonl - Each line is one JSON object (a full
Result), including steps, errors, duration, change summary. - Customize with
opts.auditLogPath. - Disable completely (caller handles logging):
opts.audit=false(CLI:--no-audit).
Common options (CommonOptions) for all APIs:
audit?: boolean(default: true)auditLogPath?: stringdryRun?: booleanincludePlanText?: booleanlogger?: { info/warn/error }
src/
api/ # high-level operations
core/ # plan/apply/fs/audit/runner/backup
manifest/ # manifest types and IO
cli/ # CLI helpers (global config)
cli.ts # CLI entrypoint (argv parsing & dispatch)
index.ts # public exports
types.ts # shared types (Result/Step/Options)
More maintainer notes: KNOWLEDGE_BASE.md.