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 +1 -1
- package/src/OverlayLinks.ts +46 -3
- package/src/commands/Check.ts +4 -5
- package/src/commands/Link.ts +14 -0
package/package.json
CHANGED
package/src/OverlayLinks.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/Check.ts
CHANGED
|
@@ -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("
|
|
265
|
+
console.log("Run `clank link` to fix them.\n");
|
|
266
266
|
for (const file of wrongMapping) {
|
|
267
|
-
console.log(`
|
|
268
|
-
if (file.currentTarget
|
|
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();
|
package/src/commands/Link.ts
CHANGED
|
@@ -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
|
|