clank-cli 0.1.52
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 +312 -0
- package/bin/clank.ts +8 -0
- package/package.json +50 -0
- package/src/AgentFiles.ts +40 -0
- package/src/ClassifyFiles.ts +203 -0
- package/src/Cli.ts +229 -0
- package/src/Config.ts +98 -0
- package/src/Exclude.ts +154 -0
- package/src/Exec.ts +5 -0
- package/src/FsUtil.ts +154 -0
- package/src/Git.ts +140 -0
- package/src/Gitignore.ts +226 -0
- package/src/Mapper.ts +330 -0
- package/src/OverlayGit.ts +78 -0
- package/src/OverlayLinks.ts +125 -0
- package/src/ScopeFromSymlink.ts +22 -0
- package/src/Templates.ts +87 -0
- package/src/Util.ts +13 -0
- package/src/commands/Add.ts +301 -0
- package/src/commands/Check.ts +314 -0
- package/src/commands/Commit.ts +35 -0
- package/src/commands/Init.ts +62 -0
- package/src/commands/Link.ts +415 -0
- package/src/commands/Move.ts +172 -0
- package/src/commands/Rm.ts +161 -0
- package/src/commands/Unlink.ts +45 -0
- package/src/commands/VsCode.ts +195 -0
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Clank
|
|
2
|
+
|
|
3
|
+
Store AI agent files and notes in a separate git repository. Git-ignored symlinks make the files visible in your projects.
|
|
4
|
+
|
|
5
|
+
- **`clank add`** to move files to the overlay.
|
|
6
|
+
- **`clank link`** to connect overlay files to your project.
|
|
7
|
+
- **`clank unlink`** to disconnect.
|
|
8
|
+
- **`clank commit`** to commit changes in the overlay repository.
|
|
9
|
+
- **`clank check`** to show overlay status and help realign overlay files when your project restructures.
|
|
10
|
+
|
|
11
|
+
## Why a Separate Repository?
|
|
12
|
+
|
|
13
|
+
Clank stores your AI agent files (CLAUDE.md, commands, notes) in a separate git repository
|
|
14
|
+
symlinks them into your project. This separation provides key advantages:
|
|
15
|
+
|
|
16
|
+
- **Different Review Cadence**: Update agent instructions, commands, and notes without requiring the same review process as production code.
|
|
17
|
+
- **Work on Repos You Don't Control**: Add agent context to open source projects or third-party codebases without forking or modifying.
|
|
18
|
+
- **Persist Knowledge Across Forks**: Keep your agent context when working across multiple forks of the same project.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Separate Tracking**: Agent files live in their own repository with independent version control.
|
|
23
|
+
- **Multi-Agent Support**: Single source file, multiple symlinks (AGENTS.md, CLAUDE.md, GEMINI.md).
|
|
24
|
+
- **Worktree-Aware**: Works seamlessly with git worktrees.
|
|
25
|
+
- **Git Ignored**: Agent files are ignored in the main repo, tracked in the overlay repo.
|
|
26
|
+
- **Three Scopes**: Global (all projects), Project (all branches), Worktree (this branch only).
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g clank
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or use directly with npx:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx clank init
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Initialize Overlay Repository
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
clank init
|
|
46
|
+
# Creates ~/clankover with default structure
|
|
47
|
+
# Creates ~/.config/clank/config.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Link to Your Project
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd ~/my-project
|
|
54
|
+
clank link
|
|
55
|
+
# Auto-detects project name from git
|
|
56
|
+
# Creates symlinks from overlay to current directory
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Add Files with `clank add`
|
|
60
|
+
|
|
61
|
+
The `clank add` command moves files to the overlay and creates symlinks.
|
|
62
|
+
Agent files (CLAUDE.md, AGENTS.md) stay in place; other files go in `clank/`.
|
|
63
|
+
|
|
64
|
+
> You can run `clank add` from any subdirectory. Agent files and `clank/` folders work at any level in your project tree.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Add to project scope (default) - shared across all branches
|
|
68
|
+
clank add CLAUDE.md
|
|
69
|
+
|
|
70
|
+
# Add to global scope - shared across all projects
|
|
71
|
+
clank add style.md --global
|
|
72
|
+
|
|
73
|
+
# Add to worktree scope - this branch only
|
|
74
|
+
clank add notes.md --worktree
|
|
75
|
+
|
|
76
|
+
# Add commands
|
|
77
|
+
clank add .claude/commands/review.md --global # All projects
|
|
78
|
+
clank add .claude/commands/build.md # This project
|
|
79
|
+
|
|
80
|
+
# Add a directory (all files inside)
|
|
81
|
+
clank add clank/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
### `clank init [overlay-path]`
|
|
87
|
+
|
|
88
|
+
Run once to create the overlay repository (default: `~/clankover`) and config file.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
clank init # Default: ~/clankover
|
|
92
|
+
clank init ~/my-clankover # Custom location
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `clank link [target]`
|
|
96
|
+
|
|
97
|
+
Create symlinks from the overlay's agent files and notes into your project (current project by default).
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
clank link # Link current project to clank
|
|
101
|
+
clank link ~/my-project # Link to specific project
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `clank add <file> [options]`
|
|
105
|
+
|
|
106
|
+
Move a file to the overlay and replace it with a symlink.
|
|
107
|
+
If the file doesn't exist, an empty file is created.
|
|
108
|
+
|
|
109
|
+
**Scope Options:**
|
|
110
|
+
|
|
111
|
+
| Flag | Scope | Shared Across |
|
|
112
|
+
|------|-------|---------------|
|
|
113
|
+
| `--global` | Global | All projects |
|
|
114
|
+
| `--project` | Project (default) | All branches in project |
|
|
115
|
+
| `--worktree` | Worktree | This branch only |
|
|
116
|
+
|
|
117
|
+
**Examples:**
|
|
118
|
+
```bash
|
|
119
|
+
clank add style.md # Project scope (default)
|
|
120
|
+
clank add style.md --global # Global scope
|
|
121
|
+
clank add notes.md --worktree # Worktree scope
|
|
122
|
+
clank add .claude/commands/review.md --global # Global command
|
|
123
|
+
clank add .claude/commands/build.md # Project command (default)
|
|
124
|
+
clank add CLAUDE.md # Creates agents.md + agent symlinks
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `clank unlink [target]`
|
|
128
|
+
|
|
129
|
+
Remove all clank symlinks from target directory.
|
|
130
|
+
|
|
131
|
+
**Example:**
|
|
132
|
+
```bash
|
|
133
|
+
clank unlink # Unlink current directory
|
|
134
|
+
clank unlink ~/my-project
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `clank commit [-m message]`
|
|
138
|
+
|
|
139
|
+
Commit all changes in the overlay repository.
|
|
140
|
+
|
|
141
|
+
**Options:**
|
|
142
|
+
- `-m, --message <message>` - Commit message (default: "update")
|
|
143
|
+
|
|
144
|
+
All commits are prefixed with `[clank]` and include a summary of changed files.
|
|
145
|
+
|
|
146
|
+
**Example:**
|
|
147
|
+
```bash
|
|
148
|
+
clank commit # Commits with "[clank] update"
|
|
149
|
+
clank commit -m "add style guide" # Commits with "[clank] add style guide"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `clank check`
|
|
153
|
+
|
|
154
|
+
Check for orphaned overlay paths that don't match the target project structure.
|
|
155
|
+
Useful when a target project has renamed directories and the overlay needs updating.
|
|
156
|
+
|
|
157
|
+
Outputs an agent-friendly prompt to help fix mismatches.
|
|
158
|
+
|
|
159
|
+
**Example:**
|
|
160
|
+
```bash
|
|
161
|
+
clank check
|
|
162
|
+
# Found 2 orphaned overlay path(s):
|
|
163
|
+
# notes.md (my-project)
|
|
164
|
+
# Expected dir: packages/old-name
|
|
165
|
+
# ...
|
|
166
|
+
# To fix with an agent, copy this prompt:
|
|
167
|
+
# ──────────────────────────────────────────────────
|
|
168
|
+
# The following overlay files no longer match...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `clank help structure`
|
|
172
|
+
|
|
173
|
+
Show the overlay directory structure and mapping rules.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
clank help structure
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `--config <path>` (global option)
|
|
180
|
+
|
|
181
|
+
Specify a custom config file location (default `~/.config/clank/config.js`).
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
clank --config /tmp/test-config.js init /tmp/test-overlay
|
|
185
|
+
clank --config /tmp/test-config.js link
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Project Symlinks
|
|
189
|
+
|
|
190
|
+
Clank places symlinks in your project referencing the relevant files in the overlay repository.
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
~/my-project/
|
|
194
|
+
├── CLAUDE.md # Agent file (→ overlay)
|
|
195
|
+
├── GEMINI.md # Same content, different name
|
|
196
|
+
├── .claude/commands/ # Claude commands (→ overlay)
|
|
197
|
+
├── clank/notes.md # Notes and other files (→ overlay)
|
|
198
|
+
└── packages/core/
|
|
199
|
+
├── CLAUDE.md # Package-level agent file
|
|
200
|
+
├── GEMINI.md
|
|
201
|
+
└── clank/architecture.md # Package-level notes
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`clank link` configures git to ignore the symlinks.
|
|
205
|
+
|
|
206
|
+
### Scope Suffixes
|
|
207
|
+
|
|
208
|
+
If you add a file with the same name at different scopes (e.g., `notes.md` with both `--global` and `--worktree`), Clank distinguishes them with suffixes:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
clank/
|
|
212
|
+
├── notes.md # Global (no suffix)
|
|
213
|
+
├── notes-project.md # Project
|
|
214
|
+
└── notes-worktree.md # Worktree
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Configuration
|
|
218
|
+
|
|
219
|
+
Global configuration is stored by default in `~/.config/clank/config.js`:
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
export default {
|
|
223
|
+
overlayRepo: "~/clankover",
|
|
224
|
+
agents: ["agents", "claude", "gemini"],
|
|
225
|
+
vscodeSettings: "auto", // "auto" | "always" | "never"
|
|
226
|
+
vscodeGitignore: true
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `agents` - which symlinks to create for agent files like CLAUDE.md
|
|
231
|
+
- `vscodeSettings` - when to generate `.vscode/settings.json` to show clank files in VS Code
|
|
232
|
+
- `"auto"` (default): only if project already has a `.vscode` directory
|
|
233
|
+
- `"always"`: always generate settings
|
|
234
|
+
- `"never"`: never auto-generate (you can still run `clank vscode` manually)
|
|
235
|
+
- `vscodeGitignore` - add `.vscode/settings.json` to `.git/info/exclude` (default: true)
|
|
236
|
+
|
|
237
|
+
By default, clank creates symlinks for AGENTS.md, CLAUDE.md, and GEMINI.md.
|
|
238
|
+
Run `clank unlink` then `clank link` to apply config changes.
|
|
239
|
+
|
|
240
|
+
## Worktree Templates
|
|
241
|
+
|
|
242
|
+
Customize `overlay/global/init/` to create starter notes and planning files
|
|
243
|
+
for worktrees.
|
|
244
|
+
When you run `clank link` in a new worktree,
|
|
245
|
+
these templates are copied into your overlay.
|
|
246
|
+
|
|
247
|
+
Available placeholders:
|
|
248
|
+
|
|
249
|
+
- `{{worktree_message}}` - "This is git worktree {branch} of project {project}."
|
|
250
|
+
- `{{project_name}}` - Project name from git
|
|
251
|
+
- `{{branch_name}}` - Current branch/worktree name
|
|
252
|
+
|
|
253
|
+
## Design Principles
|
|
254
|
+
|
|
255
|
+
1. **Everything is linked, nothing is copied** - Single source of truth in overlay
|
|
256
|
+
2. **Git-aware** - Automatic project and worktree detection
|
|
257
|
+
3. **Explicit scopes** - Three clear levels: global, project, worktree
|
|
258
|
+
4. **Flat target structure** - All clank notes show up together in `clank/`
|
|
259
|
+
5. **Simple commands** - mostly `clank add` and `clank link`
|
|
260
|
+
|
|
261
|
+
## Reference
|
|
262
|
+
|
|
263
|
+
### Overlay Repository Structure
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
~/clankover/
|
|
267
|
+
├── global/
|
|
268
|
+
│ ├── clank/ # Global files (--global)
|
|
269
|
+
│ │ └── style.md
|
|
270
|
+
│ ├── prompts/ # -> .claude/prompts/, .gemini/prompts/
|
|
271
|
+
│ │ └── review.md
|
|
272
|
+
│ ├── claude/ # Claude Code specific
|
|
273
|
+
│ │ ├── commands/ # -> .claude/commands/
|
|
274
|
+
│ │ └── agents/ # -> .claude/agents/
|
|
275
|
+
│ ├── gemini/ # Gemini specific
|
|
276
|
+
│ │ └── commands/ # -> .gemini/commands/
|
|
277
|
+
│ └── init/ # Templates for new worktrees
|
|
278
|
+
│ └── clank/
|
|
279
|
+
│ └── notes.md
|
|
280
|
+
└── targets/
|
|
281
|
+
└── my-project/
|
|
282
|
+
├── agents.md # Agent instructions (source of truth)
|
|
283
|
+
├── clank/ # Project files (--project)
|
|
284
|
+
│ └── overview.md
|
|
285
|
+
├── prompts/ # -> .claude/prompts/, .gemini/prompts/
|
|
286
|
+
│ └── manifest.md
|
|
287
|
+
├── claude/ # Claude Code specific
|
|
288
|
+
│ ├── settings.json # -> .claude/settings.json
|
|
289
|
+
│ ├── commands/ # -> .claude/commands/
|
|
290
|
+
│ └── agents/ # -> .claude/agents/
|
|
291
|
+
├── gemini/ # Gemini specific
|
|
292
|
+
│ └── commands/ # -> .gemini/commands/
|
|
293
|
+
└── worktrees/
|
|
294
|
+
├── main/
|
|
295
|
+
│ ├── clank/ # Worktree files (--worktree)
|
|
296
|
+
│ │ └── notes.md
|
|
297
|
+
│ ├── prompts/ # Worktree-specific prompts
|
|
298
|
+
│ └── agents.md # Worktree agents file (optional)
|
|
299
|
+
└── feature-auth/
|
|
300
|
+
└── clank/
|
|
301
|
+
└── notes.md
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Requirements
|
|
305
|
+
|
|
306
|
+
- Node.js >= 22.6.0
|
|
307
|
+
- Git repository (for project/worktree detection)
|
|
308
|
+
- macOS, Linux, or [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
package/bin/clank.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clank-cli",
|
|
3
|
+
"version": "0.1.52",
|
|
4
|
+
"description": "Keep AI files in a separate overlay repository",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clank": "./bin/clank.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.6.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"assistant",
|
|
19
|
+
"claude",
|
|
20
|
+
"gemini",
|
|
21
|
+
"copilot",
|
|
22
|
+
"configuration",
|
|
23
|
+
"worktree",
|
|
24
|
+
"git"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"commander": "^14.0.2",
|
|
30
|
+
"cosmiconfig": "^9.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "^2.3.8",
|
|
34
|
+
"@types/node": "^24.10.1",
|
|
35
|
+
"execa": "^9.6.1",
|
|
36
|
+
"monobump": "^0.1.5",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^4.0.14"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest",
|
|
42
|
+
"test:ui": "vitest --ui",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"bump": "monobump",
|
|
45
|
+
"global": "pnpm link --global",
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"lint": "biome check",
|
|
48
|
+
"fix": "biome check --fix --unsafe"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/** Base agent directory names (without leading dot) */
|
|
4
|
+
export const agentDirNames = ["claude", "gemini"];
|
|
5
|
+
|
|
6
|
+
/** Agent directories as they appear in target (with leading dot) */
|
|
7
|
+
export const managedAgentDirs = agentDirNames.map((d) => `.${d}`);
|
|
8
|
+
|
|
9
|
+
/** Agent file names that clank manages */
|
|
10
|
+
export const agentFiles = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
|
|
11
|
+
|
|
12
|
+
/** Directory names managed by clank (as stored in overlay, without leading dot) */
|
|
13
|
+
export const managedDirs = ["clank", "prompts", ...agentDirNames];
|
|
14
|
+
|
|
15
|
+
/** Directory names managed by clank (as stored in target, with leading dot) */
|
|
16
|
+
export const targetManagedDirs = ["clank", ...managedAgentDirs];
|
|
17
|
+
|
|
18
|
+
/** Build agent file paths mapping for a directory */
|
|
19
|
+
export function getAgentFilePaths(dir: string): Record<string, string> {
|
|
20
|
+
return {
|
|
21
|
+
agents: join(dir, "AGENTS.md"),
|
|
22
|
+
claude: join(dir, "CLAUDE.md"),
|
|
23
|
+
gemini: join(dir, "GEMINI.md"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Iterate over agent file paths, calling fn for each configured agent */
|
|
28
|
+
export async function forEachAgentPath(
|
|
29
|
+
dir: string,
|
|
30
|
+
agents: string[],
|
|
31
|
+
fn: (agentPath: string, agentName: string) => Promise<void>,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const agentPaths = getAgentFilePaths(dir);
|
|
34
|
+
for (const agent of agents) {
|
|
35
|
+
const key = agent.toLowerCase();
|
|
36
|
+
const agentPath = agentPaths[key];
|
|
37
|
+
if (!agentPath) continue;
|
|
38
|
+
await fn(agentPath, agent);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { agentFiles } from "./AgentFiles.ts";
|
|
4
|
+
import { isTrackedByGit, relativePath, resolveSymlinkTarget, walkDirectory } from "./FsUtil.ts";
|
|
5
|
+
import type { GitContext } from "./Git.ts";
|
|
6
|
+
import { type MapperContext, targetToOverlay } from "./Mapper.ts";
|
|
7
|
+
|
|
8
|
+
/** Agent files grouped by problem type */
|
|
9
|
+
export interface AgentFileClassification {
|
|
10
|
+
/** Tracked in git - needs conversion (git rm --cached + clank add) */
|
|
11
|
+
tracked: string[];
|
|
12
|
+
|
|
13
|
+
/** Untracked real files - needs clank add first */
|
|
14
|
+
untracked: string[];
|
|
15
|
+
|
|
16
|
+
/** Symlinks pointing somewhere other than overlay - needs removal */
|
|
17
|
+
staleSymlinks: string[];
|
|
18
|
+
|
|
19
|
+
/** Symlinks pointing to wrong path in overlay (e.g., after rename) */
|
|
20
|
+
outdatedSymlinks: OutdatedSymlink[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PartialClassification = Partial<AgentFileClassification>;
|
|
24
|
+
|
|
25
|
+
export interface OutdatedSymlink {
|
|
26
|
+
/** Path to the symlink in the target */
|
|
27
|
+
symlinkPath: string;
|
|
28
|
+
|
|
29
|
+
/** Where it currently points */
|
|
30
|
+
currentTarget: string;
|
|
31
|
+
|
|
32
|
+
/** Where it should point based on current location */
|
|
33
|
+
expectedTarget: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Find all agent files in the repository and classify them.
|
|
37
|
+
* Returns absolute paths in the classification.
|
|
38
|
+
*/
|
|
39
|
+
export async function classifyAgentFiles(
|
|
40
|
+
targetRoot: string,
|
|
41
|
+
overlayRoot: string,
|
|
42
|
+
gitContext?: GitContext,
|
|
43
|
+
): Promise<AgentFileClassification> {
|
|
44
|
+
const allAgentFiles = await findAllAgentFiles(targetRoot);
|
|
45
|
+
const mapperCtx: MapperContext | undefined = gitContext
|
|
46
|
+
? { overlayRoot, targetRoot, gitContext }
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const classifications = await Promise.all(
|
|
50
|
+
allAgentFiles.map((f) =>
|
|
51
|
+
classifySingleAgentFile(f, targetRoot, overlayRoot, mapperCtx),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return mergeClassifications(classifications);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @return true if classification has any problems */
|
|
59
|
+
export function agentFileProblems(
|
|
60
|
+
classification: AgentFileClassification,
|
|
61
|
+
): boolean {
|
|
62
|
+
return (
|
|
63
|
+
classification.tracked.length > 0 ||
|
|
64
|
+
classification.untracked.length > 0 ||
|
|
65
|
+
classification.staleSymlinks.length > 0 ||
|
|
66
|
+
classification.outdatedSymlinks.length > 0
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Format all agent file problems as a single message.
|
|
71
|
+
* Paths are formatted relative to cwd for copy-paste convenience.
|
|
72
|
+
*/
|
|
73
|
+
export function formatAgentFileProblems(
|
|
74
|
+
classified: AgentFileClassification,
|
|
75
|
+
cwd: string,
|
|
76
|
+
): string {
|
|
77
|
+
const sections: string[] = [];
|
|
78
|
+
const rel = (p: string) => relativePath(cwd, p);
|
|
79
|
+
|
|
80
|
+
if (classified.tracked.length > 0) {
|
|
81
|
+
const commands = [
|
|
82
|
+
...classified.tracked.map((p) => ` git rm --cached ${rel(p)}`),
|
|
83
|
+
...classified.tracked.map((p) => ` clank add ${rel(p)}`),
|
|
84
|
+
];
|
|
85
|
+
sections.push(`Found tracked agent files. Clank manages agent files via symlinks.
|
|
86
|
+
|
|
87
|
+
To convert to clank management:
|
|
88
|
+
${commands.join("\n")}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (classified.untracked.length > 0) {
|
|
92
|
+
const commands = classified.untracked.map((p) => ` clank add ${rel(p)}`);
|
|
93
|
+
sections.push(`Found untracked agent files.
|
|
94
|
+
|
|
95
|
+
Add them to clank:
|
|
96
|
+
${commands.join("\n")}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (classified.staleSymlinks.length > 0) {
|
|
100
|
+
const commands = classified.staleSymlinks.map((p) => ` rm ${rel(p)}`);
|
|
101
|
+
sections.push(`Found stale agent symlinks (not pointing to clank overlay).
|
|
102
|
+
|
|
103
|
+
Remove them, then run \`clank link\` to recreate:
|
|
104
|
+
${commands.join("\n")}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (classified.outdatedSymlinks.length > 0) {
|
|
108
|
+
const details = classified.outdatedSymlinks.map((s) => {
|
|
109
|
+
const symlinkRel = rel(s.symlinkPath);
|
|
110
|
+
return ` ${symlinkRel}\n points to: ${s.currentTarget}\n expected: ${s.expectedTarget}`;
|
|
111
|
+
});
|
|
112
|
+
sections.push(`Found outdated agent symlinks (pointing to wrong overlay path).
|
|
113
|
+
|
|
114
|
+
This typically happens after a directory rename. Remove symlinks and run \`clank link\`:
|
|
115
|
+
${details.join("\n\n")}
|
|
116
|
+
|
|
117
|
+
To fix:
|
|
118
|
+
rm ${classified.outdatedSymlinks.map((s) => rel(s.symlinkPath)).join(" ")}
|
|
119
|
+
clank link`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return sections.join("\n\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Classify a single agent file */
|
|
126
|
+
async function classifySingleAgentFile(
|
|
127
|
+
filePath: string,
|
|
128
|
+
targetRoot: string,
|
|
129
|
+
overlayRoot: string,
|
|
130
|
+
mapperCtx?: MapperContext,
|
|
131
|
+
): Promise<PartialClassification> {
|
|
132
|
+
const stat = await lstat(filePath);
|
|
133
|
+
|
|
134
|
+
if (stat.isSymbolicLink()) {
|
|
135
|
+
return classifyAgentSymlink(filePath, overlayRoot, mapperCtx);
|
|
136
|
+
}
|
|
137
|
+
if (stat.isFile()) {
|
|
138
|
+
const isTracked = await isTrackedByGit(filePath, targetRoot);
|
|
139
|
+
return isTracked ? { tracked: [filePath] } : { untracked: [filePath] };
|
|
140
|
+
}
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Classify an agent symlink - check if stale or outdated */
|
|
145
|
+
async function classifyAgentSymlink(
|
|
146
|
+
filePath: string,
|
|
147
|
+
overlayRoot: string,
|
|
148
|
+
mapperCtx?: MapperContext,
|
|
149
|
+
): Promise<PartialClassification> {
|
|
150
|
+
const absoluteTarget = await resolveSymlinkTarget(filePath);
|
|
151
|
+
|
|
152
|
+
// Symlink doesn't point to overlay at all
|
|
153
|
+
if (!absoluteTarget.startsWith(overlayRoot)) {
|
|
154
|
+
return { staleSymlinks: [filePath] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if it points to the correct path within overlay
|
|
158
|
+
if (mapperCtx) {
|
|
159
|
+
// Agent files (CLAUDE.md, etc.) map to agents.md in overlay
|
|
160
|
+
const agentsMdPath = join(dirname(filePath), "agents.md");
|
|
161
|
+
const expectedTarget = targetToOverlay(agentsMdPath, "project", mapperCtx);
|
|
162
|
+
if (absoluteTarget !== expectedTarget) {
|
|
163
|
+
return {
|
|
164
|
+
outdatedSymlinks: [
|
|
165
|
+
{
|
|
166
|
+
symlinkPath: filePath,
|
|
167
|
+
currentTarget: absoluteTarget,
|
|
168
|
+
expectedTarget,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Merge sparse classifications into a complete classification with arrays */
|
|
179
|
+
function mergeClassifications(
|
|
180
|
+
items: PartialClassification[],
|
|
181
|
+
): AgentFileClassification {
|
|
182
|
+
return {
|
|
183
|
+
tracked: items.flatMap((i) => i.tracked ?? []),
|
|
184
|
+
untracked: items.flatMap((i) => i.untracked ?? []),
|
|
185
|
+
staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
|
|
186
|
+
outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Find all agent files in the repository */
|
|
191
|
+
async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
|
|
192
|
+
const files: string[] = [];
|
|
193
|
+
const agentFileSet = new Set(agentFiles);
|
|
194
|
+
|
|
195
|
+
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
196
|
+
if (isDirectory) continue;
|
|
197
|
+
if (agentFileSet.has(basename(path))) {
|
|
198
|
+
files.push(path);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return files;
|
|
203
|
+
}
|