clank-cli 0.1.66 → 0.1.67

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/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.66",
4
+ "version": "0.1.67",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -1,14 +1,16 @@
1
- import { lstat } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { lstat, unlink } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
3
  import picomatch from "picomatch";
4
- import { managedAgentDirs } from "./AgentFiles.ts";
4
+ import { managedAgentDirs, targetManagedDirs } from "./AgentFiles.ts";
5
5
  import {
6
6
  createSymlink,
7
7
  ensureDir,
8
8
  getLinkTarget,
9
+ isSymlink,
9
10
  resolveSymlinkTarget,
10
11
  walkDirectory,
11
12
  } from "./FsUtil.ts";
13
+ import type { GitContext } from "./Git.ts";
12
14
  import {
13
15
  getPromptRelPath,
14
16
  type MapperContext,
@@ -139,3 +141,44 @@ function isMatchingPromptPath(
139
141
  const canonical = getPromptRelPath(canonicalPath);
140
142
  return canonical !== null && canonical === getPromptRelPath(actualPath);
141
143
  }
144
+
145
+ /** Find and remove symlinks pointing to wrong worktree in the overlay.
146
+ * Returns paths that were removed. */
147
+ export async function cleanStaleWorktreeSymlinks(
148
+ targetRoot: string,
149
+ overlayRoot: string,
150
+ gitContext: GitContext,
151
+ ): Promise<string[]> {
152
+ const removed: string[] = [];
153
+ const currentWorktree = gitContext.worktreeName;
154
+ const projectName = gitContext.projectName;
155
+ const worktreesPrefix = `${overlayRoot}/targets/${projectName}/worktrees/`;
156
+
157
+ for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
158
+ if (isDirectory) continue;
159
+
160
+ const relPath = relative(targetRoot, path);
161
+ if (!isInManagedDir(relPath)) continue;
162
+ if (!(await isSymlink(path))) continue;
163
+
164
+ const target = await resolveSymlinkTarget(path);
165
+ if (!target.startsWith(worktreesPrefix)) continue;
166
+
167
+ // Extract worktree name from path like .../worktrees/main/clank/notes.md
168
+ const afterPrefix = target.slice(worktreesPrefix.length);
169
+ const worktreeName = afterPrefix.split("/")[0];
170
+
171
+ if (worktreeName && worktreeName !== currentWorktree) {
172
+ await unlink(path);
173
+ removed.push(relPath);
174
+ }
175
+ }
176
+
177
+ return removed;
178
+ }
179
+
180
+ /** Check if a path is inside a clank-managed directory */
181
+ function isInManagedDir(relPath: string): boolean {
182
+ const parts = relPath.split("/");
183
+ return parts.some((part) => targetManagedDirs.includes(part));
184
+ }
@@ -250,7 +250,7 @@ function showUnaddedFiles(
250
250
  `Found ${outsideOverlay.length} stale symlink(s) in ${targetName}:\n`,
251
251
  );
252
252
  console.log("These symlinks point outside the clank overlay.");
253
- console.log("Remove them, then run `clank link` to recreate:\n");
253
+ console.log("Remove them manually, then run `clank link` to recreate:\n");
254
254
  for (const file of outsideOverlay) {
255
255
  console.log(` rm ${relativePath(cwd, file.targetPath)}`);
256
256
  }
@@ -262,12 +262,11 @@ function showUnaddedFiles(
262
262
  `Found ${wrongMapping.length} mislinked symlink(s) in ${targetName}:\n`,
263
263
  );
264
264
  console.log("These symlinks point to the wrong overlay location.");
265
- console.log("Remove them, then run `clank link` to recreate:\n");
265
+ console.log("Run `clank link` to fix them.\n");
266
266
  for (const file of wrongMapping) {
267
- console.log(` rm ${relativePath(cwd, file.targetPath)}`);
268
- if (file.currentTarget && file.expectedTarget) {
267
+ console.log(` ${relativePath(cwd, file.targetPath)}`);
268
+ if (file.currentTarget) {
269
269
  console.log(` points to: ${file.currentTarget}`);
270
- console.log(` expected: ${file.expectedTarget}`);
271
270
  }
272
271
  }
273
272
  console.log();
@@ -32,6 +32,7 @@ import {
32
32
  type TargetMapping,
33
33
  } from "../Mapper.ts";
34
34
  import {
35
+ cleanStaleWorktreeSymlinks,
35
36
  createPromptLinks as createPromptLinksShared,
36
37
  walkOverlayFiles,
37
38
  } from "../OverlayLinks.ts";
@@ -72,6 +73,19 @@ export async function linkCommand(targetDir?: string): Promise<void> {
72
73
  const overlayRoot = expandPath(config.overlayRepo);
73
74
  await validateOverlayExists(overlayRoot);
74
75
 
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
+
75
89
  // Check for problematic agent files before proceeding
76
90
  await checkAgentFiles(targetRoot, overlayRoot);
77
91