clank-cli 0.1.72 → 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/package.json +1 -1
- package/src/AgentFiles.ts +1 -1
- package/src/Cli.ts +23 -23
- package/src/FsUtil.ts +10 -0
- package/src/Mapper.ts +5 -5
- package/src/OverlayLinks.ts +1 -1
- package/src/commands/Add.ts +119 -146
- package/src/commands/Check.ts +130 -59
- package/src/commands/Link.ts +24 -31
- package/src/commands/files/Scan.ts +2 -1
package/package.json
CHANGED
package/src/AgentFiles.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
/** Base agent directory names (without leading dot) */
|
|
4
|
-
export const agentDirNames = ["claude", "gemini"];
|
|
4
|
+
export const agentDirNames = ["claude", "gemini", "codex"];
|
|
5
5
|
|
|
6
6
|
/** Agent directories as they appear in target (with leading dot) */
|
|
7
7
|
export const managedAgentDirs = agentDirNames.map((d) => `.${d}`);
|
package/src/Cli.ts
CHANGED
|
@@ -36,6 +36,8 @@ Clank Overlay Directory Structure
|
|
|
36
36
|
│ ├── settings.json # -> .claude/settings.json
|
|
37
37
|
│ ├── commands/ # -> .claude/commands/
|
|
38
38
|
│ └── agents/ # -> .claude/agents/
|
|
39
|
+
├── codex/ # Codex specific
|
|
40
|
+
│ └── config.toml # -> .codex/config.toml
|
|
39
41
|
├── <subdir>/clank/ # Subdirectory files (monorepo support)
|
|
40
42
|
│ └── notes.md # -> <subdir>/clank/notes.md
|
|
41
43
|
└── worktrees/
|
|
@@ -51,6 +53,7 @@ Mapping Rules
|
|
|
51
53
|
global/claude/commands/<file> -> .claude/commands/<file>
|
|
52
54
|
targets/<proj>/clank/<file> -> clank/<file>
|
|
53
55
|
targets/<proj>/claude/commands/ -> .claude/commands/
|
|
56
|
+
targets/<proj>/codex/config.toml -> .codex/config.toml
|
|
54
57
|
targets/<proj>/agents.md -> CLAUDE.md, AGENTS.md, GEMINI.md
|
|
55
58
|
targets/<proj>/<sub>/clank/<file> -> <sub>/clank/<file>
|
|
56
59
|
targets/<proj>/worktrees/<br>/clank -> clank/
|
|
@@ -84,31 +87,9 @@ export async function runCLI(): Promise<void> {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function registerCommands(program: Command): void {
|
|
87
|
-
registerCoreCommands(program);
|
|
88
|
-
registerHelpCommands(program);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function registerCoreCommands(program: Command): void {
|
|
92
90
|
registerOverlayCommands(program);
|
|
93
91
|
registerUtilityCommands(program);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
function registerHelpCommands(program: Command): void {
|
|
97
|
-
const help = program
|
|
98
|
-
.command("help")
|
|
99
|
-
.description("Show help information")
|
|
100
|
-
.argument("[command]", "Command to show help for")
|
|
101
|
-
.action((commandName?: string) => {
|
|
102
|
-
if (!commandName) return program.help();
|
|
103
|
-
const subcommand = program.commands.find((c) => c.name() === commandName);
|
|
104
|
-
if (subcommand) return subcommand.help();
|
|
105
|
-
console.error(`Unknown command: ${commandName}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
});
|
|
108
|
-
help
|
|
109
|
-
.command("structure")
|
|
110
|
-
.description("Show overlay directory structure")
|
|
111
|
-
.action(() => console.log(structureHelp));
|
|
92
|
+
registerHelpCommands(program);
|
|
112
93
|
}
|
|
113
94
|
|
|
114
95
|
function registerOverlayCommands(program: Command): void {
|
|
@@ -163,6 +144,7 @@ function registerUtilityCommands(program: Command): void {
|
|
|
163
144
|
.command("check")
|
|
164
145
|
.alias("status")
|
|
165
146
|
.description("Show overlay status and check for issues")
|
|
147
|
+
.option("--prompt", "Print the agent fix prompt for orphaned paths")
|
|
166
148
|
.action(withErrorHandling(checkCommand));
|
|
167
149
|
|
|
168
150
|
registerFilesCommand(program);
|
|
@@ -175,6 +157,24 @@ function registerUtilityCommands(program: Command): void {
|
|
|
175
157
|
.action(withErrorHandling(vscodeCommand));
|
|
176
158
|
}
|
|
177
159
|
|
|
160
|
+
function registerHelpCommands(program: Command): void {
|
|
161
|
+
const help = program
|
|
162
|
+
.command("help")
|
|
163
|
+
.description("Show help information")
|
|
164
|
+
.argument("[command]", "Command to show help for")
|
|
165
|
+
.action((commandName?: string) => {
|
|
166
|
+
if (!commandName) return program.help();
|
|
167
|
+
const subcommand = program.commands.find((c) => c.name() === commandName);
|
|
168
|
+
if (subcommand) return subcommand.help();
|
|
169
|
+
console.error(`Unknown command: ${commandName}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
help
|
|
173
|
+
.command("structure")
|
|
174
|
+
.description("Show overlay directory structure")
|
|
175
|
+
.action(() => console.log(structureHelp));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
178
|
function withErrorHandling<T extends unknown[]>(
|
|
179
179
|
fn: (...args: T) => Promise<void>,
|
|
180
180
|
): (...args: T) => Promise<void> {
|
package/src/FsUtil.ts
CHANGED
|
@@ -178,6 +178,16 @@ export async function isSymlink(filePath: string): Promise<boolean> {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** Check if a path is a real (non-symlink) file tracked by git */
|
|
182
|
+
export async function isTrackedRealFile(
|
|
183
|
+
filePath: string,
|
|
184
|
+
repoRoot: string,
|
|
185
|
+
): Promise<boolean> {
|
|
186
|
+
if (!(await fileExists(filePath))) return false;
|
|
187
|
+
if (await isSymlink(filePath)) return false;
|
|
188
|
+
return isTrackedByGit(filePath, repoRoot);
|
|
189
|
+
}
|
|
190
|
+
|
|
181
191
|
/** Get path relative to cwd, or "." if same directory */
|
|
182
192
|
export function relativePath(cwd: string, path: string): string {
|
|
183
193
|
return relative(cwd, path) || ".";
|
package/src/Mapper.ts
CHANGED
|
@@ -292,14 +292,14 @@ function encodeTargetPath(relPath: string, overlayBase: string): string {
|
|
|
292
292
|
if (basename(relPath) === "agents.md") {
|
|
293
293
|
return join(overlayBase, relPath);
|
|
294
294
|
}
|
|
295
|
-
//
|
|
295
|
+
// <agentDir>/prompts/ (.claude/, .gemini/, .codex/) → prompts/ in overlay (agent-agnostic)
|
|
296
296
|
for (const agentDir of managedAgentDirs) {
|
|
297
297
|
const prefix = `${agentDir}/prompts/`;
|
|
298
298
|
if (relPath.startsWith(prefix)) {
|
|
299
299
|
return join(overlayBase, "prompts", relPath.slice(prefix.length));
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
// .claude
|
|
302
|
+
// .claude/*, .gemini/*, .codex/* → claude/*, gemini/*, codex/* in overlay (agent-specific)
|
|
303
303
|
for (const agentDir of managedAgentDirs) {
|
|
304
304
|
if (relPath.startsWith(`${agentDir}/`)) {
|
|
305
305
|
const subPath = relPath.slice(agentDir.length + 1);
|
|
@@ -314,12 +314,12 @@ function encodeTargetPath(relPath: string, overlayBase: string): string {
|
|
|
314
314
|
return join(overlayBase, "clank", relPath);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/** Check if path starts with a managed agent dir (.claude/, .gemini/) */
|
|
317
|
+
/** Check if path starts with a managed agent dir (.claude/, .gemini/, .codex/) */
|
|
318
318
|
function startsWithAgentDir(path: string): boolean {
|
|
319
319
|
return managedAgentDirs.some((dir) => path.startsWith(`${dir}/`));
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
/** Check if path is inside a managed agent dir (.claude/, .gemini/) */
|
|
322
|
+
/** Check if path is inside a managed agent dir (.claude/, .gemini/, .codex/) */
|
|
323
323
|
function isInsideAgentDir(relPath: string): boolean {
|
|
324
324
|
return managedAgentDirs.some(
|
|
325
325
|
(dir) => relPath.startsWith(`${dir}/`) || relPath === dir,
|
|
@@ -346,7 +346,7 @@ function decodeOverlayPath(
|
|
|
346
346
|
};
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
// claude/, gemini/ files → map to .claude/, .gemini/ in target
|
|
349
|
+
// claude/, gemini/, codex/ files → map to .claude/, .gemini/, .codex/ in target
|
|
350
350
|
for (const agentDir of managedAgentDirs) {
|
|
351
351
|
const overlayDir = agentDir.slice(1); // "claude" or "gemini"
|
|
352
352
|
if (relPath.startsWith(`${overlayDir}/`)) {
|
package/src/OverlayLinks.ts
CHANGED
|
@@ -59,7 +59,7 @@ export async function verifyManaged(
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Prompt files are fanned out to all agent directories (.claude/prompts/, .gemini/prompts/)
|
|
62
|
+
// Prompt files are fanned out to all agent directories (.claude/prompts/, .gemini/prompts/, .codex/prompts/)
|
|
63
63
|
// Accept any agent's prompts dir as valid if the filename matches
|
|
64
64
|
if (mapping.targetPath !== linkPath) {
|
|
65
65
|
if (!isMatchingPromptPath(mapping.targetPath, linkPath)) {
|
package/src/commands/Add.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
getCwd,
|
|
18
18
|
getLinkTarget,
|
|
19
19
|
isSymlink,
|
|
20
|
-
|
|
20
|
+
isTrackedRealFile,
|
|
21
21
|
walkDirectory,
|
|
22
22
|
} from "../FsUtil.ts";
|
|
23
23
|
import { type GitContext, getGitContext } from "../Git.ts";
|
|
@@ -48,7 +48,6 @@ interface AddContext {
|
|
|
48
48
|
overlayRoot: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Create agent symlinks (CLAUDE.md, GEMINI.md, AGENTS.md → agents.md) */
|
|
52
51
|
interface AgentLinkParams {
|
|
53
52
|
overlayPath: string;
|
|
54
53
|
symlinkDir: string;
|
|
@@ -72,6 +71,17 @@ type ScopeCounts = {
|
|
|
72
71
|
skip: number;
|
|
73
72
|
};
|
|
74
73
|
|
|
74
|
+
interface CreateSymlinksParams {
|
|
75
|
+
filePath: string;
|
|
76
|
+
normalizedPath: string;
|
|
77
|
+
overlayPath: string;
|
|
78
|
+
config: { agents: string[] };
|
|
79
|
+
gitRoot: string;
|
|
80
|
+
overlayRoot: string;
|
|
81
|
+
cwd: string;
|
|
82
|
+
quiet?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
const scopeLabels: Record<Scope, string> = {
|
|
76
86
|
global: "global",
|
|
77
87
|
project: "project",
|
|
@@ -92,17 +102,11 @@ export async function addCommand(
|
|
|
92
102
|
|
|
93
103
|
const ctx = { cwd, gitContext, config, overlayRoot };
|
|
94
104
|
|
|
95
|
-
if (options.interactive) {
|
|
105
|
+
if (options.interactive || filePaths.length === 0) {
|
|
96
106
|
await addAllInteractive(ctx);
|
|
97
107
|
return;
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
if (filePaths.length === 0) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
"No files specified. Use --interactive for interactive mode.",
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
110
|
for (const filePath of filePaths) {
|
|
107
111
|
const inputPath = join(cwd, filePath);
|
|
108
112
|
|
|
@@ -137,18 +141,15 @@ async function validateAddOptions(
|
|
|
137
141
|
/** Interactive mode: add all unadded files with per-file scope selection */
|
|
138
142
|
async function addAllInteractive(ctx: AddContext): Promise<void> {
|
|
139
143
|
const { cwd, gitContext, overlayRoot } = ctx;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
targetRoot: gitContext.gitRoot,
|
|
143
|
-
gitContext,
|
|
144
|
-
};
|
|
144
|
+
const { gitRoot: targetRoot } = gitContext;
|
|
145
|
+
const context: MapperContext = { overlayRoot, targetRoot, gitContext };
|
|
145
146
|
|
|
146
147
|
const unadded = await findUnaddedFiles(context);
|
|
147
148
|
const regularFiles = unadded.filter((f) => f.kind === "unadded");
|
|
148
149
|
|
|
149
150
|
// Also find untracked agent files outside managed dirs (e.g. packages/foo/CLAUDE.md)
|
|
150
151
|
const agentClassification = await classifyAgentFiles(
|
|
151
|
-
|
|
152
|
+
targetRoot,
|
|
152
153
|
overlayRoot,
|
|
153
154
|
gitContext,
|
|
154
155
|
);
|
|
@@ -200,7 +201,6 @@ async function addSingleFile(
|
|
|
200
201
|
const { quiet } = options;
|
|
201
202
|
|
|
202
203
|
const scope = resolveScopeFromOptions(options);
|
|
203
|
-
/** Absolute path where symlink will be created in target repo */
|
|
204
204
|
const normalizedPath = normalizeAddPath(filePath, cwd, gitRoot);
|
|
205
205
|
const context: MapperContext = {
|
|
206
206
|
overlayRoot,
|
|
@@ -216,7 +216,6 @@ async function addSingleFile(
|
|
|
216
216
|
// Only check barePath for symlink - we want the user's input file, not clank/foo.md
|
|
217
217
|
const barePath = join(cwd, filePath);
|
|
218
218
|
|
|
219
|
-
// Check if already in overlay at a different scope
|
|
220
219
|
await checkScopeConflict(barePath, scope, context, cwd);
|
|
221
220
|
|
|
222
221
|
if (await fileExists(overlayPath)) {
|
|
@@ -246,53 +245,6 @@ async function addSingleFile(
|
|
|
246
245
|
});
|
|
247
246
|
}
|
|
248
247
|
|
|
249
|
-
interface CreateSymlinksParams {
|
|
250
|
-
filePath: string;
|
|
251
|
-
normalizedPath: string;
|
|
252
|
-
overlayPath: string;
|
|
253
|
-
config: { agents: string[] };
|
|
254
|
-
gitRoot: string;
|
|
255
|
-
overlayRoot: string;
|
|
256
|
-
cwd: string;
|
|
257
|
-
quiet?: boolean;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Create symlinks based on file type (agent, prompt, or regular) */
|
|
261
|
-
async function createSymlinksForFile(params: CreateSymlinksParams) {
|
|
262
|
-
const {
|
|
263
|
-
filePath,
|
|
264
|
-
normalizedPath,
|
|
265
|
-
overlayPath,
|
|
266
|
-
config,
|
|
267
|
-
gitRoot,
|
|
268
|
-
overlayRoot,
|
|
269
|
-
cwd,
|
|
270
|
-
quiet,
|
|
271
|
-
} = params;
|
|
272
|
-
|
|
273
|
-
if (isAgentFile(filePath)) {
|
|
274
|
-
const symlinkDir = dirname(normalizedPath);
|
|
275
|
-
await createAgentLinks({
|
|
276
|
-
overlayPath,
|
|
277
|
-
symlinkDir,
|
|
278
|
-
gitRoot,
|
|
279
|
-
overlayRoot,
|
|
280
|
-
agents: config.agents,
|
|
281
|
-
quiet,
|
|
282
|
-
});
|
|
283
|
-
} else if (isPromptFile(normalizedPath)) {
|
|
284
|
-
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd, quiet);
|
|
285
|
-
} else {
|
|
286
|
-
await handleRegularFile(
|
|
287
|
-
normalizedPath,
|
|
288
|
-
overlayPath,
|
|
289
|
-
overlayRoot,
|
|
290
|
-
cwd,
|
|
291
|
-
quiet,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
248
|
/** Prompt for scope and add a single file. Returns the choice or "error". */
|
|
297
249
|
async function promptAndAddFile(
|
|
298
250
|
relPath: string,
|
|
@@ -332,7 +284,7 @@ async function promptAndAddFile(
|
|
|
332
284
|
}
|
|
333
285
|
|
|
334
286
|
/** Print summary of interactive add results */
|
|
335
|
-
function printSummary(counts:
|
|
287
|
+
function printSummary(counts: ScopeCounts): void {
|
|
336
288
|
const parts: string[] = [];
|
|
337
289
|
if (counts.project > 0) parts.push(`${counts.project} to project`);
|
|
338
290
|
if (counts.worktree > 0) parts.push(`${counts.worktree} to worktree`);
|
|
@@ -398,6 +350,87 @@ async function addFileToOverlay(
|
|
|
398
350
|
}
|
|
399
351
|
}
|
|
400
352
|
|
|
353
|
+
/** Create symlinks based on file type (agent, prompt, or regular) */
|
|
354
|
+
async function createSymlinksForFile(params: CreateSymlinksParams) {
|
|
355
|
+
const { filePath, normalizedPath, overlayPath, config } = params;
|
|
356
|
+
const { gitRoot, overlayRoot, cwd, quiet } = params;
|
|
357
|
+
|
|
358
|
+
if (isAgentFile(filePath)) {
|
|
359
|
+
const symlinkDir = dirname(normalizedPath);
|
|
360
|
+
await createAgentLinks({
|
|
361
|
+
overlayPath,
|
|
362
|
+
symlinkDir,
|
|
363
|
+
gitRoot,
|
|
364
|
+
overlayRoot,
|
|
365
|
+
agents: config.agents,
|
|
366
|
+
quiet,
|
|
367
|
+
});
|
|
368
|
+
} else if (isPromptFile(normalizedPath)) {
|
|
369
|
+
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd, quiet);
|
|
370
|
+
} else {
|
|
371
|
+
await handleRegularFile(
|
|
372
|
+
normalizedPath,
|
|
373
|
+
overlayPath,
|
|
374
|
+
overlayRoot,
|
|
375
|
+
cwd,
|
|
376
|
+
quiet,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Read a single keypress for scope selection */
|
|
382
|
+
async function readScopeChoice(
|
|
383
|
+
isWorktree: boolean,
|
|
384
|
+
): Promise<InteractiveChoice> {
|
|
385
|
+
const key = await readSingleKey();
|
|
386
|
+
|
|
387
|
+
switch (key.toLowerCase()) {
|
|
388
|
+
case "p":
|
|
389
|
+
case "\r":
|
|
390
|
+
case "\n":
|
|
391
|
+
console.log("project");
|
|
392
|
+
return "project";
|
|
393
|
+
case "w":
|
|
394
|
+
if (!isWorktree) {
|
|
395
|
+
console.log("(not in worktree, using project)");
|
|
396
|
+
return "project";
|
|
397
|
+
}
|
|
398
|
+
console.log("worktree");
|
|
399
|
+
return "worktree";
|
|
400
|
+
case "g":
|
|
401
|
+
console.log("global");
|
|
402
|
+
return "global";
|
|
403
|
+
case "s":
|
|
404
|
+
console.log("skip");
|
|
405
|
+
return "skip";
|
|
406
|
+
case "q":
|
|
407
|
+
case "\x03": // Ctrl+C
|
|
408
|
+
return "quit";
|
|
409
|
+
default:
|
|
410
|
+
console.log("project");
|
|
411
|
+
return "project";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Find content from normalized path or bare input path */
|
|
416
|
+
async function findSourceContent(
|
|
417
|
+
normalizedPath: string,
|
|
418
|
+
barePath: string,
|
|
419
|
+
): Promise<string> {
|
|
420
|
+
// Try normalized path first (e.g., cwd/clank/foo.md)
|
|
421
|
+
if (
|
|
422
|
+
(await fileExists(normalizedPath)) &&
|
|
423
|
+
!(await isSymlink(normalizedPath))
|
|
424
|
+
) {
|
|
425
|
+
return await readFile(normalizedPath, "utf-8");
|
|
426
|
+
}
|
|
427
|
+
// Fall back to bare input path (e.g., cwd/foo.md)
|
|
428
|
+
if ((await fileExists(barePath)) && !(await isSymlink(barePath))) {
|
|
429
|
+
return await readFile(barePath, "utf-8");
|
|
430
|
+
}
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
|
|
401
434
|
async function createAgentLinks(p: AgentLinkParams): Promise<void> {
|
|
402
435
|
const { overlayPath, quiet, ...classifyParams } = p;
|
|
403
436
|
const { toCreate, existing, skipped } =
|
|
@@ -441,9 +474,8 @@ async function handlePromptFile(
|
|
|
441
474
|
gitRoot,
|
|
442
475
|
);
|
|
443
476
|
if (!quiet && created.length) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
);
|
|
477
|
+
const names = created.map((p) => relative(cwd, p)).join(", ");
|
|
478
|
+
console.log(`Created symlinks: ${names}`);
|
|
447
479
|
}
|
|
448
480
|
}
|
|
449
481
|
}
|
|
@@ -467,57 +499,27 @@ async function handleRegularFile(
|
|
|
467
499
|
}
|
|
468
500
|
}
|
|
469
501
|
|
|
470
|
-
/** Read a single keypress
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
502
|
+
/** Read a single keypress from stdin (raw mode) */
|
|
503
|
+
function readSingleKey(): Promise<string> {
|
|
504
|
+
return new Promise((resolve) => {
|
|
505
|
+
const wasRaw = process.stdin.isRaw;
|
|
506
|
+
if (process.stdin.isTTY) {
|
|
507
|
+
readline.emitKeypressEvents(process.stdin);
|
|
508
|
+
process.stdin.setRawMode(true);
|
|
509
|
+
}
|
|
510
|
+
process.stdin.resume();
|
|
475
511
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
case "\n":
|
|
480
|
-
console.log("project");
|
|
481
|
-
return "project";
|
|
482
|
-
case "w":
|
|
483
|
-
if (!isWorktree) {
|
|
484
|
-
console.log("(not in worktree, using project)");
|
|
485
|
-
return "project";
|
|
512
|
+
const onKeypress = (data: Buffer): void => {
|
|
513
|
+
if (process.stdin.isTTY) {
|
|
514
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
486
515
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return "global";
|
|
492
|
-
case "s":
|
|
493
|
-
console.log("skip");
|
|
494
|
-
return "skip";
|
|
495
|
-
case "q":
|
|
496
|
-
case "\x03": // Ctrl+C
|
|
497
|
-
return "quit";
|
|
498
|
-
default:
|
|
499
|
-
console.log("project");
|
|
500
|
-
return "project";
|
|
501
|
-
}
|
|
502
|
-
}
|
|
516
|
+
process.stdin.pause();
|
|
517
|
+
process.stdin.removeListener("data", onKeypress);
|
|
518
|
+
resolve(data.toString());
|
|
519
|
+
};
|
|
503
520
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
normalizedPath: string,
|
|
507
|
-
barePath: string,
|
|
508
|
-
): Promise<string> {
|
|
509
|
-
// Try normalized path first (e.g., cwd/clank/foo.md)
|
|
510
|
-
if (
|
|
511
|
-
(await fileExists(normalizedPath)) &&
|
|
512
|
-
!(await isSymlink(normalizedPath))
|
|
513
|
-
) {
|
|
514
|
-
return await readFile(normalizedPath, "utf-8");
|
|
515
|
-
}
|
|
516
|
-
// Fall back to bare input path (e.g., cwd/foo.md)
|
|
517
|
-
if ((await fileExists(barePath)) && !(await isSymlink(barePath))) {
|
|
518
|
-
return await readFile(barePath, "utf-8");
|
|
519
|
-
}
|
|
520
|
-
return "";
|
|
521
|
+
process.stdin.once("data", onKeypress);
|
|
522
|
+
});
|
|
521
523
|
}
|
|
522
524
|
|
|
523
525
|
/** Classify which agent symlinks to create vs skip */
|
|
@@ -530,18 +532,12 @@ async function classifyAgentLinks(
|
|
|
530
532
|
const toCreate: { targetPath: string; name: string }[] = [];
|
|
531
533
|
|
|
532
534
|
await forEachAgentPath(symlinkDir, agents, async (targetPath) => {
|
|
533
|
-
// Check if symlink already points to overlay
|
|
534
535
|
if (await isSymlinkToOverlay(targetPath, overlayRoot)) {
|
|
535
536
|
existing.push(basename(targetPath));
|
|
536
537
|
return;
|
|
537
538
|
}
|
|
538
539
|
|
|
539
|
-
|
|
540
|
-
(await fileExists(targetPath)) &&
|
|
541
|
-
!(await isSymlink(targetPath)) &&
|
|
542
|
-
(await isTrackedByGit(targetPath, gitRoot));
|
|
543
|
-
|
|
544
|
-
if (isTrackedFile) {
|
|
540
|
+
if (await isTrackedRealFile(targetPath, gitRoot)) {
|
|
545
541
|
skipped.push(basename(targetPath));
|
|
546
542
|
} else {
|
|
547
543
|
toCreate.push({ targetPath, name: basename(targetPath) });
|
|
@@ -550,26 +546,3 @@ async function classifyAgentLinks(
|
|
|
550
546
|
|
|
551
547
|
return { toCreate, existing, skipped };
|
|
552
548
|
}
|
|
553
|
-
|
|
554
|
-
/** Read a single keypress from stdin (raw mode) */
|
|
555
|
-
function readSingleKey(): Promise<string> {
|
|
556
|
-
return new Promise((resolve) => {
|
|
557
|
-
const wasRaw = process.stdin.isRaw;
|
|
558
|
-
if (process.stdin.isTTY) {
|
|
559
|
-
readline.emitKeypressEvents(process.stdin);
|
|
560
|
-
process.stdin.setRawMode(true);
|
|
561
|
-
}
|
|
562
|
-
process.stdin.resume();
|
|
563
|
-
|
|
564
|
-
const onKeypress = (data: Buffer): void => {
|
|
565
|
-
if (process.stdin.isTTY) {
|
|
566
|
-
process.stdin.setRawMode(wasRaw ?? false);
|
|
567
|
-
}
|
|
568
|
-
process.stdin.pause();
|
|
569
|
-
process.stdin.removeListener("data", onKeypress);
|
|
570
|
-
resolve(data.toString());
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
process.stdin.once("data", onKeypress);
|
|
574
|
-
});
|
|
575
|
-
}
|
package/src/commands/Check.ts
CHANGED
|
@@ -35,11 +35,18 @@ export type UnaddedFile = ManagedFileState & {
|
|
|
35
35
|
relativePath: string;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
export interface CheckOptions {
|
|
39
|
+
prompt?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
/** Files that should remain local and not be tracked by clank */
|
|
39
43
|
const localOnlyFiles = ["settings.local.json"];
|
|
40
44
|
|
|
45
|
+
/** Switch to a one-line summary when a category has at least this many files */
|
|
46
|
+
const compactThreshold = 5;
|
|
47
|
+
|
|
41
48
|
/** Check for orphaned overlay paths that don't match target structure */
|
|
42
|
-
export async function checkCommand(): Promise<void> {
|
|
49
|
+
export async function checkCommand(options: CheckOptions = {}): Promise<void> {
|
|
43
50
|
const cwd = await getCwd();
|
|
44
51
|
const gitContext = await getGitContext(cwd);
|
|
45
52
|
const config = await loadConfig();
|
|
@@ -50,7 +57,7 @@ export async function checkCommand(): Promise<void> {
|
|
|
50
57
|
|
|
51
58
|
await showOverlayStatus(overlayRoot, ignorePatterns);
|
|
52
59
|
|
|
53
|
-
const problems = await checkAllProblems(ctx, cwd, ignorePatterns);
|
|
60
|
+
const problems = await checkAllProblems(ctx, cwd, ignorePatterns, options);
|
|
54
61
|
if (!problems) {
|
|
55
62
|
console.log("No issues found. Overlay matches target structure.");
|
|
56
63
|
}
|
|
@@ -98,13 +105,8 @@ export async function findOrphans(
|
|
|
98
105
|
const isIgnored =
|
|
99
106
|
ignorePatterns.length > 0 ? picomatch(ignorePatterns) : null;
|
|
100
107
|
|
|
101
|
-
const skip = (relPath: string): boolean =>
|
|
102
|
-
|
|
103
|
-
const pathBasename = basename(relPath);
|
|
104
|
-
if (isIgnored(relPath) || isIgnored(pathBasename)) return true;
|
|
105
|
-
}
|
|
106
|
-
return false;
|
|
107
|
-
};
|
|
108
|
+
const skip = (relPath: string): boolean =>
|
|
109
|
+
!!isIgnored && (isIgnored(relPath) || isIgnored(basename(relPath)));
|
|
108
110
|
|
|
109
111
|
for await (const { path, isDirectory } of walkDirectory(projectDir, {
|
|
110
112
|
skipDirs: [".git", "node_modules", "worktrees"],
|
|
@@ -122,7 +124,6 @@ export async function findOrphans(
|
|
|
122
124
|
continue;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
// This is a subdirectory file - check if target dir exists
|
|
126
127
|
// e.g., tools/packages/wesl/clank/notes.md -> check tools/packages/wesl/
|
|
127
128
|
const targetSubdir = extractTargetSubdir(relPath);
|
|
128
129
|
if (!targetSubdir) continue;
|
|
@@ -173,6 +174,7 @@ async function checkAllProblems(
|
|
|
173
174
|
ctx: MapperContext,
|
|
174
175
|
cwd: string,
|
|
175
176
|
ignorePatterns: string[] = [],
|
|
177
|
+
options: CheckOptions = {},
|
|
176
178
|
): Promise<boolean> {
|
|
177
179
|
const { overlayRoot, targetRoot, gitContext } = ctx;
|
|
178
180
|
let hasProblems = false;
|
|
@@ -202,7 +204,7 @@ async function checkAllProblems(
|
|
|
202
204
|
);
|
|
203
205
|
if (orphans.length > 0) {
|
|
204
206
|
hasProblems = true;
|
|
205
|
-
showOrphanedPaths(orphans, targetRoot, overlayRoot);
|
|
207
|
+
showOrphanedPaths(orphans, targetRoot, overlayRoot, options);
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
return hasProblems;
|
|
@@ -227,12 +229,10 @@ function isLocalOnlyFile(relPath: string): boolean {
|
|
|
227
229
|
* @example "tools/packages/wesl/agents.md" -> "tools/packages/wesl"
|
|
228
230
|
*/
|
|
229
231
|
function extractTargetSubdir(relPath: string): string | null {
|
|
230
|
-
// Check for /clank/ or /claude/ in path
|
|
231
232
|
for (const dir of managedDirs) {
|
|
232
233
|
const idx = relPath.indexOf(`/${dir}/`);
|
|
233
234
|
if (idx !== -1) return relPath.slice(0, idx);
|
|
234
235
|
}
|
|
235
|
-
// Check for agents.md in a subdirectory
|
|
236
236
|
if (relPath.endsWith("/agents.md")) {
|
|
237
237
|
return relPath.slice(0, -"/agents.md".length);
|
|
238
238
|
}
|
|
@@ -250,49 +250,27 @@ function showUnaddedFiles(
|
|
|
250
250
|
? `${projectName}/${worktreeName}`
|
|
251
251
|
: projectName;
|
|
252
252
|
|
|
253
|
-
const outsideOverlay = unadded.filter(
|
|
254
|
-
|
|
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
|
+
);
|
|
255
268
|
const regularFiles = unadded.filter((f) => f.kind === "unadded");
|
|
256
269
|
|
|
257
|
-
if (outsideOverlay.length > 0)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
console.log("These symlinks point outside the clank overlay.");
|
|
262
|
-
console.log("Remove them manually, then run `clank link` to recreate:\n");
|
|
263
|
-
for (const file of outsideOverlay) {
|
|
264
|
-
console.log(` rm ${relativePath(cwd, file.targetPath)}`);
|
|
265
|
-
}
|
|
266
|
-
console.log();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (wrongMapping.length > 0) {
|
|
270
|
-
console.log(
|
|
271
|
-
`Found ${wrongMapping.length} mislinked symlink(s) in ${targetName}:\n`,
|
|
272
|
-
);
|
|
273
|
-
console.log("These symlinks point to the wrong overlay location.");
|
|
274
|
-
console.log("Run `clank link` to fix them.\n");
|
|
275
|
-
for (const file of wrongMapping) {
|
|
276
|
-
console.log(` ${relativePath(cwd, file.targetPath)}`);
|
|
277
|
-
if (file.currentTarget) {
|
|
278
|
-
console.log(` points to: ${file.currentTarget}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
console.log();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (regularFiles.length > 0) {
|
|
285
|
-
console.log(
|
|
286
|
-
`Found ${regularFiles.length} unadded file(s) in ${targetName}:\n`,
|
|
287
|
-
);
|
|
288
|
-
for (const file of regularFiles) {
|
|
289
|
-
console.log(` ${relativePath(cwd, file.targetPath)}`);
|
|
290
|
-
}
|
|
291
|
-
console.log();
|
|
292
|
-
console.log(" clank add -i # add interactively");
|
|
293
|
-
console.log(" clank add <file> [<file>...] # add specific files");
|
|
294
|
-
console.log();
|
|
295
|
-
}
|
|
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);
|
|
296
274
|
}
|
|
297
275
|
|
|
298
276
|
/** Display orphaned paths and remediation prompt */
|
|
@@ -300,12 +278,26 @@ function showOrphanedPaths(
|
|
|
300
278
|
orphans: OrphanedPath[],
|
|
301
279
|
targetRoot: string,
|
|
302
280
|
overlayRoot: string,
|
|
281
|
+
options: CheckOptions = {},
|
|
303
282
|
): void {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
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;
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
console.log("Target project:", targetRoot);
|
|
@@ -316,6 +308,85 @@ function showOrphanedPaths(
|
|
|
316
308
|
console.log("─".repeat(50));
|
|
317
309
|
}
|
|
318
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
|
+
|
|
319
390
|
/** Generate agent prompt for fixing orphaned paths */
|
|
320
391
|
function generateAgentPrompt(
|
|
321
392
|
orphans: OrphanedPath[],
|
package/src/commands/Link.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
getCwd,
|
|
22
22
|
getLinkTarget,
|
|
23
23
|
isSymlink,
|
|
24
|
-
|
|
24
|
+
isTrackedRealFile,
|
|
25
25
|
toSlash,
|
|
26
26
|
} from "../FsUtil.ts";
|
|
27
27
|
import { type GitContext, getGitContext } from "../Git.ts";
|
|
@@ -134,27 +134,6 @@ async function cleanStaleAndCheck(
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** Consolidate rules into generated AGENTS.md/GEMINI.md if rules exist */
|
|
138
|
-
async function maybeConsolidateRules(
|
|
139
|
-
overlayRoot: string,
|
|
140
|
-
targetRoot: string,
|
|
141
|
-
gitContext: GitContext,
|
|
142
|
-
config: ClankConfig,
|
|
143
|
-
): Promise<void> {
|
|
144
|
-
const consolidated = await consolidateRulesIntoAgentFiles({
|
|
145
|
-
overlayRoot,
|
|
146
|
-
targetRoot,
|
|
147
|
-
gitContext,
|
|
148
|
-
agents: config.agents,
|
|
149
|
-
});
|
|
150
|
-
if (consolidated.length > 0) {
|
|
151
|
-
console.log(`\nGenerated consolidated agent files:`);
|
|
152
|
-
for (const name of consolidated) {
|
|
153
|
-
console.log(` ${name}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
137
|
async function maybeInitWorktree(
|
|
159
138
|
overlayRoot: string,
|
|
160
139
|
gitContext: GitContext,
|
|
@@ -258,7 +237,7 @@ async function createAgentLinks(
|
|
|
258
237
|
}
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
/** 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/) */
|
|
262
241
|
async function createPromptLinks(
|
|
263
242
|
promptsMappings: FileMapping[],
|
|
264
243
|
targetRoot: string,
|
|
@@ -278,6 +257,27 @@ async function createPromptLinks(
|
|
|
278
257
|
}
|
|
279
258
|
}
|
|
280
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
|
+
|
|
281
281
|
/** Setup project settings.json - adopt existing or create new */
|
|
282
282
|
async function setupProjectSettings(
|
|
283
283
|
overlayRoot: string,
|
|
@@ -432,7 +432,7 @@ async function processAgentMapping(
|
|
|
432
432
|
const skipped: string[] = [];
|
|
433
433
|
|
|
434
434
|
await forEachAgentPath(targetDir, agents, async (agentPath) => {
|
|
435
|
-
if (await
|
|
435
|
+
if (await isTrackedRealFile(agentPath, targetRoot)) {
|
|
436
436
|
skipped.push(relative(targetRoot, agentPath));
|
|
437
437
|
} else {
|
|
438
438
|
const linkTarget = getLinkTarget(agentPath, overlayPath);
|
|
@@ -495,10 +495,3 @@ async function checkMappingParentExists(
|
|
|
495
495
|
}
|
|
496
496
|
return m;
|
|
497
497
|
}
|
|
498
|
-
|
|
499
|
-
/** Check if a file is tracked in git (exists as real file, not symlink, and tracked) */
|
|
500
|
-
async function isTrackedFile(path: string, gitRoot: string): Promise<boolean> {
|
|
501
|
-
if (!(await fileExists(path))) return false;
|
|
502
|
-
if (await isSymlink(path)) return false;
|
|
503
|
-
return isTrackedByGit(path, gitRoot);
|
|
504
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
|
+
import { managedAgentDirs } from "../../AgentFiles.ts";
|
|
2
3
|
import { expandPath, loadConfig } from "../../Config.ts";
|
|
3
4
|
import {
|
|
4
5
|
getCwd,
|
|
@@ -244,7 +245,7 @@ function passesLinkFilter(
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
function isInDotAgentDir(relPath: string): boolean {
|
|
247
|
-
return
|
|
248
|
+
return managedAgentDirs.some((dir) => isInDirectory(relPath, dir));
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
/** Enforce a max segment count beneath the nearest `clank/` path component. */
|