clank-cli 0.1.61 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,9 @@ clank link ~/my-project # Link to specific project
84
84
 
85
85
  Move a file to the overlay and replace it with a symlink. If the file doesn't exist, an empty file is created. Accepts [scope options](#scope-options).
86
86
 
87
+ **Options:**
88
+ - `-i, --interactive` - Interactively add all unadded files, prompting for scope on each
89
+
87
90
  **Examples:**
88
91
  ```bash
89
92
  clank add style.md # Project scope (default)
@@ -92,6 +95,24 @@ clank add notes.md --worktree # Worktree scope
92
95
  clank add .claude/commands/review.md --global # Global command
93
96
  clank add .claude/commands/build.md # Project command (default)
94
97
  clank add CLAUDE.md # Creates agents.md + agent symlinks
98
+ clank add -i # Interactive mode for all unadded files
99
+ ```
100
+
101
+ **Interactive mode:**
102
+
103
+ Running `clank add --interactive` finds all unadded files and prompts for each:
104
+
105
+ ```
106
+ Found 3 unadded file(s):
107
+
108
+ [1/3] clank/notes.md
109
+ (P)roject (W)orktree (G)lobal (S)kip (Q)uit [P]:
110
+ ```
111
+
112
+ Press a single key to select the scope (default: Project). The summary shows what was added:
113
+
114
+ ```
115
+ Added 2 to project, 1 skipped
95
116
  ```
96
117
 
97
118
  ### `clank unlink [target]`
@@ -173,10 +194,13 @@ clank rm style.md --global # Remove global style guide
173
194
 
174
195
  ### `clank mv <files...>` (alias: `move`)
175
196
 
176
- Move file(s) between overlay scopes. Requires one [scope option](#scope-options) to specify the destination.
197
+ Move or rename file(s) in the overlay. With a [scope option](#scope-options), moves files to that scope. With two arguments and no scope, renames within the current scope.
177
198
 
178
- **Example:**
199
+ **Examples:**
179
200
  ```bash
201
+ # Rename a file (keeps same scope)
202
+ clank mv batch-dry-check.md batch-dry.md
203
+
180
204
  # Promote a worktree note to project scope
181
205
  clank mv clank/notes.md --project
182
206
 
@@ -205,7 +229,7 @@ clank help structure
205
229
 
206
230
  ### `--config <path>` (global option)
207
231
 
208
- Specify a custom config file location (default `~/.config/clank/config.js`).
232
+ Specify a custom config file location (default `~/.config/clank/clank.config.js`).
209
233
 
210
234
  ```bash
211
235
  clank --config /tmp/test-config.js init /tmp/test-overlay
@@ -243,7 +267,7 @@ clank/
243
267
 
244
268
  ## Configuration
245
269
 
246
- Global configuration is stored by default in `~/.config/clank/config.js`:
270
+ Global configuration is stored in `~/.config/clank/clank.config.js`:
247
271
 
248
272
  ```javascript
249
273
  export default {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clank-cli",
3
3
  "description": "Keep AI files in a separate overlay repository",
4
- "version": "0.1.61",
4
+ "version": "0.1.65",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -46,10 +46,11 @@
46
46
  },
47
47
  "scripts": {
48
48
  "bump": "monobump",
49
- "fix": "biome check --fix --unsafe",
49
+ "fix": "biome check --fix --unsafe --error-on-warnings",
50
50
  "fix:pkgJsonFormat": "syncpack format",
51
51
  "global": "pnpm link --global",
52
52
  "lint": "biome check --error-on-warnings",
53
+ "prepush": "run-s fix typecheck fix:pkgJsonFormat test:once",
53
54
  "test": "vitest",
54
55
  "test:once": "vitest run",
55
56
  "test:ui": "vitest --ui",
package/src/AgentFiles.ts CHANGED
@@ -32,8 +32,7 @@ export async function forEachAgentPath(
32
32
  ): Promise<void> {
33
33
  const agentPaths = getAgentFilePaths(dir);
34
34
  for (const agent of agents) {
35
- const key = agent.toLowerCase();
36
- const agentPath = agentPaths[key];
35
+ const agentPath = agentPaths[agent.toLowerCase()];
37
36
  if (!agentPath) continue;
38
37
  await fn(agentPath, agent);
39
38
  }
@@ -25,8 +25,6 @@ export interface AgentFileClassification {
25
25
  outdatedSymlinks: OutdatedSymlink[];
26
26
  }
27
27
 
28
- type PartialClassification = Partial<AgentFileClassification>;
29
-
30
28
  export interface OutdatedSymlink {
31
29
  /** Path to the symlink in the target */
32
30
  symlinkPath: string;
@@ -38,6 +36,8 @@ export interface OutdatedSymlink {
38
36
  expectedTarget: string;
39
37
  }
40
38
 
39
+ type PartialClassification = Partial<AgentFileClassification>;
40
+
41
41
  /** Find all agent files in the repository and classify them.
42
42
  * Returns absolute paths in the classification.
43
43
  */
@@ -61,14 +61,10 @@ export async function classifyAgentFiles(
61
61
  }
62
62
 
63
63
  /** @return true if classification has any problems */
64
- export function agentFileProblems(
65
- classification: AgentFileClassification,
66
- ): boolean {
67
- return (
68
- classification.tracked.length > 0 ||
69
- classification.untracked.length > 0 ||
70
- classification.staleSymlinks.length > 0 ||
71
- classification.outdatedSymlinks.length > 0
64
+ export function agentFileProblems(c: AgentFileClassification): boolean {
65
+ const { tracked, untracked, staleSymlinks, outdatedSymlinks } = c;
66
+ return [tracked, untracked, staleSymlinks, outdatedSymlinks].some(
67
+ (a) => a.length > 0,
72
68
  );
73
69
  }
74
70
 
@@ -110,10 +106,12 @@ ${commands.join("\n")}`);
110
106
  }
