From fd7a85a56d2e0a0590ff7b3693139c4ff647d760 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 14:19:35 -0700 Subject: [PATCH] fix(backlinks): honor positional check-backlinks directory argument The help text (gbrain check-backlinks [dir]) promised a positional directory argument, but runBacklinks only parsed --dir and defaulted to cwd, so the walker ran from the wrong root and could hit EPERM on unreadable sibling dirs. Extract parseBacklinksArgs: positional [dir] is now honored, --dir still overrides it, --dry-run preserved, and a --dir flag missing its value falls back to the positional dir instead of picking up undefined. Takeover of #852 (rebased onto master past the findBacklinkGaps dedupe test block). Fixes #485. Co-authored-by: Kage18 Co-Authored-By: Claude Fable 5 --- src/commands/backlinks.ts | 48 ++++++++++++++++++++++++++++++++------- test/backlinks.test.ts | 24 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/commands/backlinks.ts b/src/commands/backlinks.ts index bdde915c1..616afb947 100644 --- a/src/commands/backlinks.ts +++ b/src/commands/backlinks.ts @@ -5,8 +5,8 @@ * checks if back-links exist, and optionally creates them. * * Usage: - * gbrain check-backlinks check [--dir ] # report missing back-links - * gbrain check-backlinks fix [--dir ] # create missing back-links + * gbrain check-backlinks check [dir] [--dir ] # report missing back-links + * gbrain check-backlinks fix [dir] [--dir ] # create missing back-links * gbrain check-backlinks fix --dry-run # preview fixes */ @@ -201,6 +201,40 @@ export interface BacklinksResult { dryRun: boolean; } +export interface ParsedBacklinksArgs { + subcommand: string | undefined; + brainDir: string; + dryRun: boolean; +} + +export function parseBacklinksArgs(args: string[]): ParsedBacklinksArgs { + const subcommand = args[0]; + const dryRun = args.includes('--dry-run'); + const dirIdx = args.indexOf('--dir'); + const flagDir = dirIdx >= 0 && args[dirIdx + 1] && !args[dirIdx + 1].startsWith('--') + ? args[dirIdx + 1] + : undefined; + + let positionalDir: string | undefined; + for (let i = 1; i < args.length; i++) { + const arg = args[i]; + if (arg === '--dir') { + i++; + continue; + } + if (arg === '--dry-run') continue; + if (arg.startsWith('--')) continue; + positionalDir = arg; + break; + } + + return { + subcommand, + brainDir: flagDir ?? positionalDir ?? '.', + dryRun, + }; +} + /** * Library-level backlinks check/fix. Throws on validation errors; returns a * structured result so Minions handlers + autopilot-cycle can surface counts. @@ -236,16 +270,14 @@ export async function runBacklinksCore(opts: BacklinksOpts): Promise= 0 ? args[dirIdx + 1] : '.'; - const dryRun = args.includes('--dry-run'); + const { subcommand, brainDir, dryRun } = parseBacklinksArgs(args); if (!subcommand || !['check', 'fix'].includes(subcommand)) { - console.error('Usage: gbrain check-backlinks [--dir ] [--dry-run]'); + console.error('Usage: gbrain check-backlinks [dir] [--dir ] [--dry-run]'); console.error(' check Report missing back-links'); console.error(' fix Create missing back-links (appends to Timeline)'); - console.error(' --dir Brain directory (default: current directory)'); + console.error(' dir Brain directory (default: current directory)'); + console.error(' --dir Brain directory override'); console.error(' --dry-run Preview fixes without writing'); process.exit(1); } diff --git a/test/backlinks.test.ts b/test/backlinks.test.ts index 649af1ed3..647dc98ea 100644 --- a/test/backlinks.test.ts +++ b/test/backlinks.test.ts @@ -4,6 +4,7 @@ import { extractPageTitle, hasBacklink, buildBacklinkEntry, + parseBacklinksArgs, } from '../src/commands/backlinks.ts'; describe('extractEntityRefs', () => { @@ -104,3 +105,26 @@ describe('findBacklinkGaps dedupe (v0.36.x #967 regression)', () => { } }); }); + +describe('parseBacklinksArgs', () => { + test('uses positional dir for check and fix subcommands', () => { + expect(parseBacklinksArgs(['check', '/tmp/brain']).brainDir).toBe('/tmp/brain'); + expect(parseBacklinksArgs(['fix', '/tmp/brain']).brainDir).toBe('/tmp/brain'); + }); + + test('defaults to cwd when no dir given', () => { + expect(parseBacklinksArgs(['check']).brainDir).toBe('.'); + }); + + test('--dir overrides positional dir and preserves dry-run', () => { + const parsed = parseBacklinksArgs(['fix', '/tmp/ignored', '--dir', '/tmp/brain', '--dry-run']); + expect(parsed.subcommand).toBe('fix'); + expect(parsed.brainDir).toBe('/tmp/brain'); + expect(parsed.dryRun).toBe(true); + }); + + test('--dir missing its value falls back to positional dir', () => { + expect(parseBacklinksArgs(['check', '/tmp/brain', '--dir']).brainDir).toBe('/tmp/brain'); + expect(parseBacklinksArgs(['check', '--dir', '--dry-run']).brainDir).toBe('.'); + }); +});