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.
- package/README.md +30 -4
- package/package.json +5 -3
- package/src/ClassifyFiles.ts +29 -29
- package/src/Cli.ts +78 -32
- package/src/Config.ts +14 -15
- package/src/Exclude.ts +9 -9
- package/src/FsUtil.ts +81 -19
- package/src/Git.ts +10 -10
- package/src/Gitignore.ts +25 -25
- package/src/Mapper.ts +77 -72
- package/src/OverlayGit.ts +30 -3
- package/src/OverlayLinks.ts +28 -17
- package/src/Util.ts +10 -0
- package/src/commands/Add.ts +107 -107
- package/src/commands/Check.ts +159 -139
- package/src/commands/Commit.ts +1 -1
- package/src/commands/Files.ts +38 -0
- package/src/commands/Link.ts +227 -193
- package/src/commands/Move.ts +16 -16
- package/src/commands/Rm.ts +29 -29
- package/src/commands/VsCode.ts +24 -24
- package/src/commands/files/Dedupe.ts +134 -0
- package/src/commands/files/Scan.ts +278 -0
package/src/commands/Link.ts
CHANGED
|
@@ -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
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
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(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
169
|
-
async function
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
282
|
-
async function
|
|
283
|
-
|
|
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<
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
299
|
-
|
|
300
|
-
|
|
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<
|
|
305
|
-
|
|
306
|
-
const validMappings = await filterValidMappings(mappings, targetRoot);
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
const setting = config.vscodeSettings ?? "auto";
|
|
307
293
|
|
|
308
|
-
|
|
309
|
-
const links = [...byTarget].flatMap(([targetPath, files]) =>
|
|
310
|
-
resolveLinks(targetPath, files),
|
|
311
|
-
);
|
|
294
|
+
if (setting === "never") return;
|
|
312
295
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
/**
|
|
375
|
-
async function
|
|
376
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
411
|
+
return { created: createdPaths.map((p) => relative(targetRoot, p)) };
|
|
412
|
+
}
|
|
388
413
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
await
|
|
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
|
}
|
package/src/commands/Move.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/Rm.ts
CHANGED
|
@@ -90,21 +90,30 @@ async function resolveScope(
|
|
|
90
90
|
return found[0];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
async function
|
|
93
|
+
/** Remove agent files (CLAUDE.md, GEMINI.md, AGENTS.md → agents.md) */
|
|
94
|
+
async function removeAgentFiles(
|
|
95
95
|
targetPath: string,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
138
|
-
async function
|
|
146
|
+
/** Search all scopes to find where the file exists */
|
|
147
|
+
async function findInScopes(
|
|
139
148
|
targetPath: string,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
await rm(overlayPath);
|
|
160
|
-
console.log(`Removed from overlay: agents.md`);
|
|
160
|
+
return found;
|
|
161
161
|
}
|