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.
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/AgentFiles.ts +1 -1
- package/src/ClassifyFiles.ts +9 -3
- package/src/Cli.ts +23 -23
- package/src/Consolidate.ts +250 -0
- package/src/FsUtil.ts +54 -4
- package/src/Git.ts +5 -2
- package/src/Gitignore.ts +20 -2
- package/src/Mapper.ts +47 -27
- package/src/OverlayLinks.ts +18 -18
- package/src/ScopeFromSymlink.ts +3 -2
- package/src/commands/Add.ts +142 -152
- package/src/commands/Check.ts +144 -61
- package/src/commands/Init.ts +1 -0
- package/src/commands/Link.ts +49 -32
- package/src/commands/Move.ts +2 -1
- package/src/commands/Rm.ts +2 -2
- package/src/commands/Unlink.ts +7 -2
- package/src/commands/VsCode.ts +2 -1
- package/src/commands/files/Dedupe.ts +5 -6
- package/src/commands/files/Scan.ts +21 -18
package/src/commands/Check.ts
CHANGED
|
@@ -7,8 +7,15 @@ import {
|
|
|
7
7
|
formatAgentFileProblems,
|
|
8
8
|
} from "../ClassifyFiles.ts";
|
|
9
9
|
import { expandPath, loadConfig } from "../Config.ts";
|
|
10
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
console.log(`
|
|
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[],
|
package/src/commands/Init.ts
CHANGED
|
@@ -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
|
|
package/src/commands/Link.ts
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
/**
|
|
123
|
-
async function
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
}
|
package/src/commands/Move.ts
CHANGED
|
@@ -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 =
|
|
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);
|
package/src/commands/Rm.ts
CHANGED
|
@@ -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 =
|
|
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);
|
package/src/commands/Unlink.ts
CHANGED
|
@@ -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 ||
|
|
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
|
|
package/src/commands/VsCode.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
133
|
-
return base.toUpperCase();
|
|
132
|
+
return basename(relPath).toUpperCase();
|
|
134
133
|
}
|