clank-cli 0.1.61 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -4
- package/package.json +3 -2
- package/src/AgentFiles.ts +1 -2
- package/src/ClassifyFiles.ts +44 -49
- package/src/Cli.ts +74 -72
- package/src/Config.ts +12 -15
- package/src/Exclude.ts +9 -9
- package/src/Git.ts +10 -10
- package/src/Gitignore.ts +38 -49
- package/src/Mapper.ts +71 -71
- package/src/OverlayGit.ts +28 -7
- package/src/OverlayLinks.ts +6 -11
- package/src/commands/Add.ts +360 -128
- package/src/commands/Check.ts +159 -139
- package/src/commands/Commit.ts +1 -1
- package/src/commands/Link.ts +226 -200
- package/src/commands/Move.ts +146 -16
- package/src/commands/Rm.ts +60 -50
- package/src/commands/VsCode.ts +24 -24
package/README.md
CHANGED
|
@@ -84,6 +84,9 @@ clank link ~/my-project # Link to specific project
|
|
|
84
84
|
|
|
85
85
|
Move a file to the overlay and replace it with a symlink. If the file doesn't exist, an empty file is created. Accepts [scope options](#scope-options).
|
|
86
86
|
|
|
87
|
+
**Options:**
|
|
88
|
+
- `-i, --interactive` - Interactively add all unadded files, prompting for scope on each
|
|
89
|
+
|
|
87
90
|
**Examples:**
|
|
88
91
|
```bash
|
|
89
92
|
clank add style.md # Project scope (default)
|
|
@@ -92,6 +95,24 @@ clank add notes.md --worktree # Worktree scope
|
|
|
92
95
|
clank add .claude/commands/review.md --global # Global command
|
|
93
96
|
clank add .claude/commands/build.md # Project command (default)
|
|
94
97
|
clank add CLAUDE.md # Creates agents.md + agent symlinks
|
|
98
|
+
clank add -i # Interactive mode for all unadded files
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Interactive mode:**
|
|
102
|
+
|
|
103
|
+
Running `clank add --interactive` finds all unadded files and prompts for each:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Found 3 unadded file(s):
|
|
107
|
+
|
|
108
|
+
[1/3] clank/notes.md
|
|
109
|
+
(P)roject (W)orktree (G)lobal (S)kip (Q)uit [P]:
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Press a single key to select the scope (default: Project). The summary shows what was added:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
Added 2 to project, 1 skipped
|
|
95
116
|
```
|
|
96
117
|
|
|
97
118
|
### `clank unlink [target]`
|
|
@@ -173,10 +194,13 @@ clank rm style.md --global # Remove global style guide
|
|
|
173
194
|
|
|
174
195
|
### `clank mv <files...>` (alias: `move`)
|
|
175
196
|
|
|
176
|
-
Move file(s)
|
|
197
|
+
Move or rename file(s) in the overlay. With a [scope option](#scope-options), moves files to that scope. With two arguments and no scope, renames within the current scope.
|
|
177
198
|
|
|
178
|
-
**
|
|
199
|
+
**Examples:**
|
|
179
200
|
```bash
|
|
201
|
+
# Rename a file (keeps same scope)
|
|
202
|
+
clank mv batch-dry-check.md batch-dry.md
|
|
203
|
+
|
|
180
204
|
# Promote a worktree note to project scope
|
|
181
205
|
clank mv clank/notes.md --project
|
|
182
206
|
|
|
@@ -205,7 +229,7 @@ clank help structure
|
|
|
205
229
|
|
|
206
230
|
### `--config <path>` (global option)
|
|
207
231
|
|
|
208
|
-
Specify a custom config file location (default `~/.config/clank/config.js`).
|
|
232
|
+
Specify a custom config file location (default `~/.config/clank/clank.config.js`).
|
|
209
233
|
|
|
210
234
|
```bash
|
|
211
235
|
clank --config /tmp/test-config.js init /tmp/test-overlay
|
|
@@ -243,7 +267,7 @@ clank/
|
|
|
243
267
|
|
|
244
268
|
## Configuration
|
|
245
269
|
|
|
246
|
-
Global configuration is stored
|
|
270
|
+
Global configuration is stored in `~/.config/clank/clank.config.js`:
|
|
247
271
|
|
|
248
272
|
```javascript
|
|
249
273
|
export default {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clank-cli",
|
|
3
3
|
"description": "Keep AI files in a separate overlay repository",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.65",
|
|
5
5
|
"author": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -46,10 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"bump": "monobump",
|
|
49
|
-
"fix": "biome check --fix --unsafe",
|
|
49
|
+
"fix": "biome check --fix --unsafe --error-on-warnings",
|
|
50
50
|
"fix:pkgJsonFormat": "syncpack format",
|
|
51
51
|
"global": "pnpm link --global",
|
|
52
52
|
"lint": "biome check --error-on-warnings",
|
|
53
|
+
"prepush": "run-s fix typecheck fix:pkgJsonFormat test:once",
|
|
53
54
|
"test": "vitest",
|
|
54
55
|
"test:once": "vitest run",
|
|
55
56
|
"test:ui": "vitest --ui",
|
package/src/AgentFiles.ts
CHANGED
|
@@ -32,8 +32,7 @@ export async function forEachAgentPath(
|
|
|
32
32
|
): Promise<void> {
|
|
33
33
|
const agentPaths = getAgentFilePaths(dir);
|
|
34
34
|
for (const agent of agents) {
|
|
35
|
-
const
|
|
36
|
-
const agentPath = agentPaths[key];
|
|
35
|
+
const agentPath = agentPaths[agent.toLowerCase()];
|
|
37
36
|
if (!agentPath) continue;
|
|
38
37
|
await fn(agentPath, agent);
|
|
39
38
|
}
|
package/src/ClassifyFiles.ts
CHANGED
|
@@ -25,8 +25,6 @@ export interface AgentFileClassification {
|
|
|
25
25
|
outdatedSymlinks: OutdatedSymlink[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
type PartialClassification = Partial<AgentFileClassification>;
|
|
29
|
-
|
|
30
28
|
export interface OutdatedSymlink {
|
|
31
29
|
/** Path to the symlink in the target */
|
|
32
30
|
symlinkPath: string;
|
|
@@ -38,6 +36,8 @@ export interface OutdatedSymlink {
|
|
|
38
36
|
expectedTarget: string;
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
type PartialClassification = Partial<AgentFileClassification>;
|
|
40
|
+
|
|
41
41
|
/** Find all agent files in the repository and classify them.
|
|
42
42
|
* Returns absolute paths in the classification.
|
|
43
43
|
*/
|
|
@@ -61,14 +61,10 @@ export async function classifyAgentFiles(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** @return true if classification has any problems */
|
|
64
|
-
export function agentFileProblems(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
classification.tracked.length > 0 ||
|
|
69
|
-
classification.untracked.length > 0 ||
|
|
70
|
-
classification.staleSymlinks.length > 0 ||
|
|
71
|
-
classification.outdatedSymlinks.length > 0
|
|
64
|
+
export function agentFileProblems(c: AgentFileClassification): boolean {
|
|
65
|
+
const { tracked, untracked, staleSymlinks, outdatedSymlinks } = c;
|
|
66
|
+
return [tracked, untracked, staleSymlinks, outdatedSymlinks].some(
|
|
67
|
+
(a) => a.length > 0,
|
|
72
68
|
);
|
|
73
69
|
}
|
|
74
70
|
|
|
@@ -110,10 +106,12 @@ ${commands.join("\n")}`);
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
if (classified.outdatedSymlinks.length > 0) {
|
|
113
|
-
const details = classified.outdatedSymlinks.map(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
109
|
+
const details = classified.outdatedSymlinks.map(
|
|
110
|
+
(s) =>
|
|
111
|
+
` ${rel(s.symlinkPath)}\n` +
|
|
112
|
+
` points to: ${s.currentTarget}\n` +
|
|
113
|
+
` expected: ${s.expectedTarget}`,
|
|
114
|
+
);
|
|
117
115
|
sections.push(`Found outdated agent symlinks (pointing to wrong overlay path).
|
|
118
116
|
|
|
119
117
|
This typically happens after a directory rename. Remove symlinks and run \`clank link\`:
|
|
@@ -127,6 +125,21 @@ To fix:
|
|
|
127
125
|
return sections.join("\n\n");
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
/** Find all agent files in the repository */
|
|
129
|
+
async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
|
|
130
|
+
const files: string[] = [];
|
|
131
|
+
const agentFileSet = new Set(agentFiles);
|
|
132
|
+
|
|
133
|
+
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
134
|
+
if (isDirectory) continue;
|
|
135
|
+
if (agentFileSet.has(basename(path))) {
|
|
136
|
+
files.push(path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return files;
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
/** Classify a single agent file */
|
|
131
144
|
async function classifySingleAgentFile(
|
|
132
145
|
filePath: string,
|
|
@@ -146,6 +159,18 @@ async function classifySingleAgentFile(
|
|
|
146
159
|
return {};
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
/** Merge sparse classifications into a complete classification with arrays */
|
|
163
|
+
function mergeClassifications(
|
|
164
|
+
items: PartialClassification[],
|
|
165
|
+
): AgentFileClassification {
|
|
166
|
+
return {
|
|
167
|
+
tracked: items.flatMap((i) => i.tracked ?? []),
|
|
168
|
+
untracked: items.flatMap((i) => i.untracked ?? []),
|
|
169
|
+
staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
|
|
170
|
+
outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
/** Classify an agent symlink - check if stale or outdated */
|
|
150
175
|
async function classifyAgentSymlink(
|
|
151
176
|
filePath: string,
|
|
@@ -165,44 +190,14 @@ async function classifyAgentSymlink(
|
|
|
165
190
|
const agentsMdPath = join(dirname(filePath), "agents.md");
|
|
166
191
|
const expectedTarget = targetToOverlay(agentsMdPath, "project", mapperCtx);
|
|
167
192
|
if (absoluteTarget !== expectedTarget) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
currentTarget: absoluteTarget,
|
|
173
|
-
expectedTarget,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
193
|
+
const symlink = {
|
|
194
|
+
symlinkPath: filePath,
|
|
195
|
+
currentTarget: absoluteTarget,
|
|
196
|
+
expectedTarget,
|
|
176
197
|
};
|
|
198
|
+
return { outdatedSymlinks: [symlink] };
|
|
177
199
|
}
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
return {};
|
|
181
203
|
}
|
|
182
|
-
|
|
183
|
-
/** Merge sparse classifications into a complete classification with arrays */
|
|
184
|
-
function mergeClassifications(
|
|
185
|
-
items: PartialClassification[],
|
|
186
|
-
): AgentFileClassification {
|
|
187
|
-
return {
|
|
188
|
-
tracked: items.flatMap((i) => i.tracked ?? []),
|
|
189
|
-
untracked: items.flatMap((i) => i.untracked ?? []),
|
|
190
|
-
staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
|
|
191
|
-
outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Find all agent files in the repository */
|
|
196
|
-
async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
|
|
197
|
-
const files: string[] = [];
|
|
198
|
-
const agentFileSet = new Set(agentFiles);
|
|
199
|
-
|
|
200
|
-
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
201
|
-
if (isDirectory) continue;
|
|
202
|
-
if (agentFileSet.has(basename(path))) {
|
|
203
|
-
files.push(path);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return files;
|
|
208
|
-
}
|
package/src/Cli.ts
CHANGED
|
@@ -93,6 +93,24 @@ function registerCoreCommands(program: Command): void {
|
|
|
93
93
|
registerUtilityCommands(program);
|
|
94
94
|
}
|
|
95
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));
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
function registerOverlayCommands(program: Command): void {
|
|
97
115
|
program
|
|
98
116
|
.command("init")
|
|
@@ -115,9 +133,10 @@ function registerOverlayCommands(program: Command): void {
|
|
|
115
133
|
.command("add")
|
|
116
134
|
.description("Add file(s) to overlay and create symlinks")
|
|
117
135
|
.argument(
|
|
118
|
-
"
|
|
136
|
+
"[files...]",
|
|
119
137
|
"File path(s) (e.g., style.md, .claude/commands/review.md)",
|
|
120
138
|
)
|
|
139
|
+
.option("-i, --interactive", "Interactively add all unadded files")
|
|
121
140
|
.option("-g, --global", "Add to global location (all projects)")
|
|
122
141
|
.option("-p, --project", "Add to project location (default)")
|
|
123
142
|
.option("-w, --worktree", "Add to worktree location (this branch only)")
|
|
@@ -155,9 +174,63 @@ function registerUtilityCommands(program: Command): void {
|
|
|
155
174
|
.action(withErrorHandling(vscodeCommand));
|
|
156
175
|
}
|
|
157
176
|
|
|
177
|
+
function withErrorHandling<T extends unknown[]>(
|
|
178
|
+
fn: (...args: T) => Promise<void>,
|
|
179
|
+
): (...args: T) => Promise<void> {
|
|
180
|
+
return async (...args: T) => {
|
|
181
|
+
try {
|
|
182
|
+
await fn(...args);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function registerRmCommand(program: Command): void {
|
|
191
|
+
program
|
|
192
|
+
.command("rm")
|
|
193
|
+
.alias("remove")
|
|
194
|
+
.description("Remove file(s) from overlay and target")
|
|
195
|
+
.argument("<files...>", "File(s) to remove")
|
|
196
|
+
.option("-g, --global", "Remove from global scope")
|
|
197
|
+
.option("-p, --project", "Remove from project scope")
|
|
198
|
+
.option("-w, --worktree", "Remove from worktree scope")
|
|
199
|
+
.action(withErrorHandling(rmCommand));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function registerMvCommand(program: Command): void {
|
|
203
|
+
const cmd = program
|
|
204
|
+
.command("mv")
|
|
205
|
+
.alias("move")
|
|
206
|
+
.description("Move or rename file(s) in overlay")
|
|
207
|
+
.argument("<files...>", "File(s) to move");
|
|
208
|
+
|
|
209
|
+
cmd.addOption(
|
|
210
|
+
new Option("-g, --global", "Move to global scope").conflicts([
|
|
211
|
+
"project",
|
|
212
|
+
"worktree",
|
|
213
|
+
]),
|
|
214
|
+
);
|
|
215
|
+
cmd.addOption(
|
|
216
|
+
new Option("-p, --project", "Move to project scope").conflicts([
|
|
217
|
+
"global",
|
|
218
|
+
"worktree",
|
|
219
|
+
]),
|
|
220
|
+
);
|
|
221
|
+
cmd.addOption(
|
|
222
|
+
new Option("-w, --worktree", "Move to worktree scope").conflicts([
|
|
223
|
+
"global",
|
|
224
|
+
"project",
|
|
225
|
+
]),
|
|
226
|
+
);
|
|
227
|
+
cmd.action(withErrorHandling(moveCommand));
|
|
228
|
+
}
|
|
229
|
+
|
|
158
230
|
function registerFilesCommand(program: Command): void {
|
|
159
231
|
const files = program
|
|
160
232
|
.command("files")
|
|
233
|
+
.alias("list")
|
|
161
234
|
.description("List clank-managed files (paths relative to cwd)")
|
|
162
235
|
.argument(
|
|
163
236
|
"[path]",
|
|
@@ -201,74 +274,3 @@ function registerFilesCommand(program: Command): void {
|
|
|
201
274
|
|
|
202
275
|
files.action(withErrorHandling(filesCommand));
|
|
203
276
|
}
|
|
204
|
-
|
|
205
|
-
function registerHelpCommands(program: Command): void {
|
|
206
|
-
const help = program
|
|
207
|
-
.command("help")
|
|
208
|
-
.description("Show help information")
|
|
209
|
-
.argument("[command]", "Command to show help for")
|
|
210
|
-
.action((commandName?: string) => {
|
|
211
|
-
if (!commandName) return program.help();
|
|
212
|
-
const subcommand = program.commands.find((c) => c.name() === commandName);
|
|
213
|
-
if (subcommand) return subcommand.help();
|
|
214
|
-
console.error(`Unknown command: ${commandName}`);
|
|
215
|
-
process.exit(1);
|
|
216
|
-
});
|
|
217
|
-
help
|
|
218
|
-
.command("structure")
|
|
219
|
-
.description("Show overlay directory structure")
|
|
220
|
-
.action(() => console.log(structureHelp));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function registerRmCommand(program: Command): void {
|
|
224
|
-
program
|
|
225
|
-
.command("rm")
|
|
226
|
-
.alias("remove")
|
|
227
|
-
.description("Remove file(s) from overlay and target")
|
|
228
|
-
.argument("<files...>", "File(s) to remove")
|
|
229
|
-
.option("-g, --global", "Remove from global scope")
|
|
230
|
-
.option("-p, --project", "Remove from project scope")
|
|
231
|
-
.option("-w, --worktree", "Remove from worktree scope")
|
|
232
|
-
.action(withErrorHandling(rmCommand));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function registerMvCommand(program: Command): void {
|
|
236
|
-
const cmd = program
|
|
237
|
-
.command("mv")
|
|
238
|
-
.alias("move")
|
|
239
|
-
.description("Move file(s) between overlay scopes")
|
|
240
|
-
.argument("<files...>", "File(s) to move");
|
|
241
|
-
|
|
242
|
-
cmd.addOption(
|
|
243
|
-
new Option("-g, --global", "Move to global scope").conflicts([
|
|
244
|
-
"project",
|
|
245
|
-
"worktree",
|
|
246
|
-
]),
|
|
247
|
-
);
|
|
248
|
-
cmd.addOption(
|
|
249
|
-
new Option("-p, --project", "Move to project scope").conflicts([
|
|
250
|
-
"global",
|
|
251
|
-
"worktree",
|
|
252
|
-
]),
|
|
253
|
-
);
|
|
254
|
-
cmd.addOption(
|
|
255
|
-
new Option("-w, --worktree", "Move to worktree scope").conflicts([
|
|
256
|
-
"global",
|
|
257
|
-
"project",
|
|
258
|
-
]),
|
|
259
|
-
);
|
|
260
|
-
cmd.action(withErrorHandling(moveCommand));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function withErrorHandling<T extends unknown[]>(
|
|
264
|
-
fn: (...args: T) => Promise<void>,
|
|
265
|
-
): (...args: T) => Promise<void> {
|
|
266
|
-
return async (...args: T) => {
|
|
267
|
-
try {
|
|
268
|
-
await fn(...args);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
271
|
-
process.exit(1);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
}
|
package/src/Config.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { join } from "node:path";
|
|
|
4
4
|
import { cosmiconfig } from "cosmiconfig";
|
|
5
5
|
import { fileExists } from "./FsUtil.ts";
|
|
6
6
|
|
|
7
|
-
export const defaultOverlayDir = "clankover";
|
|
8
|
-
|
|
9
7
|
export interface ClankConfig {
|
|
10
8
|
overlayRepo: string;
|
|
11
9
|
agents: string[];
|
|
@@ -17,6 +15,8 @@ export interface ClankConfig {
|
|
|
17
15
|
ignore?: string[];
|
|
18
16
|
}
|
|
19
17
|
|
|
18
|
+
export const defaultOverlayDir = "clankover";
|
|
19
|
+
|
|
20
20
|
const defaultConfig: ClankConfig = {
|
|
21
21
|
overlayRepo: join(homedir(), defaultOverlayDir),
|
|
22
22
|
agents: ["agents", "claude", "gemini"],
|
|
@@ -30,14 +30,11 @@ export function setConfigPath(path: string | undefined): void {
|
|
|
30
30
|
customConfigPath = path;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Load global clank configuration from ~/.config/clank/config.js or similar */
|
|
33
|
+
/** Load global clank configuration from ~/.config/clank/clank.config.js or similar */
|
|
34
34
|
export async function loadConfig(): Promise<ClankConfig> {
|
|
35
35
|
if (customConfigPath) {
|
|
36
36
|
const result = await explorer.load(customConfigPath);
|
|
37
|
-
if (!result) {
|
|
38
|
-
throw new Error(`Config file not found: ${customConfigPath}`);
|
|
39
|
-
}
|
|
40
|
-
if (result.isEmpty) {
|
|
37
|
+
if (!result || result.isEmpty) {
|
|
41
38
|
return defaultConfig;
|
|
42
39
|
}
|
|
43
40
|
return { ...defaultConfig, ...result.config };
|
|
@@ -50,10 +47,10 @@ export async function loadConfig(): Promise<ClankConfig> {
|
|
|
50
47
|
return { ...defaultConfig, ...result.config };
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
/** Create default configuration file at ~/.config/clank/config.js */
|
|
50
|
+
/** Create default configuration file at ~/.config/clank/clank.config.js */
|
|
54
51
|
export async function createDefaultConfig(overlayRepo?: string): Promise<void> {
|
|
55
52
|
const configDir = getConfigDir();
|
|
56
|
-
const configPath = customConfigPath || join(configDir, "config.js");
|
|
53
|
+
const configPath = customConfigPath || join(configDir, "clank.config.js");
|
|
57
54
|
|
|
58
55
|
const config = {
|
|
59
56
|
...defaultConfig,
|
|
@@ -84,12 +81,6 @@ export async function getOverlayPath(): Promise<string> {
|
|
|
84
81
|
return expandPath(config.overlayRepo);
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
/** Get the XDG config directory (respects XDG_CONFIG_HOME, defaults to ~/.config/clank) */
|
|
88
|
-
function getConfigDir(): string {
|
|
89
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
90
|
-
return join(xdgConfig, "clank");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
84
|
/** Validate overlay repository exists, throw if not */
|
|
94
85
|
export async function validateOverlayExists(
|
|
95
86
|
overlayRoot: string,
|
|
@@ -100,3 +91,9 @@ export async function validateOverlayExists(
|
|
|
100
91
|
);
|
|
101
92
|
}
|
|
102
93
|
}
|
|
94
|
+
|
|
95
|
+
/** Get the XDG config directory (respects XDG_CONFIG_HOME, defaults to ~/.config/clank) */
|
|
96
|
+
function getConfigDir(): string {
|
|
97
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
98
|
+
return join(xdgConfig, "clank");
|
|
99
|
+
}
|
package/src/Exclude.ts
CHANGED
|
@@ -102,15 +102,6 @@ export async function removeGitExcludes(targetRoot: string): Promise<void> {
|
|
|
102
102
|
console.log("Removed clank entries from .git/info/exclude");
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/** Remove the clank section from exclude file content */
|
|
106
|
-
function removeClankSection(content: string): string {
|
|
107
|
-
const pattern = new RegExp(
|
|
108
|
-
`\\n*${clankMarkerStart}[\\s\\S]*?${clankMarkerEnd}\\n*`,
|
|
109
|
-
"g",
|
|
110
|
-
);
|
|
111
|
-
return content.replace(pattern, "\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
105
|
/** Filter out clank section from lines */
|
|
115
106
|
export function filterClankLines(lines: string[]): string[] {
|
|
116
107
|
const result: string[] = [];
|
|
@@ -129,6 +120,15 @@ export function filterClankLines(lines: string[]): string[] {
|
|
|
129
120
|
return result;
|
|
130
121
|
}
|
|
131
122
|
|
|
123
|
+
/** Remove the clank section from exclude file content */
|
|
124
|
+
function removeClankSection(content: string): string {
|
|
125
|
+
const pattern = new RegExp(
|
|
126
|
+
`\\n*${clankMarkerStart}[\\s\\S]*?${clankMarkerEnd}\\n*`,
|
|
127
|
+
"g",
|
|
128
|
+
);
|
|
129
|
+
return content.replace(pattern, "\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
132
|
/** Check if a directory has any tracked files */
|
|
133
133
|
async function hasTrackedFiles(
|
|
134
134
|
dirPath: string,
|
package/src/Git.ts
CHANGED
|
@@ -115,16 +115,6 @@ export function parseRepoName(url: string): string | null {
|
|
|
115
115
|
return basename(normalizedUrl);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/** Execute a git command and return stdout, or null if it fails */
|
|
119
|
-
async function gitCommand(args: string, cwd?: string): Promise<string | null> {
|
|
120
|
-
try {
|
|
121
|
-
const { stdout } = await exec(`git ${args}`, { cwd });
|
|
122
|
-
return stdout.trim();
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
118
|
/** Get the .git directory for the current worktree */
|
|
129
119
|
export async function getGitDir(cwd: string): Promise<string | null> {
|
|
130
120
|
const gitDir = await gitCommand("rev-parse --git-dir", cwd);
|
|
@@ -138,3 +128,13 @@ export async function getGitCommonDir(cwd: string): Promise<string | null> {
|
|
|
138
128
|
if (!gitDir) return null;
|
|
139
129
|
return isAbsolute(gitDir) ? gitDir : join(cwd, gitDir);
|
|
140
130
|
}
|
|
131
|
+
|
|
132
|
+
/** Execute a git command and return stdout, or null if it fails */
|
|
133
|
+
async function gitCommand(args: string, cwd?: string): Promise<string | null> {
|
|
134
|
+
try {
|
|
135
|
+
const { stdout } = await exec(`git ${args}`, { cwd });
|
|
136
|
+
return stdout.trim();
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/Gitignore.ts
CHANGED
|
@@ -99,24 +99,16 @@ export function isClankPattern(glob: string): boolean {
|
|
|
99
99
|
const normalized = glob.replace(/^\*\*\//, "").replace(/\/$/, "");
|
|
100
100
|
|
|
101
101
|
// Check against managed directories
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
) {
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
102
|
+
const matchesDir = (dir: string) =>
|
|
103
|
+
normalized === dir ||
|
|
104
|
+
normalized.startsWith(`${dir}/`) ||
|
|
105
|
+
normalized.endsWith(`/${dir}`);
|
|
106
|
+
if (targetManagedDirs.some(matchesDir)) return true;
|
|
111
107
|
|
|
112
108
|
// Check against agent files
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return false;
|
|
109
|
+
const matchesAgent = (f: string) =>
|
|
110
|
+
normalized === f || normalized.endsWith(`/${f}`);
|
|
111
|
+
return agentFiles.some(matchesAgent);
|
|
120
112
|
}
|
|
121
113
|
|
|
122
114
|
/** Convert collected patterns to VS Code exclude globs, filtering clank patterns */
|
|
@@ -143,14 +135,11 @@ export function deduplicateGlobs(globs: string[]): string[] {
|
|
|
143
135
|
const coveredSuffixes = new Set(universal.map((g) => g.slice(3)));
|
|
144
136
|
|
|
145
137
|
// Keep specific patterns not covered by a universal pattern
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
return true;
|
|
153
|
-
});
|
|
138
|
+
const isCovered = (glob: string) =>
|
|
139
|
+
[...coveredSuffixes].some(
|
|
140
|
+
(s) => glob.endsWith(`/**/${s}`) || glob.endsWith(`/${s}`),
|
|
141
|
+
);
|
|
142
|
+
const uncovered = specific.filter((glob) => !isCovered(glob));
|
|
154
143
|
|
|
155
144
|
return [...universal, ...uncovered];
|
|
156
145
|
}
|
|
@@ -169,22 +158,19 @@ async function parseGitignoreFile(
|
|
|
169
158
|
result.negationWarnings.push(...parsed.negationWarnings);
|
|
170
159
|
}
|
|
171
160
|
|
|
172
|
-
/**
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
basePath: string,
|
|
177
|
-
): { pattern?: GitignorePattern; negation?: string } {
|
|
178
|
-
// Skip empty lines and comments
|
|
179
|
-
if (!trimmed || trimmed.startsWith("#")) return {};
|
|
180
|
-
|
|
181
|
-
const isNegation = trimmed.startsWith("!");
|
|
182
|
-
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
161
|
+
/** Find all nested .gitignore files (excluding root) */
|
|
162
|
+
async function findNestedGitignores(targetRoot: string): Promise<string[]> {
|
|
163
|
+
const gitignores: string[] = [];
|
|
164
|
+
const rootGitignore = join(targetRoot, ".gitignore");
|
|
183
165
|
|
|
184
|
-
|
|
185
|
-
|
|
166
|
+
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
167
|
+
if (isDirectory) continue;
|
|
168
|
+
if (basename(path) === ".gitignore" && path !== rootGitignore) {
|
|
169
|
+
gitignores.push(path);
|
|
170
|
+
}
|
|
186
171
|
}
|
|
187
|
-
|
|
172
|
+
|
|
173
|
+
return gitignores;
|
|
188
174
|
}
|
|
189
175
|
|
|
190
176
|
/** Parse gitignore file content into patterns */
|
|
@@ -211,17 +197,20 @@ function parseGitignoreContent(
|
|
|
211
197
|
return { patterns, negationWarnings };
|
|
212
198
|
}
|
|
213
199
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
200
|
+
/** Parse a single gitignore line */
|
|
201
|
+
function parseLine(
|
|
202
|
+
trimmed: string,
|
|
203
|
+
source: string,
|
|
204
|
+
basePath: string,
|
|
205
|
+
): { pattern?: GitignorePattern; negation?: string } {
|
|
206
|
+
// Skip empty lines and comments
|
|
207
|
+
if (!trimmed || trimmed.startsWith("#")) return {};
|
|
218
208
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (basename(path) === ".gitignore" && path !== rootGitignore) {
|
|
222
|
-
gitignores.push(path);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
209
|
+
const isNegation = trimmed.startsWith("!");
|
|
210
|
+
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
225
211
|
|
|
226
|
-
|
|
212
|
+
if (isNegation) {
|
|
213
|
+
return { negation: pattern };
|
|
214
|
+
}
|
|
215
|
+
return { pattern: { pattern, basePath, negation: false, source } };
|
|
227
216
|
}
|