clank-cli 0.1.62 → 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 +26 -2
- package/package.json +3 -2
- package/src/AgentFiles.ts +1 -2
- package/src/ClassifyFiles.ts +15 -20
- package/src/Cli.ts +3 -2
- package/src/Gitignore.ts +13 -24
- package/src/OverlayGit.ts +3 -9
- package/src/OverlayLinks.ts +6 -11
- package/src/commands/Add.ts +261 -29
- package/src/commands/Move.ts +135 -5
- package/src/commands/Rm.ts +31 -21
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)
|
|
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
|
-
**
|
|
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
|
|
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.
|
|
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
|
|
36
|
-
const agentPath = agentPaths[key];
|
|
35
|
+
const agentPath = agentPaths[agent.toLowerCase()];
|
|
37
36
|
if (!agentPath) continue;
|
|
38
37
|
await fn(agentPath, agent);
|
|
39
38
|
}
|
package/src/ClassifyFiles.ts
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
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\`:
|
|
@@ -192,15 +190,12 @@ async function classifyAgentSymlink(
|
|
|
192
190
|
const agentsMdPath = join(dirname(filePath), "agents.md");
|
|
193
191
|
const expectedTarget = targetToOverlay(agentsMdPath, "project", mapperCtx);
|
|
194
192
|
if (absoluteTarget !== expectedTarget) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
currentTarget: absoluteTarget,
|
|
200
|
-
expectedTarget,
|
|
201
|
-
},
|
|
202
|
-
],
|
|
193
|
+
const symlink = {
|
|
194
|
+
symlinkPath: filePath,
|
|
195
|
+
currentTarget: absoluteTarget,
|
|
196
|
+
expectedTarget,
|
|
203
197
|
};
|
|
198
|
+
return { outdatedSymlinks: [symlink] };
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
|
package/src/Cli.ts
CHANGED
|
@@ -133,9 +133,10 @@ function registerOverlayCommands(program: Command): void {
|
|
|
133
133
|
.command("add")
|
|
134
134
|
.description("Add file(s) to overlay and create symlinks")
|
|
135
135
|
.argument(
|
|
136
|
-
"
|
|
136
|
+
"[files...]",
|
|
137
137
|
"File path(s) (e.g., style.md, .claude/commands/review.md)",
|
|
138
138
|
)
|
|
139
|
+
.option("-i, --interactive", "Interactively add all unadded files")
|
|
139
140
|
.option("-g, --global", "Add to global location (all projects)")
|
|
140
141
|
.option("-p, --project", "Add to project location (default)")
|
|
141
142
|
.option("-w, --worktree", "Add to worktree location (this branch only)")
|
|
@@ -202,7 +203,7 @@ function registerMvCommand(program: Command): void {
|
|
|
202
203
|
const cmd = program
|
|
203
204
|
.command("mv")
|
|
204
205
|
.alias("move")
|
|
205
|
-
.description("Move file(s)
|
|
206
|
+
.description("Move or rename file(s) in overlay")
|
|
206
207
|
.argument("<files...>", "File(s) to move");
|
|
207
208
|
|
|
208
209
|
cmd.addOption(
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
}
|
package/src/OverlayGit.ts
CHANGED
|
@@ -54,11 +54,7 @@ function filterIgnoredLines(
|
|
|
54
54
|
if (isIgnored(filePath) || isIgnored(pathBasename)) return false;
|
|
55
55
|
|
|
56
56
|
// Check each directory segment (for patterns like ".obsidian")
|
|
57
|
-
|
|
58
|
-
if (isIgnored(segment)) return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return true;
|
|
57
|
+
return !segments.some((segment) => isIgnored(segment));
|
|
62
58
|
});
|
|
63
59
|
}
|
|
64
60
|
|
|
@@ -81,10 +77,8 @@ function parseScopedPath(filePath: string): {
|
|
|
81
77
|
Math.max(0, afterWorktrees.length - 1),
|
|
82
78
|
);
|
|
83
79
|
const branch = afterWorktrees.slice(0, branchSegments).join("/");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
pathParts: afterWorktrees.slice(branchSegments),
|
|
87
|
-
};
|
|
80
|
+
const scope = `${project}/${branch}`;
|
|
81
|
+
return { scope, pathParts: afterWorktrees.slice(branchSegments) };
|
|
88
82
|
}
|
|
89
83
|
return { scope: project || "unknown", pathParts: segments.slice(2) };
|
|
90
84
|
}
|
package/src/OverlayLinks.ts
CHANGED
|
@@ -101,14 +101,10 @@ export async function* walkOverlayFiles(
|
|
|
101
101
|
ignorePatterns.length > 0 ? picomatch(ignorePatterns) : null;
|
|
102
102
|
|
|
103
103
|
const skip = (relPath: string): boolean => {
|
|
104
|
-
// Skip templates
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const basename = relPath.split("/").at(-1) ?? "";
|
|
109
|
-
if (isIgnored(relPath) || isIgnored(basename)) return true;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
104
|
+
if (relPath.startsWith("clank/init/")) return true; // Skip templates
|
|
105
|
+
if (!isIgnored) return false;
|
|
106
|
+
const basename = relPath.split("/").at(-1) ?? "";
|
|
107
|
+
return isIgnored(relPath) || isIgnored(basename);
|
|
112
108
|
};
|
|
113
109
|
|
|
114
110
|
const genEntries = walkDirectory(overlayRoot, { skip });
|
|
@@ -140,7 +136,6 @@ function isMatchingPromptPath(
|
|
|
140
136
|
canonicalPath: string,
|
|
141
137
|
actualPath: string,
|
|
142
138
|
): boolean {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
return canonicalPrompt !== null && canonicalPrompt === actualPrompt;
|
|
139
|
+
const canonical = getPromptRelPath(canonicalPath);
|
|
140
|
+
return canonical !== null && canonical === getPromptRelPath(actualPath);
|
|
146
141
|
}
|
package/src/commands/Add.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
writeFile,
|
|
7
7
|
} from "node:fs/promises";
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
|
+
import * as readline from "node:readline";
|
|
9
10
|
import { forEachAgentPath } from "../AgentFiles.ts";
|
|
10
11
|
import { expandPath, loadConfig, validateOverlayExists } from "../Config.ts";
|
|
11
12
|
import {
|
|
@@ -31,8 +32,12 @@ import {
|
|
|
31
32
|
} from "../Mapper.ts";
|
|
32
33
|
import { createPromptLinks, isSymlinkToOverlay } from "../OverlayLinks.ts";
|
|
33
34
|
import { scopeFromSymlink } from "../ScopeFromSymlink.ts";
|
|
35
|
+
import { findUnaddedFiles } from "./Check.ts";
|
|
34
36
|
|
|
35
|
-
export type AddOptions = ScopeOptions
|
|
37
|
+
export type AddOptions = ScopeOptions & {
|
|
38
|
+
interactive?: boolean;
|
|
39
|
+
quiet?: boolean;
|
|
40
|
+
};
|
|
36
41
|
|
|
37
42
|
interface AddContext {
|
|
38
43
|
cwd: string;
|
|
@@ -48,6 +53,7 @@ interface AgentLinkParams {
|
|
|
48
53
|
gitRoot: string;
|
|
49
54
|
overlayRoot: string;
|
|
50
55
|
agents: string[];
|
|
56
|
+
quiet?: boolean;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
interface AgentLinkClassification {
|
|
@@ -56,6 +62,14 @@ interface AgentLinkClassification {
|
|
|
56
62
|
skipped: string[];
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
type InteractiveChoice = "project" | "worktree" | "global" | "skip" | "quit";
|
|
66
|
+
type ScopeCounts = {
|
|
67
|
+
project: number;
|
|
68
|
+
worktree: number;
|
|
69
|
+
global: number;
|
|
70
|
+
skip: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
59
73
|
const scopeLabels: Record<Scope, string> = {
|
|
60
74
|
global: "global",
|
|
61
75
|
project: "project",
|
|
@@ -76,6 +90,17 @@ export async function addCommand(
|
|
|
76
90
|
|
|
77
91
|
const ctx = { cwd, gitContext, config, overlayRoot };
|
|
78
92
|
|
|
93
|
+
if (options.interactive) {
|
|
94
|
+
await addAllInteractive(ctx);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (filePaths.length === 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"No files specified. Use --interactive for interactive mode.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
for (const filePath of filePaths) {
|
|
80
105
|
const inputPath = join(cwd, filePath);
|
|
81
106
|
|
|
@@ -107,6 +132,38 @@ async function validateAddOptions(
|
|
|
107
132
|
}
|
|
108
133
|
}
|
|
109
134
|
|
|
135
|
+
/** Interactive mode: add all unadded files with per-file scope selection */
|
|
136
|
+
async function addAllInteractive(ctx: AddContext): Promise<void> {
|
|
137
|
+
const { cwd, gitContext, overlayRoot } = ctx;
|
|
138
|
+
const context: MapperContext = {
|
|
139
|
+
overlayRoot,
|
|
140
|
+
targetRoot: gitContext.gitRoot,
|
|
141
|
+
gitContext,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const unadded = await findUnaddedFiles(context);
|
|
145
|
+
const regularFiles = unadded.filter((f) => f.kind === "unadded");
|
|
146
|
+
|
|
147
|
+
if (regularFiles.length === 0) {
|
|
148
|
+
console.log("No unadded files found.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`Found ${regularFiles.length} unadded file(s):\n`);
|
|
153
|
+
|
|
154
|
+
const counts: ScopeCounts = { project: 0, worktree: 0, global: 0, skip: 0 };
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < regularFiles.length; i++) {
|
|
157
|
+
const file = regularFiles[i];
|
|
158
|
+
const relPath = relative(cwd, file.targetPath);
|
|
159
|
+
const result = await promptAndAddFile(relPath, i, regularFiles.length, ctx);
|
|
160
|
+
if (result === "quit") break;
|
|
161
|
+
if (result !== "error") counts[result]++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
printSummary(counts);
|
|
165
|
+
}
|
|
166
|
+
|
|
110
167
|
async function isDirectory(path: string): Promise<boolean> {
|
|
111
168
|
try {
|
|
112
169
|
return (await lstat(path)).isDirectory();
|
|
@@ -123,6 +180,7 @@ async function addSingleFile(
|
|
|
123
180
|
): Promise<void> {
|
|
124
181
|
const { cwd, gitContext, config, overlayRoot } = ctx;
|
|
125
182
|
const { gitRoot } = gitContext;
|
|
183
|
+
const { quiet } = options;
|
|
126
184
|
|
|
127
185
|
const scope = resolveScopeFromOptions(options);
|
|
128
186
|
/** Absolute path where symlink will be created in target repo */
|
|
@@ -145,23 +203,127 @@ async function addSingleFile(
|
|
|
145
203
|
await checkScopeConflict(barePath, scope, context, cwd);
|
|
146
204
|
|
|
147
205
|
if (await fileExists(overlayPath)) {
|
|
148
|
-
|
|
206
|
+
if (!quiet)
|
|
207
|
+
console.log(`${fileName} already exists in ${scopeLabel} overlay`);
|
|
149
208
|
} else if (await isSymlink(barePath)) {
|
|
150
|
-
await addSymlinkToOverlay(barePath, overlayPath, scopeLabel);
|
|
209
|
+
await addSymlinkToOverlay(barePath, overlayPath, scopeLabel, quiet);
|
|
151
210
|
} else {
|
|
152
|
-
await addFileToOverlay(
|
|
211
|
+
await addFileToOverlay(
|
|
212
|
+
normalizedPath,
|
|
213
|
+
barePath,
|
|
214
|
+
overlayPath,
|
|
215
|
+
scopeLabel,
|
|
216
|
+
quiet,
|
|
217
|
+
);
|
|
153
218
|
}
|
|
154
219
|
|
|
155
|
-
|
|
220
|
+
await createSymlinksForFile({
|
|
221
|
+
filePath,
|
|
222
|
+
normalizedPath,
|
|
223
|
+
overlayPath,
|
|
224
|
+
config,
|
|
225
|
+
gitRoot,
|
|
226
|
+
overlayRoot,
|
|
227
|
+
cwd,
|
|
228
|
+
quiet,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface CreateSymlinksParams {
|
|
233
|
+
filePath: string;
|
|
234
|
+
normalizedPath: string;
|
|
235
|
+
overlayPath: string;
|
|
236
|
+
config: { agents: string[] };
|
|
237
|
+
gitRoot: string;
|
|
238
|
+
overlayRoot: string;
|
|
239
|
+
cwd: string;
|
|
240
|
+
quiet?: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Create symlinks based on file type (agent, prompt, or regular) */
|
|
244
|
+
async function createSymlinksForFile(params: CreateSymlinksParams) {
|
|
245
|
+
const {
|
|
246
|
+
filePath,
|
|
247
|
+
normalizedPath,
|
|
248
|
+
overlayPath,
|
|
249
|
+
config,
|
|
250
|
+
gitRoot,
|
|
251
|
+
overlayRoot,
|
|
252
|
+
cwd,
|
|
253
|
+
quiet,
|
|
254
|
+
} = params;
|
|
255
|
+
|
|
156
256
|
if (isAgentFile(filePath)) {
|
|
157
257
|
const symlinkDir = dirname(normalizedPath);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
258
|
+
await createAgentLinks({
|
|
259
|
+
overlayPath,
|
|
260
|
+
symlinkDir,
|
|
261
|
+
gitRoot,
|
|
262
|
+
overlayRoot,
|
|
263
|
+
agents: config.agents,
|
|
264
|
+
quiet,
|
|
265
|
+
});
|
|
161
266
|
} else if (isPromptFile(normalizedPath)) {
|
|
162
|
-
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd);
|
|
267
|
+
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd, quiet);
|
|
163
268
|
} else {
|
|
164
|
-
await handleRegularFile(
|
|
269
|
+
await handleRegularFile(
|
|
270
|
+
normalizedPath,
|
|
271
|
+
overlayPath,
|
|
272
|
+
overlayRoot,
|
|
273
|
+
cwd,
|
|
274
|
+
quiet,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Prompt for scope and add a single file. Returns the choice or "error". */
|
|
280
|
+
async function promptAndAddFile(
|
|
281
|
+
relPath: string,
|
|
282
|
+
index: number,
|
|
283
|
+
total: number,
|
|
284
|
+
ctx: AddContext,
|
|
285
|
+
): Promise<InteractiveChoice | "error"> {
|
|
286
|
+
process.stdout.write(`\x1b[1m${relPath}\x1b[0m [${index + 1}/${total}]\n`);
|
|
287
|
+
process.stdout.write(
|
|
288
|
+
" (P)roject (W)orktree (G)lobal (S)kip (Q)uit [P]: ",
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const choice = await readScopeChoice(ctx.gitContext.isWorktree);
|
|
292
|
+
|
|
293
|
+
if (choice === "quit" || choice === "skip") {
|
|
294
|
+
if (choice === "quit") console.log("\nAborted.");
|
|
295
|
+
else console.log();
|
|
296
|
+
return choice;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const scopeOptions: AddOptions = {
|
|
300
|
+
project: choice === "project",
|
|
301
|
+
worktree: choice === "worktree",
|
|
302
|
+
global: choice === "global",
|
|
303
|
+
quiet: true,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await addSingleFile(relPath, scopeOptions, ctx);
|
|
308
|
+
console.log();
|
|
309
|
+
return choice;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(` Error: ${error instanceof Error ? error.message : error}`);
|
|
312
|
+
console.log();
|
|
313
|
+
return "error";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Print summary of interactive add results */
|
|
318
|
+
function printSummary(counts: Record<string, number>): void {
|
|
319
|
+
const parts: string[] = [];
|
|
320
|
+
if (counts.project > 0) parts.push(`${counts.project} to project`);
|
|
321
|
+
if (counts.worktree > 0) parts.push(`${counts.worktree} to worktree`);
|
|
322
|
+
if (counts.global > 0) parts.push(`${counts.global} to global`);
|
|
323
|
+
if (counts.skip > 0) parts.push(`${counts.skip} skipped`);
|
|
324
|
+
|
|
325
|
+
if (parts.length > 0) {
|
|
326
|
+
console.log(`Added ${parts.join(", ")}`);
|
|
165
327
|
}
|
|
166
328
|
}
|
|
167
329
|
|
|
@@ -187,11 +349,15 @@ async function addSymlinkToOverlay(
|
|
|
187
349
|
inputPath: string,
|
|
188
350
|
overlayPath: string,
|
|
189
351
|
scopeLabel: string,
|
|
352
|
+
quiet?: boolean,
|
|
190
353
|
): Promise<void> {
|
|
191
354
|
const target = await readlink(inputPath);
|
|
192
355
|
await ensureDir(dirname(overlayPath));
|
|
193
356
|
await symlink(target, overlayPath);
|
|
194
|
-
|
|
357
|
+
if (!quiet)
|
|
358
|
+
console.log(
|
|
359
|
+
`Copied symlink ${basename(inputPath)} to ${scopeLabel} overlay`,
|
|
360
|
+
);
|
|
195
361
|
}
|
|
196
362
|
|
|
197
363
|
/** Copy file content to overlay */
|
|
@@ -200,20 +366,23 @@ async function addFileToOverlay(
|
|
|
200
366
|
barePath: string,
|
|
201
367
|
overlayPath: string,
|
|
202
368
|
scopeLabel: string,
|
|
369
|
+
quiet?: boolean,
|
|
203
370
|
): Promise<void> {
|
|
204
371
|
await ensureDir(dirname(overlayPath));
|
|
205
372
|
const content = await findSourceContent(normalizedPath, barePath);
|
|
206
373
|
await writeFile(overlayPath, content, "utf-8");
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
374
|
+
if (!quiet) {
|
|
375
|
+
const fileName = basename(overlayPath);
|
|
376
|
+
if (content) {
|
|
377
|
+
console.log(`Copied ${fileName} to ${scopeLabel} overlay`);
|
|
378
|
+
} else {
|
|
379
|
+
console.log(`Created empty ${fileName} in ${scopeLabel} overlay`);
|
|
380
|
+
}
|
|
212
381
|
}
|
|
213
382
|
}
|
|
214
383
|
|
|
215
384
|
async function createAgentLinks(p: AgentLinkParams): Promise<void> {
|
|
216
|
-
const { overlayPath, ...classifyParams } = p;
|
|
385
|
+
const { overlayPath, quiet, ...classifyParams } = p;
|
|
217
386
|
const { toCreate, existing, skipped } =
|
|
218
387
|
await classifyAgentLinks(classifyParams);
|
|
219
388
|
|
|
@@ -223,17 +392,19 @@ async function createAgentLinks(p: AgentLinkParams): Promise<void> {
|
|
|
223
392
|
});
|
|
224
393
|
await Promise.all(promisedLinks);
|
|
225
394
|
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
395
|
+
if (!quiet) {
|
|
396
|
+
if (toCreate.length) {
|
|
397
|
+
const created = toCreate.map(({ name }) => name);
|
|
398
|
+
console.log(`Created symlinks: ${created.join(", ")}`);
|
|
399
|
+
}
|
|
230
400
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
401
|
+
if (existing.length) {
|
|
402
|
+
console.log(`Symlinks already exist: ${existing.join(", ")}`);
|
|
403
|
+
}
|
|
234
404
|
|
|
235
|
-
|
|
236
|
-
|
|
405
|
+
if (skipped.length) {
|
|
406
|
+
console.log(`Skipped (already tracked in git): ${skipped.join(", ")}`);
|
|
407
|
+
}
|
|
237
408
|
}
|
|
238
409
|
}
|
|
239
410
|
|
|
@@ -243,6 +414,7 @@ async function handlePromptFile(
|
|
|
243
414
|
overlayPath: string,
|
|
244
415
|
gitRoot: string,
|
|
245
416
|
cwd: string,
|
|
417
|
+
quiet?: boolean,
|
|
246
418
|
): Promise<void> {
|
|
247
419
|
const promptRelPath = getPromptRelPath(normalizedPath);
|
|
248
420
|
if (promptRelPath) {
|
|
@@ -251,7 +423,7 @@ async function handlePromptFile(
|
|
|
251
423
|
promptRelPath,
|
|
252
424
|
gitRoot,
|
|
253
425
|
);
|
|
254
|
-
if (created.length) {
|
|
426
|
+
if (!quiet && created.length) {
|
|
255
427
|
console.log(
|
|
256
428
|
`Created symlinks: ${created.map((p) => relative(cwd, p)).join(", ")}`,
|
|
257
429
|
);
|
|
@@ -265,13 +437,50 @@ async function handleRegularFile(
|
|
|
265
437
|
overlayPath: string,
|
|
266
438
|
overlayRoot: string,
|
|
267
439
|
cwd: string,
|
|
440
|
+
quiet?: boolean,
|
|
268
441
|
): Promise<void> {
|
|
269
442
|
if (await isSymlinkToOverlay(normalizedPath, overlayRoot)) {
|
|
270
|
-
|
|
443
|
+
if (!quiet)
|
|
444
|
+
console.log(`Symlink already exists: ${relative(cwd, normalizedPath)}`);
|
|
271
445
|
} else {
|
|
272
446
|
const linkTarget = getLinkTarget(normalizedPath, overlayPath);
|
|
273
447
|
await createSymlink(linkTarget, normalizedPath);
|
|
274
|
-
|
|
448
|
+
if (!quiet)
|
|
449
|
+
console.log(`Created symlink: ${relative(cwd, normalizedPath)}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Read a single keypress for scope selection */
|
|
454
|
+
async function readScopeChoice(
|
|
455
|
+
isWorktree: boolean,
|
|
456
|
+
): Promise<InteractiveChoice> {
|
|
457
|
+
const key = await readSingleKey();
|
|
458
|
+
|
|
459
|
+
switch (key.toLowerCase()) {
|
|
460
|
+
case "p":
|
|
461
|
+
case "\r":
|
|
462
|
+
case "\n":
|
|
463
|
+
console.log("project");
|
|
464
|
+
return "project";
|
|
465
|
+
case "w":
|
|
466
|
+
if (!isWorktree) {
|
|
467
|
+
console.log("(not in worktree, using project)");
|
|
468
|
+
return "project";
|
|
469
|
+
}
|
|
470
|
+
console.log("worktree");
|
|
471
|
+
return "worktree";
|
|
472
|
+
case "g":
|
|
473
|
+
console.log("global");
|
|
474
|
+
return "global";
|
|
475
|
+
case "s":
|
|
476
|
+
console.log("skip");
|
|
477
|
+
return "skip";
|
|
478
|
+
case "q":
|
|
479
|
+
case "\x03": // Ctrl+C
|
|
480
|
+
return "quit";
|
|
481
|
+
default:
|
|
482
|
+
console.log("project");
|
|
483
|
+
return "project";
|
|
275
484
|
}
|
|
276
485
|
}
|
|
277
486
|
|
|
@@ -324,3 +533,26 @@ async function classifyAgentLinks(
|
|
|
324
533
|
|
|
325
534
|
return { toCreate, existing, skipped };
|
|
326
535
|
}
|
|
536
|
+
|
|
537
|
+
/** Read a single keypress from stdin (raw mode) */
|
|
538
|
+
function readSingleKey(): Promise<string> {
|
|
539
|
+
return new Promise((resolve) => {
|
|
540
|
+
const wasRaw = process.stdin.isRaw;
|
|
541
|
+
if (process.stdin.isTTY) {
|
|
542
|
+
readline.emitKeypressEvents(process.stdin);
|
|
543
|
+
process.stdin.setRawMode(true);
|
|
544
|
+
}
|
|
545
|
+
process.stdin.resume();
|
|
546
|
+
|
|
547
|
+
const onKeypress = (data: Buffer): void => {
|
|
548
|
+
if (process.stdin.isTTY) {
|
|
549
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
550
|
+
}
|
|
551
|
+
process.stdin.pause();
|
|
552
|
+
process.stdin.removeListener("data", onKeypress);
|
|
553
|
+
resolve(data.toString());
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
process.stdin.once("data", onKeypress);
|
|
557
|
+
});
|
|
558
|
+
}
|
package/src/commands/Move.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { rename, unlink } from "node:fs/promises";
|
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { forEachAgentPath, managedAgentDirs } from "../AgentFiles.ts";
|
|
4
4
|
import { expandPath, loadConfig, validateOverlayExists } from "../Config.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createSymlink,
|
|
7
|
+
ensureDir,
|
|
8
|
+
fileExists,
|
|
9
|
+
getLinkTarget,
|
|
10
|
+
} from "../FsUtil.ts";
|
|
6
11
|
import { getGitContext } from "../Git.ts";
|
|
7
12
|
import {
|
|
8
13
|
getPromptRelPath,
|
|
@@ -26,12 +31,12 @@ interface MoveContext {
|
|
|
26
31
|
agents: string[];
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/** Move file(s)
|
|
34
|
+
/** Move or rename file(s) in overlay */
|
|
30
35
|
export async function moveCommand(
|
|
31
36
|
filePaths: string[],
|
|
32
37
|
options: MoveOptions,
|
|
33
38
|
): Promise<void> {
|
|
34
|
-
const
|
|
39
|
+
const hasScope = options.global || options.project || options.worktree;
|
|
35
40
|
const cwd = process.cwd();
|
|
36
41
|
const gitContext = await getGitContext(cwd);
|
|
37
42
|
const config = await loadConfig();
|
|
@@ -42,8 +47,23 @@ export async function moveCommand(
|
|
|
42
47
|
const { gitRoot: targetRoot } = gitContext;
|
|
43
48
|
const context: MapperContext = { overlayRoot, targetRoot, gitContext };
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
if (hasScope) {
|
|
51
|
+
// Scope-move mode (existing behavior)
|
|
52
|
+
const targetScope = resolveScopeFromOptions(options, "require");
|
|
53
|
+
for (const filePath of filePaths) {
|
|
54
|
+
await moveSingleFile(filePath, targetScope, context, cwd, config);
|
|
55
|
+
}
|
|
56
|
+
} else if (filePaths.length === 2) {
|
|
57
|
+
// Rename mode
|
|
58
|
+
await renameFile(filePaths[0], filePaths[1], context, cwd, config);
|
|
59
|
+
} else if (filePaths.length === 1) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Must specify destination or scope flag (--global, --project, --worktree)",
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Too many arguments without scope flag. Use --global, --project, or --worktree",
|
|
66
|
+
);
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
69
|
|
|
@@ -101,6 +121,64 @@ async function moveSingleFile(
|
|
|
101
121
|
);
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
/** Rename a file within its current scope */
|
|
125
|
+
async function renameFile(
|
|
126
|
+
sourcePath: string,
|
|
127
|
+
destName: string,
|
|
128
|
+
context: MapperContext,
|
|
129
|
+
cwd: string,
|
|
130
|
+
config: { agents: string[] },
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const { overlayRoot, targetRoot: gitRoot } = context;
|
|
133
|
+
const sourceBarePath = join(cwd, sourcePath);
|
|
134
|
+
|
|
135
|
+
// Verify source is managed by clank
|
|
136
|
+
const currentScope = await scopeFromSymlink(sourceBarePath, context);
|
|
137
|
+
if (!currentScope) {
|
|
138
|
+
throw new Error(`${sourcePath} is not managed by clank`);
|
|
139
|
+
}
|
|
140
|
+
if (isAgentFile(sourcePath)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Cannot rename agent files (CLAUDE.md, AGENTS.md, GEMINI.md)",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build dest path (same directory as source)
|
|
147
|
+
const sourceDir = dirname(sourcePath);
|
|
148
|
+
const destPath = sourceDir === "." ? destName : join(sourceDir, destName);
|
|
149
|
+
|
|
150
|
+
// Calculate overlay paths
|
|
151
|
+
const normalizedSource = normalizeAddPath(sourcePath, cwd, gitRoot);
|
|
152
|
+
const normalizedDest = normalizeAddPath(destPath, cwd, gitRoot);
|
|
153
|
+
const sourceOverlay = targetToOverlay(
|
|
154
|
+
normalizedSource,
|
|
155
|
+
currentScope,
|
|
156
|
+
context,
|
|
157
|
+
);
|
|
158
|
+
const destOverlay = targetToOverlay(normalizedDest, currentScope, context);
|
|
159
|
+
|
|
160
|
+
if (await fileExists(destOverlay)) {
|
|
161
|
+
throw new Error(`Destination already exists: ${destName}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Rename in overlay and update symlinks
|
|
165
|
+
await ensureDir(dirname(destOverlay));
|
|
166
|
+
await rename(sourceOverlay, destOverlay);
|
|
167
|
+
const moveCtx: MoveContext = { overlayRoot, gitRoot, agents: config.agents };
|
|
168
|
+
if (isPromptFile(normalizedSource)) {
|
|
169
|
+
await renamePromptLinks(
|
|
170
|
+
normalizedSource,
|
|
171
|
+
normalizedDest,
|
|
172
|
+
destOverlay,
|
|
173
|
+
moveCtx,
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
await renameSymlink(normalizedSource, normalizedDest, destOverlay, moveCtx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`Renamed ${sourcePath} → ${destName} (${currentScope} scope)`);
|
|
180
|
+
}
|
|
181
|
+
|
|
104
182
|
/** Recreate agent symlinks (CLAUDE.md, GEMINI.md, AGENTS.md) after moving */
|
|
105
183
|
async function recreateAgentLinks(
|
|
106
184
|
normalizedPath: string,
|
|
@@ -172,3 +250,55 @@ async function recreateSymlink(
|
|
|
172
250
|
const linkTarget = getLinkTarget(targetPath, overlayPath);
|
|
173
251
|
await createSymlink(linkTarget, targetPath);
|
|
174
252
|
}
|
|
253
|
+
|
|
254
|
+
/** Rename prompt symlinks in all agent directories */
|
|
255
|
+
async function renamePromptLinks(
|
|
256
|
+
oldNormPath: string,
|
|
257
|
+
newNormPath: string,
|
|
258
|
+
newOverlayPath: string,
|
|
259
|
+
ctx: MoveContext,
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const { overlayRoot, gitRoot } = ctx;
|
|
262
|
+
const oldPromptRel = getPromptRelPath(oldNormPath);
|
|
263
|
+
const newPromptRel = getPromptRelPath(newNormPath);
|
|
264
|
+
if (!oldPromptRel || !newPromptRel) return;
|
|
265
|
+
|
|
266
|
+
// Remove old symlinks
|
|
267
|
+
for (const agentDir of managedAgentDirs) {
|
|
268
|
+
const oldTarget = join(gitRoot, agentDir, "prompts", oldPromptRel);
|
|
269
|
+
if (await isSymlinkToOverlay(oldTarget, overlayRoot)) {
|
|
270
|
+
await unlink(oldTarget);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Create new symlinks
|
|
275
|
+
const created = await createPromptLinks(
|
|
276
|
+
newOverlayPath,
|
|
277
|
+
newPromptRel,
|
|
278
|
+
gitRoot,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (created.length > 0) {
|
|
282
|
+
console.log(
|
|
283
|
+
`Updated symlinks: ${created.map((p) => relative(gitRoot, p)).join(", ")}`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Rename a regular symlink */
|
|
289
|
+
async function renameSymlink(
|
|
290
|
+
oldTargetPath: string,
|
|
291
|
+
newTargetPath: string,
|
|
292
|
+
newOverlayPath: string,
|
|
293
|
+
ctx: MoveContext,
|
|
294
|
+
): Promise<void> {
|
|
295
|
+
// Remove old symlink
|
|
296
|
+
if (await isSymlinkToOverlay(oldTargetPath, ctx.overlayRoot)) {
|
|
297
|
+
await unlink(oldTargetPath);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Create new symlink
|
|
301
|
+
await ensureDir(dirname(newTargetPath));
|
|
302
|
+
const linkTarget = getLinkTarget(newTargetPath, newOverlayPath);
|
|
303
|
+
await createSymlink(linkTarget, newTargetPath);
|
|
304
|
+
}
|
package/src/commands/Rm.ts
CHANGED
|
@@ -41,14 +41,18 @@ export async function rmCommand(
|
|
|
41
41
|
for (const filePath of filePaths) {
|
|
42
42
|
const normalizedPath = normalizeAddPath(filePath, cwd, gitRoot);
|
|
43
43
|
|
|
44
|
+
// Try to find overlay path (may not exist for unmanaged files)
|
|
44
45
|
const scope = await resolveScope(normalizedPath, options, context);
|
|
45
|
-
const overlayPath =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error(`Not found in overlay: ${relative(cwd, normalizedPath)}`);
|
|
49
|
-
}
|
|
46
|
+
const overlayPath = scope
|
|
47
|
+
? targetToOverlay(normalizedPath, scope, context)
|
|
48
|
+
: null;
|
|
50
49
|
|
|
51
50
|
if (isAgentFile(filePath)) {
|
|
51
|
+
if (!overlayPath || !(await fileExists(overlayPath))) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Not found in overlay: ${relative(cwd, normalizedPath)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
52
56
|
await removeAgentFiles(normalizedPath, overlayPath, overlayRoot, config);
|
|
53
57
|
} else {
|
|
54
58
|
await removeFile(normalizedPath, overlayPath, overlayRoot, cwd);
|
|
@@ -56,12 +60,12 @@ export async function rmCommand(
|
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
/** Resolve which scope to remove from */
|
|
63
|
+
/** Resolve which scope to remove from, or null if not in overlay */
|
|
60
64
|
async function resolveScope(
|
|
61
65
|
targetPath: string,
|
|
62
66
|
options: RmOptions,
|
|
63
67
|
context: MapperContext,
|
|
64
|
-
): Promise<Scope> {
|
|
68
|
+
): Promise<Scope | null> {
|
|
65
69
|
// Explicit scope takes priority
|
|
66
70
|
if (options.global || options.project || options.worktree) {
|
|
67
71
|
return resolveScopeFromOptions(options);
|
|
@@ -73,10 +77,8 @@ async function resolveScope(
|
|
|
73
77
|
const found = await findInScopes(targetPath, context);
|
|
74
78
|
|
|
75
79
|
if (found.length === 0) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`File does not exist in any scope (global, project, worktree)`,
|
|
79
|
-
);
|
|
80
|
+
// Not in overlay - might be an unmanaged local file
|
|
81
|
+
return null;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
if (found.length > 1) {
|
|
@@ -119,28 +121,36 @@ async function removeAgentFiles(
|
|
|
119
121
|
/** Remove a regular file */
|
|
120
122
|
async function removeFile(
|
|
121
123
|
targetPath: string,
|
|
122
|
-
overlayPath: string,
|
|
124
|
+
overlayPath: string | null,
|
|
123
125
|
overlayRoot: string,
|
|
124
126
|
cwd: string,
|
|
125
127
|
): Promise<void> {
|
|
126
128
|
const fileName = relative(cwd, targetPath);
|
|
129
|
+
const targetExists = await fileExists(targetPath);
|
|
130
|
+
const overlayExists = overlayPath ? await fileExists(overlayPath) : false;
|
|
127
131
|
|
|
128
|
-
//
|
|
129
|
-
if (
|
|
132
|
+
// Nothing to remove
|
|
133
|
+
if (!targetExists && !overlayExists) {
|
|
134
|
+
throw new Error(`File not found: ${fileName}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle target file
|
|
138
|
+
if (targetExists) {
|
|
130
139
|
if (await isSymlinkToOverlay(targetPath, overlayRoot)) {
|
|
131
140
|
await unlink(targetPath);
|
|
132
141
|
console.log(`Removed symlink: ${fileName}`);
|
|
133
142
|
} else {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
143
|
+
// Unmanaged file in clank/ directory - just remove it
|
|
144
|
+
await rm(targetPath);
|
|
145
|
+
console.log(`Removed: ${fileName}`);
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
// Remove from overlay
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
// Remove from overlay if it exists
|
|
150
|
+
if (overlayPath && overlayExists) {
|
|
151
|
+
await rm(overlayPath);
|
|
152
|
+
console.log(`Removed from overlay: ${basename(overlayPath)}`);
|
|
153
|
+
}
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/** Search all scopes to find where the file exists */
|