feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (#664)#1003
Conversation
…ame or path (abhigyanpatwari#664) Add a `remove` CLI command that deletes the `.gitnexus/` index AND unregisters a repo from the global registry (~/.gitnexus/registry.json), addressing the lifecycle gap flagged in abhigyanpatwari#664: previously users had to cd into the repo to run `clean`, and there was no path-based or alias-based remove for an already-deleted working tree. - New command `gitnexus remove <target> [-f|--force]`. `<target>` is alias / basename-derived name / remote-inferred name / absolute path. - New helper `resolveRegistryEntry(entries, target)` in repo-manager.ts with path > name precedence; throws RegistryNotFoundError or RegistryAmbiguousTargetError (typed, `kind`-discriminated). - Atomicity mirrors `clean`: fs.rm first, then unregisterRepo; partial failures self-heal on next `listRegisteredRepos({ validate: true })`. - Idempotent on unknown targets (exit 0 with warning) per the abhigyanpatwari#664 spec: "behave atomically and idempotently so retries are safe". - `--force` uses `clean`-style confirmation-skip semantics — distinct from `analyze --force` (pipeline re-index); here there is no pipeline so no conflation. - 7 new unit tests cover resolver precedence, case sensitivity, ambiguity, and not-found hints; 2 integration tests cover the real CLI -> registry -> filesystem chain including the --allow-duplicate-name (abhigyanpatwari#829) ambiguity case.
|
@azizur100389 is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 6927 tests passed 97 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
|
@azizur100389 Since remove now supports custom path input, cross-platform path normalization needs to be addressed, but also for existing analyze and remove. Additionally, consider backward compatibility: paths stored by older versions of removeRepo (before normalization) should still be matchable when remove looks them up. |
|
I agree with @evander-wang please look into this plus the following findings. |
|
Claude finished @magyargergo's task in 4m 34s —— View job PR Review:
|
| Check | Status |
|---|---|
| No secrets/machine-specific paths | ✅ Pass |
| No blind find-and-replace renames | ✅ Pass (no renames) |
| No HIGH/CRITICAL impact ignored | ✅ Pass (pure addition) |
| PR title follows conventional commit format | ✅ feat(cli): gitnexus remove <target>… |
| PolyForm license untouched | ✅ Pass |
tsc --noEmit |
✅ CI typecheck passed |
vitest run |
What I could not fully verify
- Root cause of CI Tests stage failure — cannot determine from the diff whether the failure is a coverage-threshold exit, a platform-specific (Windows) runner error, or an infrastructure flake. The
vitest.config.tscoverage thresholds would need to be read to confirm. - Backward compatibility of
storagePathvalues stored by oldergitnexusversions — cannot verify without registry.json samples from prior versions; however,registerRepohas always calledgetStoragePaths(resolved)(which usespath.join), making this low-risk. - macOS symlink realpath behavior in integration tests —
os.tmpdir()on macOS returns/var/folders/...(symlink to/private/var/folders/...). The integration tests use structural assertions (entry count, basename) rather than exact paths, which sidesteps this correctly.
Final verdict
The logic is correct, the resolver contract is solid, tests are meaningful, and the implementation is clean. One item must be resolved before merge:
- CI Tests stage is ❌ — even with 0 test failures, a red CI stage must be explained and resolved before merging to
main. Confirm whether this is a coverage-threshold exit and, if so, either add unit tests forremoveCommandor adjust the threshold with a documented justification.
The Windows unregisterRepo case-sensitivity inconsistency is pre-existing and low-risk in the current flow, but worth patching before the MCP follow-up PR ships.
|
@azizur100389 Please make sure CI is healthy 🙏 |
…tforms (abhigyanpatwari#1003 review) Address review feedback from @evander-wang and @magyargergo on PR abhigyanpatwari#1003 plus the Windows + macOS CI failure (same root cause). Problem: - macOS: /var is a symlink to /private/var. `path.resolve` does NOT follow symlinks, so a child running analyze in /var/folders/X stores /private/var/folders/X (realpath from OS cwd) but an outer caller passing the symlink form misses. - Windows: GitHub runners surface tmpdirs in 8.3 short-name form (RUNNERA~1) while process.cwd() returns the long form (runneradmin). Same divergence. Fix: new `canonicalizePath(p)` helper wraps `path.resolve` plus `fs.realpathSync.native`, falling back to `path.resolve` when the path doesn't exist (preserves idempotent-on-missing semantics needed by `remove <unknown>`). Applied at 3 call-sites — registerRepo, unregisterRepo, resolveRegistryEntry — canonicalising BOTH the input and each stored `entry.path` at compare time. That last bit is the backward-compat story: registries written by older versions (pre-canonicalisation) still match correctly, so we don't need a migration script. Test side: the ambiguous-target integration test now reads the path from the registry snapshot rather than passing the outer `repoA` variable directly, so it exercises the registry contract regardless of which path form the platform stores. 4 new unit tests cover the helper (idempotent, fallback-on-missing, absolute-for-relative) plus the backward-compat resolver path.
|
@evander-wang @magyargergo — thanks for the review. Pushed Root cause of the CI redThe failing integration test was hitting cross-platform path divergence:
FixNew helper export const canonicalizePath = (p: string): string => {
const resolved = path.resolve(p);
try {
return realpathSync.native(resolved);
// macOS: /var → /private/var; Windows: RUNNERA~1 → runneradmin
} catch {
return resolved;
// path doesn't exist on disk — fall back, preserves
// resolveRegistryEntry's idempotent-on-missing semantics
}
};Applied at 3 call-sites:
Backward compatibility (addressing @evander-wang's second point)Because Test side
Local verification
CI should light up green shortly. |
| try { | ||
| await fs.rm(entry.storagePath, { recursive: true, force: true }); | ||
| await unregisterRepo(entry.path); | ||
| console.log(`Removed: ${entry.name}`); | ||
| console.log(` Path: ${entry.path}`); | ||
| console.log(` Storage: ${entry.storagePath}`); | ||
| } catch (err) { | ||
| console.error(`Failed to remove ${entry.name}:`, err); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Please be very careful here! We don't want to remove the physical path of the code base. We can safely remove files/folders int the .gitnexus folder but outside is prohibited. Please introduce safe guards here.
There was a problem hiding this comment.
Good catch, thank you. Addressed in 610ee9b9 with a guard that blocks destructive fs.rm whenever the registry entry's storagePath isn't the canonical <entry.path>/.gitnexus subfolder.
The threat model
~/.gitnexus/registry.json is a plain-text user-writable file. A corrupted or hand-edited entry could plausibly end up with:
storagePath === entry.path(the repo root → catastrophic: fs.rm recursively wipes the working tree)storagePath === ""(path.resolve resolves to cwd → rm cwd)storagePathpointing at a parent dir, sibling dir, or anywhere else
fs.rm(recursive: true, force: true) on any of those is a runtime disaster.
Fix shape
New UnsafeStoragePathError + exported assertSafeStoragePath() in repo-manager.ts. Pure lexical string check (Windows case-insensitive) asserting entry.storagePath === path.join(entry.path, '.gitnexus'). Does NOT depend on the paths existing — it's a structural integrity check on the registry, not a filesystem probe.
Audit & sibling fix
Before committing I audited every fs.rm(...storagePath...) site in the codebase:
| Site | Source of storagePath | Safety |
|---|---|---|
remove.ts |
entry.storagePath from registry |
✅ guarded |
clean.ts --all |
entry.storagePath from registry (same pattern) |
✅ guarded (found during the audit — same vulnerability, fixed in the same commit) |
clean.ts default |
findRepo(cwd) → lexically recomputed |
✅ safe by construction |
server/api.ts |
getStoragePath(entry.path) → lexically recomputed |
✅ safe by construction |
clean --all skips poisoned entries with a warning and continues with the rest of the batch — preserves its existing per-repo error-tolerance semantics (one bad entry doesn't halt cleanup).
Tests
- 8 unit tests on the guard: valid
<repo>/.gitnexus, repo-root-as-storage (catastrophic case), parent-as-storage, empty storage (→ cwd), totally-unrelated path, sibling.gitnexus(right basename, wrong parent), error payload shape, Windows case-insensitive acceptance. - 2 integration tests: poisons a registry entry's storagePath to the repo root, runs the destructive command, asserts (a) exit code, (b) the working tree +
.git/+.gitnexus/all survive on disk, (c) registry state is correct (poisoned entry retained, valid siblings cleaned). One test coversremove, one coversclean --allwith a mixed good/bad registry.
Local verification
tsc --noEmitcleanrepo-manager.test.ts49/49cli-e2e.test.ts -t "remove|clean --all"4/4 (3 remove + 1 new clean --all poisoned)- Full unit suite 4177 pass + 2 pre-existing git-utils env failures (unchanged, unrelated)
CI on 610ee9b9 should light up green shortly.
…zePath (abhigyanpatwari#1003 CI) Follow-up to c5eceba. The previous commit canonicalised the repo path at BOTH write-time AND compare-time in registerRepo — that expanded Windows 8.3 short names (RUNNER~1) to long names (runneradmin) when storing `entry.path`. Pre-existing abhigyanpatwari#829 unit tests that assert `path.resolve(err.existingPath) === path.resolve(tmpPath)` then broke because `tmpPath` is still short-form (path.resolve doesn't expand 8.3) while `entry.path` was long-form (canonicalizePath does). Fix: split storage from comparison. - entry.path stores `path.resolve(repoPath)` — whatever form the caller passed. `list` output and error messages show the path the user typed. - All compare points (existing-entry lookup in registerRepo, the collision guard, unregisterRepo, resolveRegistryEntry path tier) canonicalise BOTH sides via `canonicalizePath`. That is where the /var ↔ /private/var and RUNNER~1 ↔ runneradmin divergence actually matters. Net effect: storage is tolerant (preserves user input), matching is strict (canonical-vs-canonical). Pre-existing abhigyanpatwari#829 tests stay green because `err.existingPath` is unchanged from what `path.resolve` gives back; the cross-platform CI failure from abhigyanpatwari#1003 stays fixed because every comparison path goes through `canonicalizePath`.
|
Follow-up fix in Revised shape: split storage from comparison cleanly.
Net effect: storage is tolerant (preserves user input), matching is strict (canonical-vs-canonical). This keeps pre-existing #829 tests green AND still fixes the cross-platform matching divergence from #1003. Local verification: tsc clean · repo-manager.test.ts 41/41 · cli-e2e.test.ts |
|
@azizur100389 please look into the inline comment I left before |
…epo>/.gitnexus (abhigyanpatwari#1003 review) Address @magyargergo's inline review finding on remove.ts:89 and the sibling vulnerability in clean.ts --all (caught during a pre-commit safety audit). ~/.gitnexus/registry.json is a user-writable plain-text file, so a corrupted or hand-edited entry could point storagePath at the repo root (catastrophic: rm the working tree), an empty string (→ cwd), a parent dir, or anywhere else. fs.rm(recursive: true, force: true) on any of those is a runtime disaster. - New UnsafeStoragePathError + exported assertSafeStoragePath() in repo-manager.ts. Pure lexical string check (Windows-case- insensitive) asserting entry.storagePath === path.join(entry.path, '.gitnexus'). - Guard wired into BOTH destructive registry-trusting sites: - remove.ts: exit 1 with actionable hint - clean.ts --all: skip the poisoned entry with a warning and continue (preserves existing per-repo error tolerance — one bad entry doesn't halt the batch) - clean.ts default path and server/api.ts are safe-by-construction (they recompute storagePath from findRepo / getStoragePath rather than trusting the registry field). - 8 unit tests cover the guard (valid, repo-root, parent, empty, unrelated, sibling, error payload, Windows case). - 2 integration tests prove the full CLI path: remove-poisoned exits 1 without touching the working tree; clean --all with a poisoned sibling entry cleans the good entry, skips the bad one, and leaves the poisoned repo intact.
|
Claude finished @magyargergo's task in 5m 7s —— View job PR Review:
|
| Check | Status |
|---|---|
| No secrets/machine-specific paths | ✅ Pass |
| No blind find-and-replace renames | ✅ Pass (pure additions) |
| No HIGH/CRITICAL impact left unaddressed | ✅ Pass (audited clean --all and patched it in the same PR) |
| PR title follows conventional commit format | ✅ feat(cli): gitnexus remove <target>… |
| PolyForm license untouched | ✅ Pass |
tsc --noEmit |
✅ CI typecheck passed |
vitest run — all 3 platforms |
✅ CI run 24714532874 = success |
What I could not fully verify
realpathSync.nativeexact behavior on Windows GitHub runners at 8.3 short-name boundaries: The PR author's account is consistent with known Windows behavior, and CI passes on the Windows runner, so confidence is high — but I cannot independently reproduce the scenario.canonicalizePathperformance at scale: Not memoized; called O(n) times per registry read. With typical registry sizes (< 50) this is negligible; no test at pathological scale.
Final verdict
✅ Ready to merge — no blocking issues.
The logic is correct, the safety guard closes a real and well-specified threat (corrupted storagePath → catastrophic fs.rm), the resolver contract is clean and exported for the MCP follow-up, cross-platform path handling is consistent across all call-sites, and CI is green on the current HEAD. The one NIT (weak dry-run assertion) is not merge-relevant. The implementation is production-ready for GitNexus.
…npatwari#1003 NIT) Address the one NIT from the senior-reviewer pass on PR abhigyanpatwari#1003: the integration test was only checking for the "Run with --force" hint in dry-run output, not verifying that the three actual console.log lines (alias, repo path, storage path) appear. Same weak check on the success-branch "Removed" output. Tighten both assertions to toContain(alias), toContain(entry.path), toContain(storagePath). Catches silent format regressions — e.g. a future refactor that drops a console.log line or swaps entry.name/entry.path in the output. No code change; +20 test lines. All assertions in the happy-path integration test now fire for a meaningful reason.
|
Thanks for the bot review. Addressed the one NIT in `99c47e31` — tightened the happy-path integration test to assert the full output shape of both the dry-run and the success branch (alias + resolved path + storage path all appear), not just the `Run with --force` hint. Same NIT logic covers both the dry-run and the success-case `console.log` triples, so both are now guarded against silent format regression. Local verification on the final commit:
CI running. |
Key upstream changes merged: - feat(cli): gitnexus remove <target> command (abhigyanpatwari#1003) - feat(python): scope-based call resolution pipeline RFC abhigyanpatwari#909 Ring 3 (abhigyanpatwari#980) - feat(cli): repo fingerprinting via remote URL for sibling-clone detection (abhigyanpatwari#982) - fix(group): friendly error on missing group name (abhigyanpatwari#989) - fix(group): bubble local-impact errors in groupImpact (abhigyanpatwari#1007) - fix(fts): don't cache failed FTS index (abhigyanpatwari#1006) - fix(ci): docker alpine → debian (abhigyanpatwari#1014) - deps: graphology 0.26.0, uuid 14, @types/node 25, @types/uuid 11 Conflict resolutions: - AGENTS.md: kept our NEVER-format rules, added upstream language-specific hook rule, Tools Quick Reference table, Impact Risk Levels, gRPC guide link - package.json: kept @types/multer, took upstream node/uuid type bumps - package-lock.json: regenerated via npm install Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ame or path (abhigyanpatwari#664) (abhigyanpatwari#1003) * feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (abhigyanpatwari#664) Add a `remove` CLI command that deletes the `.gitnexus/` index AND unregisters a repo from the global registry (~/.gitnexus/registry.json), addressing the lifecycle gap flagged in abhigyanpatwari#664: previously users had to cd into the repo to run `clean`, and there was no path-based or alias-based remove for an already-deleted working tree. - New command `gitnexus remove <target> [-f|--force]`. `<target>` is alias / basename-derived name / remote-inferred name / absolute path. - New helper `resolveRegistryEntry(entries, target)` in repo-manager.ts with path > name precedence; throws RegistryNotFoundError or RegistryAmbiguousTargetError (typed, `kind`-discriminated). - Atomicity mirrors `clean`: fs.rm first, then unregisterRepo; partial failures self-heal on next `listRegisteredRepos({ validate: true })`. - Idempotent on unknown targets (exit 0 with warning) per the abhigyanpatwari#664 spec: "behave atomically and idempotently so retries are safe". - `--force` uses `clean`-style confirmation-skip semantics — distinct from `analyze --force` (pipeline re-index); here there is no pipeline so no conflation. - 7 new unit tests cover resolver precedence, case sensitivity, ambiguity, and not-found hints; 2 integration tests cover the real CLI -> registry -> filesystem chain including the --allow-duplicate-name (abhigyanpatwari#829) ambiguity case. * fix(cli): canonicalize repo paths so remove/register match across platforms (abhigyanpatwari#1003 review) Address review feedback from @evander-wang and @magyargergo on PR abhigyanpatwari#1003 plus the Windows + macOS CI failure (same root cause). Problem: - macOS: /var is a symlink to /private/var. `path.resolve` does NOT follow symlinks, so a child running analyze in /var/folders/X stores /private/var/folders/X (realpath from OS cwd) but an outer caller passing the symlink form misses. - Windows: GitHub runners surface tmpdirs in 8.3 short-name form (RUNNERA~1) while process.cwd() returns the long form (runneradmin). Same divergence. Fix: new `canonicalizePath(p)` helper wraps `path.resolve` plus `fs.realpathSync.native`, falling back to `path.resolve` when the path doesn't exist (preserves idempotent-on-missing semantics needed by `remove <unknown>`). Applied at 3 call-sites — registerRepo, unregisterRepo, resolveRegistryEntry — canonicalising BOTH the input and each stored `entry.path` at compare time. That last bit is the backward-compat story: registries written by older versions (pre-canonicalisation) still match correctly, so we don't need a migration script. Test side: the ambiguous-target integration test now reads the path from the registry snapshot rather than passing the outer `repoA` variable directly, so it exercises the registry contract regardless of which path form the platform stores. 4 new unit tests cover the helper (idempotent, fallback-on-missing, absolute-for-relative) plus the backward-compat resolver path. * fix(cli): store resolved (non-canonical) path, compare via canonicalizePath (abhigyanpatwari#1003 CI) Follow-up to c5eceba. The previous commit canonicalised the repo path at BOTH write-time AND compare-time in registerRepo — that expanded Windows 8.3 short names (RUNNER~1) to long names (runneradmin) when storing `entry.path`. Pre-existing abhigyanpatwari#829 unit tests that assert `path.resolve(err.existingPath) === path.resolve(tmpPath)` then broke because `tmpPath` is still short-form (path.resolve doesn't expand 8.3) while `entry.path` was long-form (canonicalizePath does). Fix: split storage from comparison. - entry.path stores `path.resolve(repoPath)` — whatever form the caller passed. `list` output and error messages show the path the user typed. - All compare points (existing-entry lookup in registerRepo, the collision guard, unregisterRepo, resolveRegistryEntry path tier) canonicalise BOTH sides via `canonicalizePath`. That is where the /var ↔ /private/var and RUNNER~1 ↔ runneradmin divergence actually matters. Net effect: storage is tolerant (preserves user input), matching is strict (canonical-vs-canonical). Pre-existing abhigyanpatwari#829 tests stay green because `err.existingPath` is unchanged from what `path.resolve` gives back; the cross-platform CI failure from abhigyanpatwari#1003 stays fixed because every comparison path goes through `canonicalizePath`. * fix(cli): refuse destructive fs.rm when registry storagePath isn't <repo>/.gitnexus (abhigyanpatwari#1003 review) Address @magyargergo's inline review finding on remove.ts:89 and the sibling vulnerability in clean.ts --all (caught during a pre-commit safety audit). ~/.gitnexus/registry.json is a user-writable plain-text file, so a corrupted or hand-edited entry could point storagePath at the repo root (catastrophic: rm the working tree), an empty string (→ cwd), a parent dir, or anywhere else. fs.rm(recursive: true, force: true) on any of those is a runtime disaster. - New UnsafeStoragePathError + exported assertSafeStoragePath() in repo-manager.ts. Pure lexical string check (Windows-case- insensitive) asserting entry.storagePath === path.join(entry.path, '.gitnexus'). - Guard wired into BOTH destructive registry-trusting sites: - remove.ts: exit 1 with actionable hint - clean.ts --all: skip the poisoned entry with a warning and continue (preserves existing per-repo error tolerance — one bad entry doesn't halt the batch) - clean.ts default path and server/api.ts are safe-by-construction (they recompute storagePath from findRepo / getStoragePath rather than trusting the registry field). - 8 unit tests cover the guard (valid, repo-root, parent, empty, unrelated, sibling, error payload, Windows case). - 2 integration tests prove the full CLI path: remove-poisoned exits 1 without touching the working tree; clean --all with a poisoned sibling entry cleans the good entry, skips the bad one, and leaves the poisoned repo intact. * test(cli): assert full remove dry-run + success output shape (abhigyanpatwari#1003 NIT) Address the one NIT from the senior-reviewer pass on PR abhigyanpatwari#1003: the integration test was only checking for the "Run with --force" hint in dry-run output, not verifying that the three actual console.log lines (alias, repo path, storage path) appear. Same weak check on the success-branch "Removed" output. Tighten both assertions to toContain(alias), toContain(entry.path), toContain(storagePath). Catches silent format regressions — e.g. a future refactor that drops a console.log line or swaps entry.name/entry.path in the output. No code change; +20 test lines. All assertions in the happy-path integration test now fire for a meaningful reason.
Problem (#664)
The existing
cleancommand is cwd-scoped — it deletes the.gitnexus/index for whichever repo you're standing in. That leaves two gaps:cd-ing into it first (awkward in scripts; impossible if the working tree has been deleted).cleanhas no way to say "remove the one I registered asapp-a" or "remove the one at/old/path/project"; it's strictlyfindRepo(cwd).Issue #664 asks for a
removeAPI that closes both gaps: identify a repo via a unique identifier (alias, name, or path), remove associated data atomically and idempotently.What this PR adds
New command:
gitnexus remove <target> [-f|--force]<target>is resolved against~/.gitnexus/registry.jsonviaresolveRegistryEntry()with a tiered precedence:namematch (case-insensitive) — if two entries share the alias (only possible after--allow-duplicate-namefrom analyze has no way to disambiguate repos with the same basename — -r <name> becomes ambiguous #829), throwsRegistryAmbiguousTargetErrorwith both paths surfacedRegistryNotFoundErrorwith a disambiguated available-names listNo fuzzy / partial matching — destructive commands should be unambiguous and scriptable.
Behaviour
--force) is a dry-run that lists what would be deleted. Mirrorsclean's confirmation-gate pattern.--forcedeletes.gitnexus/storage and unregisters from the global registry.remove X && analyze Ykeeps working in scripts. This matches the Add a feature to remove a indexed repo #664 spec explicitly: "behave atomically and idempotently so retries are safe".clean.ts:fs.rmfirst →unregisterReposecond. Partial failure leaves the registry pointing at a missing dir (recoverable bylistRegisteredRepos({ validate: true })on next read) rather than orphaning.gitnexus/on disk.--forcesemantics matchclean -f(skip the safety prompt). Distinct fromanalyze --force(pipeline re-index) — no conflation because there's no pipeline inremove.What's NOT in this PR (deliberate scope trim)
resolveRegistryEntryisexported specifically so the MCP-sideremovetool is a trivial follow-up PR that reuses the same resolver. Keeping this PR CLI-only keeps the diff reviewable.clean—cleanremains cwd-scoped. The two commands now cover complementary workflows (cleanfor "I'm here, delete this";removefor "delete that one over there").Tests
Unit (7 new in
test/unit/repo-manager.test.ts)RegistryAmbiguousTargetErrorwith both paths in the messageRegistryNotFoundErrorwith a disambiguated available-names hint (app (path)whenappappears twice)Integration (2 new in
test/integration/cli-e2e.test.ts)analyze --name alias-a→ dry-runremove alias-a(verify state unchanged) →remove alias-a --force(verify.gitnexus/gone + registry empty) →remove alias-aagain (verify exit 0 + "Nothing to remove" warning — idempotent)--name sharedwith--allow-duplicate-name→remove shared --forceerrors with exit 1 and both paths surfaced, registry unchanged →remove <absolute-path> --forcesucceeds, leaves the other survivorBoth tests sandbox via
GITNEXUS_HOMEso they never touch the developer's real~/.gitnexus/registry.json. Structural assertions (entry count, basename, path distinctness) avoid the path-realpath flakiness that bit #955 on cross-platform CI.Verification
From
gitnexus/:Pre-commit hook (lint-staged + eslint --fix + prettier --write + gitnexus tsc) passes.
Backwards compatibility
registry.jsonfiles load unchanged.clean,analyze,list,statusare all untouched. Only a new command is added.Closes #664.