111
107
 
112
108
  if (classified.outdatedSymlinks.length > 0) {
113
- const details = classified.outdatedSymlinks.map((s) => {
114
- const symlinkRel = rel(s.symlinkPath);
115
- return ` ${symlinkRel}\n points to: ${s.currentTarget}\n expected: ${s.expectedTarget}`;
116
- });
109
+ const details = classified.outdatedSymlinks.map(
110
+ (s) =>
111
+ ` ${rel(s.symlinkPath)}\n` +
112
+ ` points to: ${s.currentTarget}\n` +
113
+ ` expected: ${s.expectedTarget}`,
114
+ );
117
115
  sections.push(`Found outdated agent symlinks (pointing to wrong overlay path).
118
116
 
119
117
  This typically happens after a directory rename. Remove symlinks and run \`clank link\`:
@@ -127,6 +125,21 @@ To fix:
127
125
  return sections.join("\n\n");
128
126
  }
129
127
 
128
+ /** Find all agent files in the repository */
129
+ async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
130
+ const files: string[] = [];
131
+ const agentFileSet = new Set(agentFiles);
132
+
133
+ for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
134
+ if (isDirectory) continue;
135
+ if (agentFileSet.has(basename(path))) {
136
+ files.push(path);
137
+ }
138
+ }
139
+
140
+ return files;
141
+ }
142
+
130
143
  /** Classify a single agent file */
131
144
  async function classifySingleAgentFile(
132
145
  filePath: string,
@@ -146,6 +159,18 @@ async function classifySingleAgentFile(
146
159
  return {};
147
160
  }
148
161
 
162
+ /** Merge sparse classifications into a complete classification with arrays */
163
+ function mergeClassifications(
164
+ items: PartialClassification[],
165
+ ): AgentFileClassification {
166
+ return {
167
+ tracked: items.flatMap((i) => i.tracked ?? []),
168
+ untracked: items.flatMap((i) => i.untracked ?? []),
169
+ staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
170
+ outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
171
+ };
172
+ }
173
+
149
174
  /** Classify an agent symlink - check if stale or outdated */
150
175
  async function classifyAgentSymlink(
151
176
  filePath: string,
@@ -165,44 +190,14 @@ async function classifyAgentSymlink(
165
190
  const agentsMdPath = join(dirname(filePath), "agents.md");
166
191
  const expectedTarget = targetToOverlay(agentsMdPath, "project", mapperCtx);
167
192
  if (absoluteTarget !== expectedTarget) {
168
- return {
169
- outdatedSymlinks: [
170
- {
171
- symlinkPath: filePath,
172
- currentTarget: absoluteTarget,
173
- expectedTarget,
174
- },
175
- ],
193
+ const symlink = {
194
+ symlinkPath: filePath,
195
+ currentTarget: absoluteTarget,
196
+ expectedTarget,
176
197
  };
198
+ return { outdatedSymlinks: [symlink] };
177
199
  }
178
200
  }
179
201
 
180
202
  return {};
181
203
  }
182
-
183
- /** Merge sparse classifications into a complete classification with arrays */
184
- function mergeClassifications(
185
- items: PartialClassification[],
186
- ): AgentFileClassification {
187
- return {
188
- tracked: items.flatMap((i) => i.tracked ?? []),
189
- untracked: items.flatMap((i) => i.untracked ?? []),
190
- staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
191
- outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
192
- };
193
- }
194
-
195
- /** Find all agent files in the repository */
196
- async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
197
- const files: string[] = [];
198
- const agentFileSet = new Set(agentFiles);
199
-
200
- for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
201
- if (isDirectory) continue;
202
- if (agentFileSet.has(basename(path))) {
203
- files.push(path);
204
- }
205
- }
206
-
207
- return files;
208
- }
package/src/Cli.ts CHANGED
@@ -93,6 +93,24 @@ function registerCoreCommands(program: Command): void {
93
93
  registerUtilityCommands(program);
94
94
  }
95
95
 
96
+ function registerHelpCommands(program: Command): void {
97
+ const help = program
98
+ .command("help")
99
+ .description("Show help information")
100
+ .argument("[command]", "Command to show help for")
101
+ .action((commandName?: string) => {
102
+ if (!commandName) return program.help();
103
+ const subcommand = program.commands.find((c) => c.name() === commandName);
104
+ if (subcommand) return subcommand.help();
105
+ console.error(`Unknown command: ${commandName}`);
106
+ process.exit(1);
107
+ });
108
+ help
109
+ .command("structure")
110
+ .description("Show overlay directory structure")
111
+ .action(() => console.log(structureHelp));
112
+ }
113
+
96
114
  function registerOverlayCommands(program: Command): void {
97
115
  program
98
116
  .command("init")
@@ -115,9 +133,10 @@ function registerOverlayCommands(program: Command): void {
115
133
  .command("add")
116
134
  .description("Add file(s) to overlay and create symlinks")
117
135
  .argument(
118
- "<files...>",
136
+ "[files...]",
119
137
  "File path(s) (e.g., style.md, .claude/commands/review.md)",
120
138
  )
139
+ .option("-i, --interactive", "Interactively add all unadded files")
121
140
  .option("-g, --global", "Add to global location (all projects)")
122
141
  .option("-p, --project", "Add to project location (default)")
123
142
  .option("-w, --worktree", "Add to worktree location (this branch only)")
@@ -155,9 +174,63 @@ function registerUtilityCommands(program: Command): void {
155
174
  .action(withErrorHandling(vscodeCommand));
156
175
  }
157
176
 
177
+ function withErrorHandling<T extends unknown[]>(
178
+ fn: (...args: T) => Promise<void>,
179
+ ): (...args: T) => Promise<void> {
180
+ return async (...args: T) => {
181
+ try {
182
+ await fn(...args);
183
+ } catch (error) {
184
+ console.error("Error:", error instanceof Error ? error.message : error);
185
+ process.exit(1);
186
+ }
187
+ };
188
+ }
189
+
190
+ function registerRmCommand(program: Command): void {
191
+ program
192
+ .command("rm")
193
+ .alias("remove")
194
+ .description("Remove file(s) from overlay and target")
195
+ .argument("<files...>", "File(s) to remove")
196
+ .option("-g, --global", "Remove from global scope")
197
+ .option("-p, --project", "Remove from project scope")
198
+ .option("-w, --worktree", "Remove from worktree scope")
199
+ .action(withErrorHandling(rmCommand));
200
+ }
201
+
202
+ function registerMvCommand(program: Command): void {
203
+ const cmd = program
204
+ .command("mv")
205
+ .alias("move")
206
+ .description("Move or rename file(s) in overlay")
207
+ .argument("<files...>", "File(s) to move");
208
+
209
+ cmd.addOption(
210
+ new Option("-g, --global", "Move to global scope").conflicts([
211
+ "project",
212
+ "worktree",
213
+ ]),
214
+ );
215
+ cmd.addOption(
216
+ new Option("-p, --project", "Move to project scope").conflicts([
217
+ "global",
218
+ "worktree",
219
+ ]),
220
+ );
221
+ cmd.addOption(
222
+ new Option("-w, --worktree", "Move to worktree scope").conflicts([
223
+ "global",
224
+ "project",
225
+ ]),
226
+ );
227
+ cmd.action(withErrorHandling(moveCommand));
228
+ }
229
+
158
230
  function registerFilesCommand(program: Command): void {
159
231
  const files = program
160
232
  .command("files")
233
+ .alias("list")
161
234
  .description("List clank-managed files (paths relative to cwd)")
162
235
  .argument(
163
236
  "[path]",
@@ -201,74 +274,3 @@ function registerFilesCommand(program: Command): void {
201
274
 
202
275
  files.action(withErrorHandling(filesCommand));
203
276
  }
204
-
205
- function registerHelpCommands(program: Command): void {
206
- const help = program
207
- .command("help")
208
- .description("Show help information")
209
- .argument("[command]", "Command to show help for")
210
- .action((commandName?: string) => {
211
- if (!commandName) return program.help();
212
- const subcommand = program.commands.find((c) => c.name() === commandName);
213
- if (subcommand) return subcommand.help();
214
- console.error(`Unknown command: ${commandName}`);
215
- process.exit(1);
216
- });
217
- help
218
- .command("structure")
219
- .description("Show overlay directory structure")
220
- .action(() => console.log(structureHelp));
221
- }
222
-
223
- function registerRmCommand(program: Command): void {
224
- program
225
- .command("rm")
226
- .alias("remove")
227
- .description("Remove file(s) from overlay and target")
228
- .argument("<files...>", "File(s) to remove")
229
- .option("-g, --global", "Remove from global scope")
230
- .option("-p, --project", "Remove from project scope")
231
- .option("-w, --worktree", "Remove from worktree scope")
232
- .action(withErrorHandling(rmCommand));
233
- }
234
-
235
- function registerMvCommand(program: Command): void {
236
- const cmd = program
237
- .command("mv")
238
- .alias("move")
239
- .description("Move file(s) between overlay scopes")
240
- .argument("<files...>", "File(s) to move");
241
-
242
- cmd.addOption(
243
- new Option("-g, --global", "Move to global scope").conflicts([
244
- "project",
245
- "worktree",
246
- ]),
247
- );
248
- cmd.addOption(
249
- new Option("-p, --project", "Move to project scope").conflicts([
250
- "global",
251
- "worktree",
252
- ]),
253
- );
254
- cmd.addOption(
255
- new Option("-w, --worktree", "Move to worktree scope").conflicts([
256
- "global",
257
- "project",
258
- ]),
259
- );
260
- cmd.action(withErrorHandling(moveCommand));
261
- }
262
-
263
- function withErrorHandling<T extends unknown[]>(
264
- fn: (...args: T) => Promise<void>,
265
- ): (...args: T) => Promise<void> {
266
- return async (...args: T) => {
267
- try {
268
- await fn(...args);
269
- } catch (error) {
270
- console.error("Error:", error instanceof Error ? error.message : error);
271
- process.exit(1);
272
- }
273
- };
274
- }
package/src/Config.ts CHANGED
@@ -4,8 +4,6 @@ import { join } from "node:path";
4
4
  import { cosmiconfig } from "cosmiconfig";
5
5
  import { fileExists } from "./FsUtil.ts";
6
6
 
7
- export const defaultOverlayDir = "clankover";
8
-
9
7
  export interface ClankConfig {
10
8
  overlayRepo: string;
11
9
  agents: string[];
@@ -17,6 +15,8 @@ export interface ClankConfig {
17
15
  ignore?: string[];
18
16
  }
19
17
 
18
+ export const defaultOverlayDir = "clankover";
19
+
20
20
  const defaultConfig: ClankConfig = {
21
21
  overlayRepo: join(homedir(), defaultOverlayDir),
22
22
  agents: ["agents", "claude", "gemini"],
@@ -30,14 +30,11 @@ export function setConfigPath(path: string | undefined): void {
30
30
  customConfigPath = path;
31
31
  }
32
32
 
33
- /** Load global clank configuration from ~/.config/clank/config.js or similar */
33
+ /** Load global clank configuration from ~/.config/clank/clank.config.js or similar */
34
34
  export async function loadConfig(): Promise<ClankConfig> {
35
35
  if (customConfigPath) {
36
36
  const result = await explorer.load(customConfigPath);
37
- if (!result) {
38
- throw new Error(`Config file not found: ${customConfigPath}`);
39
- }
40
- if (result.isEmpty) {
37
+ if (!result || result.isEmpty) {
41
38
  return defaultConfig;
42
39
  }
43
40
  return { ...defaultConfig, ...result.config };
@@ -50,10 +47,10 @@ export async function loadConfig(): Promise<ClankConfig> {
50
47
  return { ...defaultConfig, ...result.config };
51
48
  }
52
49
 
53
- /** Create default configuration file at ~/.config/clank/config.js */
50
+ /** Create default configuration file at ~/.config/clank/clank.config.js */
54
51
  export async function createDefaultConfig(overlayRepo?: string): Promise<void> {
55
52
  const configDir = getConfigDir();
56
- const configPath = customConfigPath || join(configDir, "config.js");
53
+ const configPath = customConfigPath || join(configDir, "clank.config.js");
57
54
 
58
55
  const config = {
59
56
  ...defaultConfig,
@@ -84,12 +81,6 @@ export async function getOverlayPath(): Promise<string> {
84
81
  return expandPath(config.overlayRepo);
85
82
  }
86
83
 
87
- /** Get the XDG config directory (respects XDG_CONFIG_HOME, defaults to ~/.config/clank) */
88
- function getConfigDir(): string {
89
- const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
90
- return join(xdgConfig, "clank");
91
- }
92
-
93
84
  /** Validate overlay repository exists, throw if not */
94
85
  export async function validateOverlayExists(
95
86
  overlayRoot: string,
@@ -100,3 +91,9 @@ export async function validateOverlayExists(
100
91
  );
101
92
  }
102
93
  }
94
+
95
+ /** Get the XDG config directory (respects XDG_CONFIG_HOME, defaults to ~/.config/clank) */
96
+ function getConfigDir(): string {
97
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
98
+ return join(xdgConfig, "clank");
99
+ }
package/src/Exclude.ts CHANGED
@@ -102,15 +102,6 @@ export async function removeGitExcludes(targetRoot: string): Promise<void> {
102
102
  console.log("Removed clank entries from .git/info/exclude");
103
103
  }
104
104
 
105
- /** Remove the clank section from exclude file content */
106
- function removeClankSection(content: string): string {
107
- const pattern = new RegExp(
108
- `\\n*${clankMarkerStart}[\\s\\S]*?${clankMarkerEnd}\\n*`,
109
- "g",
110
- );
111
- return content.replace(pattern, "\n");
112
- }
113
-
114
105
  /** Filter out clank section from lines */
115
106
  export function filterClankLines(lines: string[]): string[] {
116
107
  const result: string[] = [];
@@ -129,6 +120,15 @@ export function filterClankLines(lines: string[]): string[] {
129
120
  return result;
130
121
  }
131
122
 
123
+ /** Remove the clank section from exclude file content */
124
+ function removeClankSection(content: string): string {
125
+ const pattern = new RegExp(
126
+ `\\n*${clankMarkerStart}[\\s\\S]*?${clankMarkerEnd}\\n*`,
127
+ "g",
128
+ );
129
+ return content.replace(pattern, "\n");
130
+ }
131
+
132
132
  /** Check if a directory has any tracked files */
133
133
  async function hasTrackedFiles(
134
134
  dirPath: string,
package/src/Git.ts CHANGED
@@ -115,16 +115,6 @@ export function parseRepoName(url: string): string | null {
115
115
  return basename(normalizedUrl);
116
116
  }
117
117
 
118
- /** Execute a git command and return stdout, or null if it fails */
119
- async function gitCommand(args: string, cwd?: string): Promise<string | null> {
120
- try {
121
- const { stdout } = await exec(`git ${args}`, { cwd });
122
- return stdout.trim();
123
- } catch {
124
- return null;
125
- }
126
- }
127
-
128
118
  /** Get the .git directory for the current worktree */
129
119
  export async function getGitDir(cwd: string): Promise<string | null> {
130
120
  const gitDir = await gitCommand("rev-parse --git-dir", cwd);
@@ -138,3 +128,13 @@ export async function getGitCommonDir(cwd: string): Promise<string | null> {
138
128
  if (!gitDir) return null;
139
129
  return isAbsolute(gitDir) ? gitDir : join(cwd, gitDir);
140
130
  }
131
+
132
+ /** Execute a git command and return stdout, or null if it fails */
133
+ async function gitCommand(args: string, cwd?: string): Promise<string | null> {
134
+ try {
135
+ const { stdout } = await exec(`git ${args}`, { cwd });
136
+ return stdout.trim();
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
package/src/Gitignore.ts CHANGED
@@ -99,24 +99,16 @@ export function isClankPattern(glob: string): boolean {
99
99
  const normalized = glob.replace(/^\*\*\//, "").replace(/\/$/, "");
100
100
 
101
101
  // Check against managed directories
102
- for (const dir of targetManagedDirs) {
103
- if (
104
- normalized === dir ||
105
- normalized.startsWith(`${dir}/`) ||
106
- normalized.endsWith(`/${dir}`)
107
- ) {
108
- return true;
109
- }
110
- }
102
+ const matchesDir = (dir: string) =>
103
+ normalized === dir ||
104
+ normalized.startsWith(`${dir}/`) ||
105
+ normalized.endsWith(`/${dir}`);
106
+ if (targetManagedDirs.some(matchesDir)) return true;
111
107
 
112
108
  // Check against agent files
113
- for (const agentFile of agentFiles) {
114
- if (normalized === agentFile || normalized.endsWith(`/${agentFile}`)) {
115
- return true;
116
- }
117
- }
118
-
119
- return false;
109
+ const matchesAgent = (f: string) =>
110
+ normalized === f || normalized.endsWith(`/${f}`);
111
+ return agentFiles.some(matchesAgent);
120
112
  }
121
113
 
122
114
  /** Convert collected patterns to VS Code exclude globs, filtering clank patterns */
@@ -143,14 +135,11 @@ export function deduplicateGlobs(globs: string[]): string[] {
143
135
  const coveredSuffixes = new Set(universal.map((g) => g.slice(3)));
144
136
 
145
137
  // Keep specific patterns not covered by a universal pattern
146
- const uncovered = specific.filter((glob) => {
147
- for (const suffix of coveredSuffixes) {
148
- if (glob.endsWith(`/**/${suffix}`) || glob.endsWith(`/${suffix}`)) {
149
- return false;
150
- }
151
- }
152
- return true;
153
- });
138
+ const isCovered = (glob: string) =>
139
+ [...coveredSuffixes].some(
140
+ (s) => glob.endsWith(`/**/${s}`) || glob.endsWith(`/${s}`),
141
+ );
142
+ const uncovered = specific.filter((glob) => !isCovered(glob));
154
143
 
155
144
  return [...universal, ...uncovered];
156
145
  }
@@ -169,22 +158,19 @@ async function parseGitignoreFile(
169
158
  result.negationWarnings.push(...parsed.negationWarnings);
170
159
  }
171
160
 
172
- /** Parse a single gitignore line */
173
- function parseLine(
174
- trimmed: string,
175
- source: string,
176
- basePath: string,
177
- ): { pattern?: GitignorePattern; negation?: string } {
178
- // Skip empty lines and comments
179
- if (!trimmed || trimmed.startsWith("#")) return {};
180
-
181
- const isNegation = trimmed.startsWith("!");
182
- const pattern = isNegation ? trimmed.slice(1) : trimmed;
161
+ /** Find all nested .gitignore files (excluding root) */
162
+ async function findNestedGitignores(targetRoot: string): Promise<string[]> {
163
+ const gitignores: string[] = [];
164
+ const rootGitignore = join(targetRoot, ".gitignore");
183
165
 
184
- if (isNegation) {
185
- return { negation: pattern };
166
+ for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
167
+ if (isDirectory) continue;
168
+ if (basename(path) === ".gitignore" && path !== rootGitignore) {
169
+ gitignores.push(path);
170
+ }
186
171
  }
187
- return { pattern: { pattern, basePath, negation: false, source } };
172
+
173
+ return gitignores;
188
174
  }
189
175
 
190
176
  /** Parse gitignore file content into patterns */
@@ -211,17 +197,20 @@ function parseGitignoreContent(
211
197
  return { patterns, negationWarnings };
212
198
  }
213
199
 
214
- /** Find all nested .gitignore files (excluding root) */
215
- async function findNestedGitignores(targetRoot: string): Promise<string[]> {
216
- const gitignores: string[] = [];
217
- const rootGitignore = join(targetRoot, ".gitignore");
200
+ /** Parse a single gitignore line */
201
+ function parseLine(
202
+ trimmed: string,
203
+ source: string,
204
+ basePath: string,
205
+ ): { pattern?: GitignorePattern; negation?: string } {
206
+ // Skip empty lines and comments
207
+ if (!trimmed || trimmed.startsWith("#")) return {};
218
208
 
219
- for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
220
- if (isDirectory) continue;
221
- if (basename(path) === ".gitignore" && path !== rootGitignore) {
222
- gitignores.push(path);
223
- }
224
- }
209
+ const isNegation = trimmed.startsWith("!");
210
+ const pattern = isNegation ? trimmed.slice(1) : trimmed;
225
211
 
226
- return gitignores;
212
+ if (isNegation) {
213
+ return { negation: pattern };
214
+ }
215
+ return { pattern: { pattern, basePath, negation: false, source } };
227
216
  }