clank-cli 0.1.67 → 0.1.74

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.
@@ -7,8 +7,15 @@ import {
7
7
  formatAgentFileProblems,
8
8
  } from "../ClassifyFiles.ts";
9
9
  import { expandPath, loadConfig } from "../Config.ts";
10
- import { fileExists, relativePath, walkDirectory } from "../FsUtil.ts";
10
+ import {
11
+ fileExists,
12
+ getCwd,
13
+ relativePath,
14
+ toSlash,
15
+ walkDirectory,
16
+ } from "../FsUtil.ts";
11
17
  import { type GitContext, getGitContext } from "../Git.ts";
18
+ import { loadGitignore } from "../Gitignore.ts";
12
19
  import { type MapperContext, overlayProjectDir } from "../Mapper.ts";
13
20
  import { formatStatusLines, getOverlayStatus } from "../OverlayGit.ts";
14
21
  import { type ManagedFileState, verifyManaged } from "../OverlayLinks.ts";
@@ -28,12 +35,19 @@ export type UnaddedFile = ManagedFileState & {
28
35
  relativePath: string;
29
36
  };
30
37
 
38
+ export interface CheckOptions {
39
+ prompt?: boolean;
40
+ }
41
+
31
42
  /** Files that should remain local and not be tracked by clank */
32
43
  const localOnlyFiles = ["settings.local.json"];
33
44
 
45
+ /** Switch to a one-line summary when a category has at least this many files */
46
+ const compactThreshold = 5;
47
+
34
48
  /** Check for orphaned overlay paths that don't match target structure */
