clank-cli 0.1.59 → 0.1.62

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.
@@ -51,6 +51,12 @@ interface LinkedFile {
51
51
  scope: Scope;
52
52
  }
53
53
 
54
+ interface SeparatedMappings {
55
+ agentsMappings: FileMapping[];
56
+ promptsMappings: FileMapping[];
57
+ regularMappings: FileMapping[];
58
+ }
59
+
54
60
  /** Link overlay repository to target directory */
55
61
  export async function linkCommand(targetDir?: string): Promise<void> {
56
62
  const gitContext = await getGitContext(targetDir || process.cwd());
@@ -68,19 +74,9 @@ export async function linkCommand(targetDir?: string): Promise<void> {
68
74
  await ensureDir(join(overlayRoot, "targets", gitContext.projectName));
69
75
  await maybeInitWorktree(overlayRoot, gitContext);
70
76
 
71
- // Collect and separate mappings (agents.md and prompts get special handling)
72
- const mappings = await overlayMappings(overlayRoot, gitContext, targetRoot);
73
- const agentsMappings = mappings.filter(
74
- (m) => basename(m.targetPath) === "agents.md",
75
- );
76
- const promptsMappings = mappings.filter((m) =>
77
- m.targetPath.includes("/.claude/prompts/"),
78
- );
79
- const regularMappings = mappings.filter(
80
- (m) =>
81
- basename(m.targetPath) !== "agents.md" &&
82
- !m.targetPath.includes("/.claude/prompts/"),
83
- );
77
+ const ignorePatterns = config.ignore ?? [];
78
+ const { agentsMappings, promptsMappings, regularMappings } =
79
+ await collectMappings(overlayRoot, gitContext, targetRoot, ignorePatterns);
84
80
 
85
81
  // Create symlinks
86
82
  const linkedPaths = await createLinks(regularMappings, targetRoot);
@@ -91,26 +87,12 @@ export async function linkCommand(targetDir?: string): Promise<void> {
91
87
  await setupProjectSettings(overlayRoot, gitContext, targetRoot);
92
88
  await addGitExcludes(targetRoot);
93
89
  await maybeGenerateVscodeSettings(config, targetRoot);
94
- await warnOrphans(overlayRoot, targetRoot, gitContext.projectName);
95
- }
96
-
97
- /** Generate VS Code settings if configured */
98
- async function maybeGenerateVscodeSettings(
99
- config: ClankConfig,
100
- targetRoot: string,
101
- ): Promise<void> {
102
- const setting = config.vscodeSettings ?? "auto";
103
-
104
- if (setting === "never") return;
105
-
106
- if (setting === "auto") {
107
- const isVscode = await isVscodeProject(targetRoot);
108
- if (!isVscode) return;
109
- }
110
-
111
- // setting === "always" or (setting === "auto" && isVscodeProject)
112
- console.log("");
113
- await generateVscodeSettings(targetRoot);
90
+ await warnOrphans(
91
+ overlayRoot,
92
+ targetRoot,
93
+ gitContext.projectName,
94
+ ignorePatterns,
95
+ );
114
96
  }
115
97
 
116
98
  function logGitContext(ctx: GitContext): void {
@@ -119,27 +101,6 @@ function logGitContext(ctx: GitContext): void {
119
101
  console.log(`Branch: ${ctx.worktreeName}${suffix}`);
120
102
  }
121
103
 
122
- function logLinkedPaths(files: LinkedFile[]): void {
123
- if (files.length === 0) return;
124
- console.log(`\nLinked ${files.length} file(s):`);
125
- for (const { path, scope } of files) {
126
- const suffix = scope === "project" ? "" : ` (${scope})`;
127
- console.log(` ${path}${suffix}`);
128
- }
129
- }
130
-
131
- async function warnOrphans(
132
- overlayRoot: string,
133
- targetRoot: string,
134
- projectName: string,
135
- ): Promise<void> {
136
- const orphans = await findOrphans(overlayRoot, targetRoot, projectName);
137
- if (orphans.length > 0) {
138
- console.log(`\nWarning: ${orphans.length} orphaned overlay path(s) found.`);
139
- console.log("Run 'clank check' for details.");
140
- }
141
- }
142
-
143
104
  /** Check for problematic agent files and error if found */
144
105
  async function checkAgentFiles(
145
106
  targetRoot: string,
@@ -165,67 +126,64 @@ async function maybeInitWorktree(
165
126
  }
166
127
  }
167
128
 
168
- /** Collect all file mappings from global, project, and worktree locations */
169
- async function overlayMappings(
129
+ /** Collect and separate mappings by type (agents.md and prompts get special handling) */
130
+ async function collectMappings(
170
131
  overlayRoot: string,
171
132
  gitContext: GitContext,
172
133
  targetRoot: string,
173
- ): Promise<FileMapping[]> {
174
- const context: MapperContext = { overlayRoot, targetRoot, gitContext };
175
- const overlayGlobal = join(overlayRoot, "global");
176
- const overlayProject = overlayProjectDir(overlayRoot, gitContext.projectName);
177
-
178
- return [
179
- ...(await dirMappings(overlayGlobal, context)),
180
- ...(await dirMappings(overlayProject, context)),
181
- ];
182
- }
183
-
184
- async function dirMappings(
185
- dir: string,
186
- context: MapperContext,
187
- ): Promise<FileMapping[]> {
188
- if (!(await fileExists(dir))) return [];
134
+ ignorePatterns: string[],
135
+ ): Promise<SeparatedMappings> {
136
+ const mappings = await overlayMappings(
137
+ overlayRoot,
138
+ gitContext,
139
+ targetRoot,
140
+ ignorePatterns,
141
+ );
142
+ const isAgent = ({ targetPath }: FileMapping) =>
143
+ basename(targetPath) === "agents.md";
144
+ const isPrompt = ({ targetPath }: FileMapping) =>
145
+ targetPath.includes("/.claude/prompts/");
189
146
 
190
- const mappings: FileMapping[] = [];
191
- for await (const overlayPath of walkOverlayFiles(dir)) {
192
- const result = overlayToTarget(overlayPath, context);
193
- if (result) {
194
- mappings.push({ overlayPath, ...result });
195
- }
196
- }
197
- return mappings;
198
- }
147
+ const agentsMappings = mappings.filter(isAgent);
148
+ const promptsMappings = mappings.filter((m) => !isAgent(m) && isPrompt(m));
149
+ const regularMappings = mappings.filter((m) => !isAgent(m) && !isPrompt(m));
199
150
 
200
- /** Check if a file is tracked in git (exists as real file, not symlink, and tracked) */
201
- async function isTrackedFile(path: string, gitRoot: string): Promise<boolean> {
202
- if (!(await fileExists(path))) return false;
203
- if (await isSymlink(path)) return false;
204
- return isTrackedByGit(path, gitRoot);
151
+ return { agentsMappings, promptsMappings, regularMappings };
205
152
  }
206
153
 
207
- /** Process a single agents.md mapping into agent symlink paths */
208
- async function processAgentMapping(
209
- mapping: FileMapping,
154
+ /** Create symlinks, handling conflicts with scope suffixes.
155
+ * Conflicts occur when the same filename exists at multiple scopes (global, project, worktree).
156
+ * Returns linked files with their scopes. */
157
+ async function createLinks(
158
+ mappings: FileMapping[],
210
159
  targetRoot: string,
211
- agents: string[],
212
- ): Promise<{ created: string[]; skipped: string[] }> {
213
- const { overlayPath, targetPath } = mapping;
214
- const targetDir = dirname(targetPath);
215
- const created: string[] = [];
216
- const skipped: string[] = [];
160
+ ): Promise<LinkedFile[]> {
161
+ // Filter out subdirectory clank files where parent doesn't exist in target
162
+ const validMappings = await filterValidMappings(mappings, targetRoot);
217
163
 
218
- await forEachAgentPath(targetDir, agents, async (agentPath) => {
219
- if (await isTrackedFile(agentPath, targetRoot)) {
220
- skipped.push(relative(targetRoot, agentPath));
221
- } else {
222
- const linkTarget = getLinkTarget(agentPath, overlayPath);
223
- await createSymlink(linkTarget, agentPath);
224
- created.push(relative(targetRoot, agentPath));
225
- }
226
- });
164
+ const byTarget = Map.groupBy(validMappings, (m) => m.targetPath);
165
+ const links = [...byTarget].flatMap(([targetPath, files]) =>
166
+ resolveLinks(targetPath, files),
167
+ );
227
168
 
228
- return { created, skipped };
169
+ const linkPromises = links.map(({ overlayPath, linkPath }) =>
170
+ createSymlink(getLinkTarget(linkPath, overlayPath), linkPath),
171
+ );
172
+ await Promise.all(linkPromises);
173
+
174
+ return links.map(({ linkPath, scope }) => ({
175
+ path: relative(targetRoot, linkPath),
176
+ scope,
177
+ }));
178
+ }
179
+
180
+ function logLinkedPaths(files: LinkedFile[]): void {
181
+ if (files.length === 0) return;
182
+ console.log(`\nLinked ${files.length} file(s):`);
183
+ for (const { path, scope } of files) {
184
+ const suffix = scope === "project" ? "" : ` (${scope})`;
185
+ console.log(` ${path}${suffix}`);
186
+ }
229
187
  }
230
188
 
231
189
  /** Create agent symlinks (CLAUDE.md, GEMINI.md, AGENTS.md → agents.md) for all agents.md files */
@@ -278,47 +236,106 @@ async function createPromptLinks(
278
236
  }
279
237
  }
280
238
 
281
- /** Process a single prompt mapping into symlinks for all agent directories */
282
- async function processPromptMapping(
283
- mapping: FileMapping,
239
+ /** Setup project settings.json - adopt existing or create new */
240
+ async function setupProjectSettings(
241
+ overlayRoot: string,
242
+ gitContext: GitContext,
284
243
  targetRoot: string,
285
- ): Promise<{ created: string[] }> {
286
- const { overlayPath, targetPath } = mapping;
287
- const promptRelPath = getPromptRelPath(targetPath);
288
- if (!promptRelPath) return { created: [] };
289
-
290
- const createdPaths = await createPromptLinksShared(
291
- overlayPath,
292
- promptRelPath,
293
- targetRoot,
244
+ ): Promise<void> {
245
+ const overlayPath = join(
246
+ overlayRoot,
247
+ "targets",
248
+ gitContext.projectName,
249
+ "claude",
250
+ "settings.json",
294
251
  );
295
- return { created: createdPaths.map((p) => relative(targetRoot, p)) };
252
+ const targetPath = join(targetRoot, ".claude/settings.json");
253
+
254
+ const inOverlay = await fileExists(overlayPath);
255
+ const inTarget =
256
+ (await fileExists(targetPath)) && !(await isSymlink(targetPath));
257
+
258
+ if (inTarget && inOverlay) {
259
+ throw new Error(
260
+ `Conflict: settings.json exists in both target and overlay.\n` +
261
+ ` Target: ${targetPath}\n` +
262
+ ` Overlay: ${overlayPath}\n` +
263
+ `Remove one to resolve.`,
264
+ );
265
+ }
266
+
267
+ if (inTarget) {
268
+ // Adopt target's settings - move to overlay
269
+ await ensureDir(dirname(overlayPath));
270
+ await rename(targetPath, overlayPath);
271
+ console.log(`Moved .claude/settings.json to overlay`);
272
+ } else if (!inOverlay) {
273
+ // Neither exists - create blank in overlay
274
+ await ensureDir(dirname(overlayPath));
275
+ await writeFile(overlayPath, "{}\n", "utf-8");
276
+ console.log(
277
+ `Created .claude/settings.json symlink (project settings will be stored in overlay)`,
278
+ );
279
+ }
280
+
281
+ // Create symlink (overlay now has the file)
282
+ await ensureDir(dirname(targetPath));
283
+ const linkTarget = getLinkTarget(targetPath, overlayPath);
284
+ await createSymlink(linkTarget, targetPath);
296
285
  }
297
286
 
298
- /** Create symlinks, handling conflicts with scope suffixes.
299
- * Conflicts occur when the same filename exists at multiple scopes (global, project, worktree).
300
- * Returns linked files with their scopes. */
301
- async function createLinks(
302
- mappings: FileMapping[],
287
+ /** Generate VS Code settings if configured */
288
+ async function maybeGenerateVscodeSettings(
289
+ config: ClankConfig,
303
290
  targetRoot: string,
304
- ): Promise<LinkedFile[]> {
305
- // Filter out subdirectory clank files where parent doesn't exist in target
306
- const validMappings = await filterValidMappings(mappings, targetRoot);
291
+ ): Promise<void> {
292
+ const setting = config.vscodeSettings ?? "auto";
307
293
 
308
- const byTarget = Map.groupBy(validMappings, (m) => m.targetPath);
309
- const links = [...byTarget].flatMap(([targetPath, files]) =>
310
- resolveLinks(targetPath, files),
311
- );
294
+ if (setting === "never") return;
312
295
 
313
- const linkPromises = links.map(({ overlayPath, linkPath }) =>
314
- createSymlink(getLinkTarget(linkPath, overlayPath), linkPath),
296
+ if (setting === "auto") {
297
+ const isVscode = await isVscodeProject(targetRoot);
298
+ if (!isVscode) return;
299
+ }
300
+
301
+ // setting === "always" or (setting === "auto" && isVscodeProject)
302
+ console.log("");
303
+ await generateVscodeSettings(targetRoot);
304
+ }
305
+
306
+ async function warnOrphans(
307
+ overlayRoot: string,
308
+ targetRoot: string,
309
+ projectName: string,
310
+ ignorePatterns: string[] = [],
311
+ ): Promise<void> {
312
+ const orphans = await findOrphans(
313
+ overlayRoot,
314
+ targetRoot,
315
+ projectName,
316
+ ignorePatterns,
315
317
  );
316
- await Promise.all(linkPromises);
318
+ if (orphans.length > 0) {
319
+ console.log(`\nWarning: ${orphans.length} orphaned overlay path(s) found.`);
320
+ console.log("Run 'clank check' for details.");
321
+ }
322
+ }
317
323
 
318
- return links.map(({ linkPath, scope }) => ({
319
- path: relative(targetRoot, linkPath),
320
- scope,
321
- }));
324
+ /** Collect all file mappings from global, project, and worktree locations */
325
+ async function overlayMappings(
326
+ overlayRoot: string,
327
+ gitContext: GitContext,
328
+ targetRoot: string,
329
+ ignorePatterns: string[] = [],
330
+ ): Promise<FileMapping[]> {
331
+ const context: MapperContext = { overlayRoot, targetRoot, gitContext };
332
+ const overlayGlobal = join(overlayRoot, "global");
333
+ const overlayProject = overlayProjectDir(overlayRoot, gitContext.projectName);
334
+
335
+ return [
336
+ ...(await dirMappings(overlayGlobal, context, ignorePatterns)),
337
+ ...(await dirMappings(overlayProject, context, ignorePatterns)),
338
+ ];
322
339
  }
323
340
 
324
341
  /** Filter mappings to exclude subdirectory clank files where target parent doesn't exist.
@@ -334,24 +351,6 @@ async function filterValidMappings(
334
351
  return results.filter((m): m is FileMapping => m !== null);
335
352
  }
336
353
 
337
- /** Check if a subdirectory clank file's parent exists in the target */
338
- async function checkMappingParentExists(
339
- m: FileMapping,
340
- targetRoot: string,
341
- ): Promise<FileMapping | null> {
342
- const relPath = relative(targetRoot, m.targetPath);
343
- // Subdirectory clank files have /clank/ in the middle of the path
344
- const clankIndex = relPath.indexOf("/clank/");
345
- if (clankIndex !== -1) {
346
- // Check if parent directory exists (e.g., packages/foo for packages/foo/clank/notes.md)
347
- const parentDir = join(targetRoot, relPath.slice(0, clankIndex));
348
- if (!(await fileExists(parentDir))) {
349
- return null;
350
- }
351
- }
352
- return m;
353
- }
354
-
355
354
  /** Compute link paths, adding scope suffixes when the same target has multiple sources */
356
355
  function resolveLinks(
357
356
  targetPath: string,
@@ -371,50 +370,85 @@ function resolveLinks(
371
370
  }));
372
371
  }
373
372
 
374
- /** Setup project settings.json - adopt existing or create new */
375
- async function setupProjectSettings(
376
- overlayRoot: string,
377
- gitContext: GitContext,
373
+ /** Process a single agents.md mapping into agent symlink paths */
374
+ async function processAgentMapping(
375
+ mapping: FileMapping,
378
376
  targetRoot: string,
379
- ): Promise<void> {
380
- const overlayPath = join(
381
- overlayRoot,
382
- "targets",
383
- gitContext.projectName,
384
- "claude",
385
- "settings.json",
377
+ agents: string[],
378
+ ): Promise<{ created: string[]; skipped: string[] }> {
379
+ const { overlayPath, targetPath } = mapping;
380
+ const targetDir = dirname(targetPath);
381
+ const created: string[] = [];
382
+ const skipped: string[] = [];
383
+
384
+ await forEachAgentPath(targetDir, agents, async (agentPath) => {
385
+ if (await isTrackedFile(agentPath, targetRoot)) {
386
+ skipped.push(relative(targetRoot, agentPath));
387
+ } else {
388
+ const linkTarget = getLinkTarget(agentPath, overlayPath);
389
+ await createSymlink(linkTarget, agentPath);
390
+ created.push(relative(targetRoot, agentPath));
391
+ }
392
+ });
393
+
394
+ return { created, skipped };
395
+ }
396
+
397
+ /** Process a single prompt mapping into symlinks for all agent directories */
398
+ async function processPromptMapping(
399
+ mapping: FileMapping,
400
+ targetRoot: string,
401
+ ): Promise<{ created: string[] }> {
402
+ const { overlayPath, targetPath } = mapping;
403
+ const promptRelPath = getPromptRelPath(targetPath);
404
+ if (!promptRelPath) return { created: [] };
405
+
406
+ const createdPaths = await createPromptLinksShared(
407
+ overlayPath,
408
+ promptRelPath,
409
+ targetRoot,
386
410
  );
387
- const targetPath = join(targetRoot, ".claude/settings.json");
411
+ return { created: createdPaths.map((p) => relative(targetRoot, p)) };
412
+ }
388
413
 
389
- const inOverlay = await fileExists(overlayPath);
390
- const inTarget =
391
- (await fileExists(targetPath)) && !(await isSymlink(targetPath));
414
+ async function dirMappings(
415
+ dir: string,
416
+ context: MapperContext,
417
+ ignorePatterns: string[] = [],
418
+ ): Promise<FileMapping[]> {
419
+ if (!(await fileExists(dir))) return [];
392
420
 
393
- if (inTarget && inOverlay) {
394
- throw new Error(
395
- `Conflict: settings.json exists in both target and overlay.\n` +
396
- ` Target: ${targetPath}\n` +
397
- ` Overlay: ${overlayPath}\n` +
398
- `Remove one to resolve.`,
399
- );
421
+ const mappings: FileMapping[] = [];
422
+ for await (const overlayPath of walkOverlayFiles(dir, ignorePatterns)) {
423
+ const result = overlayToTarget(overlayPath, context);
424
+ if (result) {
425
+ mappings.push({ overlayPath, ...result });
426
+ }
400
427
  }
428
+ return mappings;
429
+ }
401
430
 
402
- if (inTarget) {
403
- // Adopt target's settings - move to overlay
404
- await ensureDir(dirname(overlayPath));
405
- await rename(targetPath, overlayPath);
406
- console.log(`Moved .claude/settings.json to overlay`);
407
- } else if (!inOverlay) {
408
- // Neither exists - create blank in overlay
409
- await ensureDir(dirname(overlayPath));
410
- await writeFile(overlayPath, "{}\n", "utf-8");
411
- console.log(
412
- `Created .claude/settings.json symlink (project settings will be stored in overlay)`,
413
- );
431
+ /** Check if a subdirectory clank file's parent exists in the target */
432
+ async function checkMappingParentExists(
433
+ m: FileMapping,
434
+ targetRoot: string,
435
+ ): Promise<FileMapping | null> {
436
+ const relPath = relative(targetRoot, m.targetPath);
437
+ // Subdirectory clank files have /clank/ in the middle of the path
438
+ const clankIndex = relPath.indexOf("/clank/");
439
+ if (clankIndex !== -1) {
440
+ // Check if parent directory exists (e.g., packages/foo for packages/foo/clank/notes.md)
441
+ const parentDir = join(targetRoot, relPath.slice(0, clankIndex));
442
+ if (!(await fileExists(parentDir))) {
443
+ return null;
444
+ }
414
445
  }
446
+ return m;
447
+ }
415
448
 
416
- // Create symlink (overlay now has the file)
417
- await ensureDir(dirname(targetPath));
418
- const linkTarget = getLinkTarget(targetPath, overlayPath);
419
- await createSymlink(linkTarget, targetPath);
449
+ /** Check if a file is tracked in git (exists as real file, not symlink, and tracked) */
450
+ async function isTrackedFile(path: string, gitRoot: string): Promise<boolean> {
451
+ if (!(await fileExists(path))) return false;
452
+ if (await isSymlink(path)) return false;
453
+ return isTrackedByGit(path, gitRoot);
420
454
  }
@@ -101,22 +101,6 @@ async function moveSingleFile(
101
101
  );
102
102
  }
103
103
 
104
- /** Recreate a regular symlink after moving */
105
- async function recreateSymlink(
106
- targetPath: string,
107
- overlayPath: string,
108
- ctx: MoveContext,
109
- ): Promise<void> {
110
- // Remove old symlink if it exists
111
- if (await isSymlinkToOverlay(targetPath, ctx.overlayRoot)) {
112
- await unlink(targetPath);
113
- }
114
-
115
- // Create new symlink
116
- const linkTarget = getLinkTarget(targetPath, overlayPath);
117
- await createSymlink(linkTarget, targetPath);
118
- }
119
-
120
104
  /** Recreate agent symlinks (CLAUDE.md, GEMINI.md, AGENTS.md) after moving */
121
105
  async function recreateAgentLinks(
122
106
  normalizedPath: string,
@@ -172,3 +156,19 @@ async function recreatePromptLinks(
172
156
  );
173
157
  }
174
158
  }
159
+
160
+ /** Recreate a regular symlink after moving */
161
+ async function recreateSymlink(
162
+ targetPath: string,
163
+ overlayPath: string,
164
+ ctx: MoveContext,
165
+ ): Promise<void> {
166
+ // Remove old symlink if it exists
167
+ if (await isSymlinkToOverlay(targetPath, ctx.overlayRoot)) {
168
+ await unlink(targetPath);
169
+ }
170
+
171
+ // Create new symlink
172
+ const linkTarget = getLinkTarget(targetPath, overlayPath);
173
+ await createSymlink(linkTarget, targetPath);
174
+ }
@@ -90,21 +90,30 @@ async function resolveScope(
90
90
  return found[0];
91
91
  }
92
92
 
93
- /** Search all scopes to find where the file exists */
94
- async function findInScopes(
93
+ /** Remove agent files (CLAUDE.md, GEMINI.md, AGENTS.md agents.md) */
94
+ async function removeAgentFiles(
95
95
  targetPath: string,
96
- context: MapperContext,
97
- ): Promise<Scope[]> {
98
- const found: Scope[] = [];
96
+ overlayPath: string,
97
+ overlayRoot: string,
98
+ config: { agents: string[] },
99
+ ): Promise<void> {
100
+ const dir = dirname(targetPath);
101
+ const removed: string[] = [];
99
102
 
100
- for (const scope of ["worktree", "project", "global"] as const) {
101
- const overlayPath = targetToOverlay(targetPath, scope, context);
102
- if (await fileExists(overlayPath)) {
103
- found.push(scope);
103
+ await forEachAgentPath(dir, config.agents, async (linkPath) => {
104
+ if (await isSymlinkToOverlay(linkPath, overlayRoot)) {
105
+ await unlink(linkPath);
106
+ removed.push(basename(linkPath));
104
107
  }
108
+ });
109
+
110
+ if (removed.length > 0) {
111
+ console.log(`Removed symlinks: ${removed.join(", ")}`);
105
112
  }
106
113
 
107
- return found;
114
+ // Remove agents.md from overlay
115
+ await rm(overlayPath);
116
+ console.log(`Removed from overlay: agents.md`);
108
117
  }
109
118
 
110
119
  /** Remove a regular file */
@@ -134,28 +143,19 @@ async function removeFile(
134
143
  console.log(`Removed from overlay: ${basename(overlayPath)}`);
135
144
  }
136
145
 
137
- /** Remove agent files (CLAUDE.md, GEMINI.md, AGENTS.md agents.md) */
138
- async function removeAgentFiles(
146
+ /** Search all scopes to find where the file exists */
147
+ async function findInScopes(
139
148
  targetPath: string,
140
- overlayPath: string,
141
- overlayRoot: string,
142
- config: { agents: string[] },
143
- ): Promise<void> {
144
- const dir = dirname(targetPath);
145
- const removed: string[] = [];
149
+ context: MapperContext,
150
+ ): Promise<Scope[]> {
151
+ const found: Scope[] = [];
146
152
 
147
- await forEachAgentPath(dir, config.agents, async (linkPath) => {
148
- if (await isSymlinkToOverlay(linkPath, overlayRoot)) {
149
- await unlink(linkPath);
150
- removed.push(basename(linkPath));
153
+ for (const scope of ["worktree", "project", "global"] as const) {
154
+ const overlayPath = targetToOverlay(targetPath, scope, context);
155
+ if (await fileExists(overlayPath)) {
156
+ found.push(scope);
151
157
  }
152
- });
153
-
154
- if (removed.length > 0) {
155
- console.log(`Removed symlinks: ${removed.join(", ")}`);
156
158
  }
157
159
 
158
- // Remove agents.md from overlay
159
- await rm(overlayPath);
160
- console.log(`Removed from overlay: agents.md`);
160
+ return found;
161
161
  }