35
- export async function checkCommand(): Promise<void> {
36
- const cwd = process.cwd();
49
+ export async function checkCommand(options: CheckOptions = {}): Promise<void> {
50
+ const cwd = await getCwd();
37
51
  const gitContext = await getGitContext(cwd);
38
52
  const config = await loadConfig();
39
53
  const overlayRoot = expandPath(config.overlayRepo);
@@ -43,7 +57,7 @@ export async function checkCommand(): Promise<void> {
43
57
 
44
58
  await showOverlayStatus(overlayRoot, ignorePatterns);
45
59
 
46
- const problems = await checkAllProblems(ctx, cwd, ignorePatterns);
60
+ const problems = await checkAllProblems(ctx, cwd, ignorePatterns, options);
47
61
  if (!problems) {
48
62
  console.log("No issues found. Overlay matches target structure.");
49
63
  }
@@ -53,15 +67,17 @@ export async function checkCommand(): Promise<void> {
53
67
  export async function findUnaddedFiles(
54
68
  context: MapperContext,
55
69
  ): Promise<UnaddedFile[]> {
56
- const { targetRoot } = context;
70
+ const { targetRoot, overlayRoot } = context;
57
71
  const unadded: UnaddedFile[] = [];
72
+ const isIgnored = await loadGitignore(overlayRoot);
58
73
 
59
74
  for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
60
75
  if (isDirectory) continue;
61
76
 
62
- const relPath = relative(targetRoot, path);
77
+ const relPath = toSlash(relative(targetRoot, path));
63
78
  if (!isInManagedDir(relPath)) continue;
64
79
  if (isLocalOnlyFile(relPath)) continue;
80
+ if (isIgnored(basename(relPath))) continue;
65
81
 
66
82
  const managed = await verifyManaged(path, context);
67
83
  if (managed.kind !== "valid") {
@@ -89,13 +105,8 @@ export async function findOrphans(
89
105
  const isIgnored =
90
106
  ignorePatterns.length > 0 ? picomatch(ignorePatterns) : null;
91
107
 
92
- const skip = (relPath: string): boolean => {
93
- if (isIgnored) {
94
- const pathBasename = relPath.split("/").at(-1) ?? "";
95
- if (isIgnored(relPath) || isIgnored(pathBasename)) return true;
96
- }
97
- return false;
98
- };
108
+ const skip = (relPath: string): boolean =>
109
+ !!isIgnored && (isIgnored(relPath) || isIgnored(basename(relPath)));
99
110
 
100
111
  for await (const { path, isDirectory } of walkDirectory(projectDir, {
101
112
  skipDirs: [".git", "node_modules", "worktrees"],
@@ -103,7 +114,7 @@ export async function findOrphans(
103
114
  })) {
104
115
  if (isDirectory) continue;
105
116
 
106
- const relPath = relative(projectDir, path);
117
+ const relPath = toSlash(relative(projectDir, path));
107
118
 
108
119
  // Skip files at project root (agents.md, settings.json)
109
120
  if (!relPath.includes("/")) continue;
@@ -113,7 +124,6 @@ export async function findOrphans(
113
124
  continue;
114
125
  }
115
126
 
116
- // This is a subdirectory file - check if target dir exists
117
127
  // e.g., tools/packages/wesl/clank/notes.md -> check tools/packages/wesl/
118
128
  const targetSubdir = extractTargetSubdir(relPath);
119
129
  if (!targetSubdir) continue;
@@ -164,6 +174,7 @@ async function checkAllProblems(
164
174
  ctx: MapperContext,
165
175
  cwd: string,
166
176
  ignorePatterns: string[] = [],
177
+ options: CheckOptions = {},
167
178
  ): Promise<boolean> {
168
179
  const { overlayRoot, targetRoot, gitContext } = ctx;
169
180
  let hasProblems = false;
@@ -193,7 +204,7 @@ async function checkAllProblems(
193
204
  );
194
205
  if (orphans.length > 0) {
195
206
  hasProblems = true;
196
- showOrphanedPaths(orphans, targetRoot, overlayRoot);
207
+ showOrphanedPaths(orphans, targetRoot, overlayRoot, options);
197
208
  }
198
209
 
199
210
  return hasProblems;
@@ -218,12 +229,10 @@ function isLocalOnlyFile(relPath: string): boolean {
218
229
  * @example "tools/packages/wesl/agents.md" -> "tools/packages/wesl"
219
230
  */
220
231
  function extractTargetSubdir(relPath: string): string | null {
221
- // Check for /clank/ or /claude/ in path
222
232
  for (const dir of managedDirs) {
223
233
  const idx = relPath.indexOf(`/${dir}/`);
224
234
  if (idx !== -1) return relPath.slice(0, idx);
225
235
  }
226
- // Check for agents.md in a subdirectory
227
236
  if (relPath.endsWith("/agents.md")) {
228
237
  return relPath.slice(0, -"/agents.md".length);
229
238
  }
@@ -241,46 +250,27 @@ function showUnaddedFiles(
241
250
  ? `${projectName}/${worktreeName}`
242
251
  : projectName;
243
252
 
244
- const outsideOverlay = unadded.filter((f) => f.kind === "outside-overlay");
245
- const wrongMapping = unadded.filter((f) => f.kind === "wrong-mapping");
253
+ const outsideOverlay = unadded.filter(
254
+ (
255
+ f,
256
+ ): f is UnaddedFile & { kind: "outside-overlay"; currentTarget: string } =>
257
+ f.kind === "outside-overlay",
258
+ );
259
+ const wrongMapping = unadded.filter(
260
+ (
261
+ f,
262
+ ): f is UnaddedFile & {
263
+ kind: "wrong-mapping";
264
+ currentTarget: string;
265
+ expectedTarget: string;
266
+ } => f.kind === "wrong-mapping",
267
+ );
246
268
  const regularFiles = unadded.filter((f) => f.kind === "unadded");
247
269
 
248
- if (outsideOverlay.length > 0) {
249
- console.log(
250
- `Found ${outsideOverlay.length} stale symlink(s) in ${targetName}:\n`,
251
- );
252
- console.log("These symlinks point outside the clank overlay.");
253
- console.log("Remove them manually, then run `clank link` to recreate:\n");
254
- for (const file of outsideOverlay) {
255
- console.log(` rm ${relativePath(cwd, file.targetPath)}`);
256
- }
257
- console.log();
258
- }
259
-
260
- if (wrongMapping.length > 0) {
261
- console.log(
262
- `Found ${wrongMapping.length} mislinked symlink(s) in ${targetName}:\n`,
263
- );
264
- console.log("These symlinks point to the wrong overlay location.");
265
- console.log("Run `clank link` to fix them.\n");
266
- for (const file of wrongMapping) {
267
- console.log(` ${relativePath(cwd, file.targetPath)}`);
268
- if (file.currentTarget) {
269
- console.log(` points to: ${file.currentTarget}`);
270
- }
271
- }
272
- console.log();
273
- }
274
-
275
- if (regularFiles.length > 0) {
276
- console.log(
277
- `Found ${regularFiles.length} unadded file(s) in ${targetName}:\n`,
278
- );
279
- for (const file of regularFiles) {
280
- console.log(` clank add ${relativePath(cwd, file.targetPath)}`);
281
- }
282
- console.log();
283
- }
270
+ if (outsideOverlay.length > 0)
271
+ showOutsideOverlay(outsideOverlay, cwd, targetName);
272
+ if (wrongMapping.length > 0) showWrongMapping(wrongMapping, cwd, targetName);
273
+ if (regularFiles.length > 0) showUnadded(regularFiles, cwd, targetName);
284
274
  }
285
275
 
286
276
  /** Display orphaned paths and remediation prompt */
@@ -288,12 +278,26 @@ function showOrphanedPaths(
288
278
  orphans: OrphanedPath[],
289
279
  targetRoot: string,
290
280
  overlayRoot: string,
281
+ options: CheckOptions = {},
291
282
  ): void {
292
- console.log(`Found ${orphans.length} orphaned overlay path(s):\n`);
293
- for (const orphan of orphans) {
294
- console.log(` ${orphan.fileName} (${orphan.scope})`);
295
- console.log(` Overlay: ${orphan.overlayPath}`);
296
- console.log(` Expected dir: ${orphan.expectedTargetDir}\n`);
283
+ const compact = orphans.length >= compactThreshold;
284
+
285
+ if (compact) {
286
+ const names = orphans.map((o) => `${o.expectedTargetDir}/${o.fileName}`);
287
+ console.log(`Found ${orphans.length} orphaned overlay path(s):`);
288
+ console.log(` ${formatInlineList(names)}\n`);
289
+ } else {
290
+ console.log(`Found ${orphans.length} orphaned overlay path(s):\n`);
291
+ for (const orphan of orphans) {
292
+ console.log(` ${orphan.fileName} (${orphan.scope})`);
293
+ console.log(` Overlay: ${orphan.overlayPath}`);
294
+ console.log(` Expected dir: ${orphan.expectedTargetDir}\n`);
295
+ }
296
+ }
297
+
298
+ if (compact && !options.prompt) {
299
+ console.log("Run `clank status --prompt` for an agent fix prompt.");
300
+ return;
297
301
  }
298
302
 
299
303
  console.log("Target project:", targetRoot);
@@ -304,6 +308,85 @@ function showOrphanedPaths(
304
308
  console.log("─".repeat(50));
305
309
  }
306
310
 
311
+ function showOutsideOverlay(
312
+ items: (UnaddedFile & { currentTarget: string })[],
313
+ cwd: string,
314
+ targetName: string,
315
+ ): void {
316
+ console.log(`Found ${items.length} stale symlink(s) in ${targetName}:\n`);
317
+ console.log("These symlinks point outside the clank overlay.");
318
+ console.log("Remove them manually, then run `clank link` to recreate:\n");
319
+ for (const file of items) {
320
+ console.log(` rm ${relativePath(cwd, file.targetPath)}`);
321
+ }
322
+ console.log();
323
+ }
324
+
325
+ function showWrongMapping(
326
+ items: (UnaddedFile & { currentTarget: string })[],
327
+ cwd: string,
328
+ targetName: string,
329
+ ): void {
330
+ if (items.length >= compactThreshold) {
331
+ const names = items.map((f) => relativePath(cwd, f.targetPath));
332
+ console.log(
333
+ `Found ${items.length} mislinked symlink(s) in ${targetName} — run \`clank link\` to fix:`,
334
+ );
335
+ console.log(` ${formatInlineList(names)}\n`);
336
+ return;
337
+ }
338
+ console.log(`Found ${items.length} mislinked symlink(s) in ${targetName}:\n`);
339
+ console.log("These symlinks point to the wrong overlay location.");
340
+ console.log("Run `clank link` to fix them.\n");
341
+ for (const file of items) {
342
+ console.log(` ${relativePath(cwd, file.targetPath)}`);
343
+ if (file.currentTarget) {
344
+ console.log(` points to: ${file.currentTarget}`);
345
+ }
346
+ }
347
+ console.log();
348
+ }
349
+
350
+ function showUnadded(
351
+ items: UnaddedFile[],
352
+ cwd: string,
353
+ targetName: string,
354
+ ): void {
355
+ if (items.length >= compactThreshold) {
356
+ const names = items.map((f) => relativePath(cwd, f.targetPath));
357
+ console.log(
358
+ `Found ${items.length} unadded file(s) in ${targetName} — run \`clank add\` to add:`,
359
+ );
360
+ console.log(` ${formatInlineList(names)}\n`);
361
+ return;
362
+ }
363
+ console.log(`Found ${items.length} unadded file(s) in ${targetName}:\n`);
364
+ for (const file of items) {
365
+ console.log(` ${relativePath(cwd, file.targetPath)}`);
366
+ }
367
+ console.log();
368
+ console.log(" clank add # add interactively");
369
+ console.log(" clank add <file> [<file>...] # add specific files");
370
+ console.log();
371
+ }
372
+
373
+ /** Join names with spaces up to maxChars, then append "(+N more)" if truncated */
374
+ function formatInlineList(names: string[], maxChars = 80): string {
375
+ const parts: string[] = [];
376
+ let used = 0;
377
+ for (let i = 0; i < names.length; i++) {
378
+ const next = names[i];
379
+ const sep = parts.length === 0 ? 0 : 1;
380
+ if (parts.length > 0 && used + sep + next.length > maxChars) {
381
+ parts.push(`(+${names.length - i} more)`);
382
+ break;
383
+ }
384
+ parts.push(next);
385
+ used += sep + next.length;
386
+ }
387
+ return parts.join(" ");
388
+ }
389
+
307
390
  /** Generate agent prompt for fixing orphaned paths */
308
391
  function generateAgentPrompt(
309
392
  orphans: OrphanedPath[],
@@ -33,6 +33,7 @@ export async function initCommand(overlayPath?: string): Promise<void> {
33
33
  await ensureDir(join(targetPath, "targets"));
34
34
 
35
35
  await createDefaultTemplates(targetPath);
36
+ await writeFile(join(targetPath, ".gitignore"), ".DS_Store\n", "utf-8");
36
37
 
37
38
  await createDefaultConfig(targetPath);
38
39
 
@@ -12,14 +12,17 @@ import {
12
12
  loadConfig,
13
13
  validateOverlayExists,
14
14
  } from "../Config.ts";
15
+ import { consolidateRulesIntoAgentFiles } from "../Consolidate.ts";
15
16
  import { addGitExcludes } from "../Exclude.ts";
16
17
  import {
17
18
  createSymlink,
18
19
  ensureDir,
19
20
  fileExists,
21
+ getCwd,
20
22
  getLinkTarget,
21
23
  isSymlink,
22
- isTrackedByGit,
24
+ isTrackedRealFile,
25
+ toSlash,
23
26
  } from "../FsUtil.ts";
24
27
  import { type GitContext, getGitContext } from "../Git.ts";
25
28
  import {
@@ -64,7 +67,7 @@ interface SeparatedMappings {
64
67
 
65
68
  /** Link overlay repository to target directory */
66
69
  export async function linkCommand(targetDir?: string): Promise<void> {
67
- const gitContext = await getGitContext(targetDir || process.cwd());
70
+ const gitContext = await getGitContext(targetDir || (await getCwd()));
68
71
  const targetRoot = gitContext.gitRoot;
69
72
  console.log(`Linking clank overlay to: ${targetRoot}\n`);
70
73
  logGitContext(gitContext);
@@ -73,21 +76,7 @@ export async function linkCommand(targetDir?: string): Promise<void> {
73
76
  const overlayRoot = expandPath(config.overlayRepo);
74
77
  await validateOverlayExists(overlayRoot);
75
78
 
76
- // Clean up symlinks pointing to wrong worktree before linking
77
- const staleRemoved = await cleanStaleWorktreeSymlinks(
78
- targetRoot,
79
- overlayRoot,
80
- gitContext,
81
- );
82
- if (staleRemoved.length > 0) {
83
- console.log(`\nCleaned ${staleRemoved.length} stale worktree symlink(s):`);
84
- for (const path of staleRemoved) {
85
- console.log(` ${path}`);
86
- }
87
- }
88
-
89
- // Check for problematic agent files before proceeding
90
- await checkAgentFiles(targetRoot, overlayRoot);
79
+ await cleanStaleAndCheck(targetRoot, overlayRoot, gitContext);
91
80
 
92
81
  await ensureDir(join(overlayRoot, "targets", gitContext.projectName));
93
82
  await maybeInitWorktree(overlayRoot, gitContext);
@@ -102,6 +91,8 @@ export async function linkCommand(targetDir?: string): Promise<void> {
102
91
  await createAgentLinks(agentsMappings, targetRoot, config.agents);
103
92
  await createPromptLinks(promptsMappings, targetRoot);
104
93
 
94
+ await maybeConsolidateRules(overlayRoot, targetRoot, gitContext, config);
95
+
105
96
  await setupProjectSettings(overlayRoot, gitContext, targetRoot);
106
97
  await addGitExcludes(targetRoot);
107
98
  await maybeGenerateVscodeSettings(config, targetRoot);
@@ -119,15 +110,27 @@ function logGitContext(ctx: GitContext): void {
119
110
  console.log(`Branch: ${ctx.worktreeName}${suffix}`);
120
111
  }
121
112
 
122
- /** Check for problematic agent files and error if found */
123
- async function checkAgentFiles(
113
+ /** Clean stale worktree symlinks and check for problematic agent files */
114
+ async function cleanStaleAndCheck(
124
115
  targetRoot: string,
125
116
  overlayRoot: string,
117
+ gitContext: GitContext,
126
118
  ): Promise<void> {
127
- const classification = await classifyAgentFiles(targetRoot, overlayRoot);
119
+ const staleRemoved = await cleanStaleWorktreeSymlinks(
120
+ targetRoot,
121
+ overlayRoot,
122
+ gitContext,
123
+ );
124
+ if (staleRemoved.length > 0) {
125
+ console.log(`\nCleaned ${staleRemoved.length} stale worktree symlink(s):`);
126
+ for (const path of staleRemoved) {
127
+ console.log(` ${path}`);
128
+ }
129
+ }
128
130
 
131
+ const classification = await classifyAgentFiles(targetRoot, overlayRoot);
129
132
  if (agentFileProblems(classification)) {
130
- throw new Error(formatAgentFileProblems(classification, process.cwd()));
133
+ throw new Error(formatAgentFileProblems(classification, await getCwd()));
131
134
  }
132
135
  }
133
136
 
@@ -160,7 +163,7 @@ async function collectMappings(
160
163
  const isAgent = ({ targetPath }: FileMapping) =>
161
164
  basename(targetPath) === "agents.md";
162
165
  const isPrompt = ({ targetPath }: FileMapping) =>
163
- targetPath.includes("/.claude/prompts/");
166
+ toSlash(targetPath).includes("/.claude/prompts/");
164
167
 
165
168
  const agentsMappings = mappings.filter(isAgent);
166
169
  const promptsMappings = mappings.filter((m) => !isAgent(m) && isPrompt(m));
@@ -234,7 +237,7 @@ async function createAgentLinks(
234
237
  }
235
238
  }
236
239
 
237
- /** Create prompt symlinks in all agent directories (.claude/prompts/, .gemini/prompts/) */
240
+ /** Create prompt symlinks in all agent directories (.claude/prompts/, .gemini/prompts/, .codex/prompts/) */
238
241
  async function createPromptLinks(
239
242
  promptsMappings: FileMapping[],
240
243
  targetRoot: string,
@@ -254,6 +257,27 @@ async function createPromptLinks(
254
257
  }
255
258
  }
256
259
 
260
+ /** Consolidate rules into generated AGENTS.md/GEMINI.md if rules exist */
261
+ async function maybeConsolidateRules(
262
+ overlayRoot: string,
263
+ targetRoot: string,
264
+ gitContext: GitContext,
265
+ config: ClankConfig,
266
+ ): Promise<void> {
267
+ const consolidated = await consolidateRulesIntoAgentFiles({
268
+ overlayRoot,
269
+ targetRoot,
270
+ gitContext,
271
+ agents: config.agents,
272
+ });
273
+ if (consolidated.length > 0) {
274
+ console.log(`\nGenerated consolidated agent files:`);
275
+ for (const name of consolidated) {
276
+ console.log(` ${name}`);
277
+ }
278
+ }
279
+ }
280
+
257
281
  /** Setup project settings.json - adopt existing or create new */
258
282
  async function setupProjectSettings(
259
283
  overlayRoot: string,
@@ -408,7 +432,7 @@ async function processAgentMapping(
408
432
  const skipped: string[] = [];
409
433
 
410
434
  await forEachAgentPath(targetDir, agents, async (agentPath) => {
411
- if (await isTrackedFile(agentPath, targetRoot)) {
435
+ if (await isTrackedRealFile(agentPath, targetRoot)) {
412
436
  skipped.push(relative(targetRoot, agentPath));
413
437
  } else {
414
438
  const linkTarget = getLinkTarget(agentPath, overlayPath);
@@ -459,7 +483,7 @@ async function checkMappingParentExists(
459
483
  m: FileMapping,
460
484
  targetRoot: string,
461
485
  ): Promise<FileMapping | null> {
462
- const relPath = relative(targetRoot, m.targetPath);
486
+ const relPath = toSlash(relative(targetRoot, m.targetPath));
463
487
  // Subdirectory clank files have /clank/ in the middle of the path
464
488
  const clankIndex = relPath.indexOf("/clank/");
465
489
  if (clankIndex !== -1) {
@@ -471,10 +495,3 @@ async function checkMappingParentExists(
471
495
  }
472
496
  return m;
473
497
  }
474
-
475
- /** Check if a file is tracked in git (exists as real file, not symlink, and tracked) */
476
- async function isTrackedFile(path: string, gitRoot: string): Promise<boolean> {
477
- if (!(await fileExists(path))) return false;
478
- if (await isSymlink(path)) return false;
479
- return isTrackedByGit(path, gitRoot);
480
- }
@@ -6,6 +6,7 @@ import {
6
6
  createSymlink,
7
7
  ensureDir,
8
8
  fileExists,
9
+ getCwd,
9
10
  getLinkTarget,
10
11
  } from "../FsUtil.ts";
11
12
  import { getGitContext } from "../Git.ts";
@@ -37,7 +38,7 @@ export async function moveCommand(
37
38
  options: MoveOptions,
38
39
  ): Promise<void> {
39
40
  const hasScope = options.global || options.project || options.worktree;
40
- const cwd = process.cwd();
41
+ const cwd = await getCwd();
41
42
  const gitContext = await getGitContext(cwd);
42
43
  const config = await loadConfig();
43
44
  const overlayRoot = expandPath(config.overlayRepo);
@@ -2,7 +2,7 @@ import { rm, unlink } from "node:fs/promises";
2
2
  import { basename, dirname, relative } from "node:path";
3
3
  import { forEachAgentPath } from "../AgentFiles.ts";
4
4
  import { expandPath, loadConfig, validateOverlayExists } from "../Config.ts";
5
- import { fileExists } from "../FsUtil.ts";
5
+ import { fileExists, getCwd } from "../FsUtil.ts";
6
6
  import { getGitContext } from "../Git.ts";
7
7
  import {
8
8
  isAgentFile,
@@ -23,7 +23,7 @@ export async function rmCommand(
23
23
  filePaths: string[],
24
24
  options: RmOptions = {},
25
25
  ): Promise<void> {
26
- const cwd = process.cwd();
26
+ const cwd = await getCwd();
27
27
  const gitContext = await getGitContext(cwd);
28
28
  const config = await loadConfig();
29
29
  const overlayRoot = expandPath(config.overlayRepo);
@@ -1,13 +1,14 @@
1
1
  import { expandPath, loadConfig } from "../Config.ts";
2
+ import { removeGeneratedAgentFiles } from "../Consolidate.ts";
2
3
  import { removeGitExcludes } from "../Exclude.ts";
3
- import { fileExists, removeSymlink, walkDirectory } from "../FsUtil.ts";
4
+ import { fileExists, getCwd, removeSymlink, walkDirectory } from "../FsUtil.ts";
4
5
  import { getGitContext } from "../Git.ts";
5
6
  import { isSymlinkToOverlay } from "../OverlayLinks.ts";
6
7
  import { removeVscodeSettings } from "./VsCode.ts";
7
8
 
8
9
  /** Remove all symlinks pointing to overlay repository */
9
10
  export async function unlinkCommand(targetDir?: string): Promise<void> {
10
- const gitContext = await getGitContext(targetDir || process.cwd());
11
+ const gitContext = await getGitContext(targetDir || (await getCwd()));
11
12
  const targetRoot = gitContext.gitRoot;
12
13
 
13
14
  console.log(`Removing clank symlinks from: ${targetRoot}\n`);
@@ -34,6 +35,10 @@ export async function unlinkCommand(targetDir?: string): Promise<void> {
34
35
  }
35
36
  }
36
37
 
38
+ // Remove generated agent files (from rules consolidation)
39
+ const generatedRemoved = await removeGeneratedAgentFiles(targetRoot);
40
+ removedCount += generatedRemoved.length;
41
+
37
42
  await removeGitExcludes(targetRoot);
38
43
  await removeVscodeSettings(targetRoot);
39
44
 
@@ -5,6 +5,7 @@ import { addToGitExclude } from "../Exclude.ts";
5
5
  import {
6
6
  ensureDir,
7
7
  fileExists,
8
+ getCwd,
8
9
  isTrackedByGit,
9
10
  writeJsonFile,
10
11
  } from "../FsUtil.ts";
@@ -109,7 +110,7 @@ export async function checkVscodeTracking(
109
110
 
110
111
  /** Generate VS Code settings to show clank files in search and explorer */
111
112
  export async function vscodeCommand(options?: VscodeOptions): Promise<void> {
112
- const targetRoot = await detectGitRoot(process.cwd());
113
+ const targetRoot = await detectGitRoot(await getCwd());
113
114
 
114
115
  if (options?.remove) {
115
116
  await removeVscodeSettings(targetRoot);
@@ -1,4 +1,4 @@
1
- import { dirname } from "node:path";
1
+ import { basename, dirname } from "node:path";
2
2
  import { agentFiles } from "../../AgentFiles.ts";
3
3
  import { getPromptRelPath } from "../../Mapper.ts";
4
4
  import { partition } from "../../Util.ts";
@@ -15,9 +15,9 @@ export function dedupeEntries(
15
15
 
16
16
  /** Check if a relative path ends with an agent filename (CLAUDE.md, etc.) */
17
17
  export function isAgentFilePath(relPath: string): boolean {
18
- const base = relPath.split("/").at(-1)?.toLowerCase();
19
- if (!base) return false;
20
- return agentFiles.some((f) => f.toLowerCase() === base);
18
+ return agentFiles.some(
19
+ (f) => f.toLowerCase() === basename(relPath).toLowerCase(),
20
+ );
21
21
  }
22
22
 
23
23
  /** Keep at most one agent file per directory, using the configured preference order. */
@@ -129,6 +129,5 @@ function mapPreference(
129
129
  }
130
130
 
131
131
  function basenameUpper(relPath: string): string {
132
- const base = relPath.split("/").at(-1) ?? "";
133
- return base.toUpperCase();
132
+ return basename(relPath).toUpperCase();
134
133
  }