clank-cli 0.1.61 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/ClassifyFiles.ts +29 -29
- package/src/Cli.ts +72 -71
- package/src/Config.ts +12 -15
- package/src/Exclude.ts +9 -9
- package/src/Git.ts +10 -10
- package/src/Gitignore.ts +25 -25
- package/src/Mapper.ts +71 -71
- package/src/OverlayGit.ts +30 -3
- package/src/commands/Add.ts +107 -107
- 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 +16 -16
- package/src/commands/Rm.ts +29 -29
- package/src/commands/VsCode.ts +24 -24
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ clank help structure
|
|
|
205
205
|
|
|
206
206
|
### `--config <path>` (global option)
|
|
207
207
|
|
|
208
|
-
Specify a custom config file location (default `~/.config/clank/config.js`).
|
|
208
|
+
Specify a custom config file location (default `~/.config/clank/clank.config.js`).
|
|
209
209
|
|
|
210
210
|
```bash
|
|
211
211
|
clank --config /tmp/test-config.js init /tmp/test-overlay
|
|
@@ -243,7 +243,7 @@ clank/
|
|
|
243
243
|
|
|
244
244
|
## Configuration
|
|
245
245
|
|
|
246
|
-
Global configuration is stored
|
|
246
|
+
Global configuration is stored in `~/.config/clank/clank.config.js`:
|
|
247
247
|
|
|
248
248
|
```javascript
|
|
249
249
|
export default {
|
package/package.json
CHANGED
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
|
*/
|
|
@@ -127,6 +127,21 @@ To fix:
|
|
|
127
127
|
return sections.join("\n\n");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Find all agent files in the repository */
|
|
131
|
+
async function findAllAgentFiles(targetRoot: string): Promise<string[]> {
|
|
132
|
+
const files: string[] = [];
|
|
133
|
+
const agentFileSet = new Set(agentFiles);
|
|
134
|
+
|
|
135
|
+
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
136
|
+
if (isDirectory) continue;
|
|
137
|
+
if (agentFileSet.has(basename(path))) {
|
|
138
|
+
files.push(path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/** Classify a single agent file */
|
|
131
146
|
async function classifySingleAgentFile(
|
|
132
147
|
filePath: string,
|
|
@@ -146,6 +161,18 @@ async function classifySingleAgentFile(
|
|
|
146
161
|
return {};
|
|
147
162
|
}
|
|
148
163
|
|
|
164
|
+
/** Merge sparse classifications into a complete classification with arrays */
|
|
165
|
+
function mergeClassifications(
|
|
166
|
+
items: PartialClassification[],
|
|
167
|
+
): AgentFileClassification {
|
|
168
|
+
return {
|
|
169
|
+
tracked: items.flatMap((i) => i.tracked ?? []),
|
|
170
|
+
untracked: items.flatMap((i) => i.untracked ?? []),
|
|
171
|
+
staleSymlinks: items.flatMap((i) => i.staleSymlinks ?? []),
|
|
172
|
+
outdatedSymlinks: items.flatMap((i) => i.outdatedSymlinks ?? []),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
/** Classify an agent symlink - check if stale or outdated */
|
|
150
177
|
async function classifyAgentSymlink(
|
|
151
178
|
filePath: string,
|
|
@@ -179,30 +206,3 @@ async function classifyAgentSymlink(
|
|
|
179
206
|
|
|
180
207
|
return {};
|
|
181
208
|
}
|
|
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")
|
|
@@ -155,9 +173,63 @@ function registerUtilityCommands(program: Command): void {
|
|
|
155
173
|
.action(withErrorHandling(vscodeCommand));
|
|
156
174
|
}
|
|
157
175
|
|
|
176
|
+
function withErrorHandling<T extends unknown[]>(
|
|
177
|
+
fn: (...args: T) => Promise<void>,
|
|
178
|
+
): (...args: T) => Promise<void> {
|
|
179
|
+
return async (...args: T) => {
|
|
180
|
+
try {
|
|
181
|
+
await fn(...args);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function registerRmCommand(program: Command): void {
|
|
190
|
+
program
|
|
191
|
+
.command("rm")
|
|
192
|
+
.alias("remove")
|
|
193
|
+
.description("Remove file(s) from overlay and target")
|
|
194
|
+
.argument("<files...>", "File(s) to remove")
|
|
195
|
+
.option("-g, --global", "Remove from global scope")
|
|
196
|
+
.option("-p, --project", "Remove from project scope")
|
|
197
|
+
.option("-w, --worktree", "Remove from worktree scope")
|
|
198
|
+
.action(withErrorHandling(rmCommand));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function registerMvCommand(program: Command): void {
|
|
202
|
+
const cmd = program
|
|
203
|
+
.command("mv")
|
|
204
|
+
.alias("move")
|
|
205
|
+
.description("Move file(s) between overlay scopes")
|
|
206
|
+
.argument("<files...>", "File(s) to move");
|
|
207
|
+
|
|
208
|
+
cmd.addOption(
|
|
209
|
+
new Option("-g, --global", "Move to global scope").conflicts([
|
|
210
|
+
"project",
|
|
211
|
+
"worktree",
|
|
212
|
+
]),
|
|
213
|
+
);
|
|
214
|
+
cmd.addOption(
|
|
215
|
+
new Option("-p, --project", "Move to project scope").conflicts([
|
|
216
|
+
"global",
|
|
217
|
+
"worktree",
|
|
218
|
+
]),
|
|
219
|
+
);
|
|
220
|
+
cmd.addOption(
|
|
221
|
+
new Option("-w, --worktree", "Move to worktree scope").conflicts([
|
|
222
|
+
"global",
|
|
223
|
+
"project",
|
|
224
|
+
]),
|
|
225
|
+
);
|
|
226
|
+
cmd.action(withErrorHandling(moveCommand));
|
|
227
|
+
}
|
|
228
|
+
|
|
158
229
|
function registerFilesCommand(program: Command): void {
|
|
159
230
|
const files = program
|
|
160
231
|
.command("files")
|
|
232
|
+
.alias("list")
|
|
161
233
|
.description("List clank-managed files (paths relative to cwd)")
|
|
162
234
|
.argument(
|
|
163
235
|
"[path]",
|
|
@@ -201,74 +273,3 @@ function registerFilesCommand(program: Command): void {
|
|
|
201
273
|
|
|
202
274
|
files.action(withErrorHandling(filesCommand));
|
|
203
275
|
}
|
|
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
|
@@ -169,22 +169,19 @@ async function parseGitignoreFile(
|
|
|
169
169
|
result.negationWarnings.push(...parsed.negationWarnings);
|
|
170
170
|
}
|
|
171
171
|
|
|
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;
|
|
172
|
+
/** Find all nested .gitignore files (excluding root) */
|
|
173
|
+
async function findNestedGitignores(targetRoot: string): Promise<string[]> {
|
|
174
|
+
const gitignores: string[] = [];
|
|
175
|
+
const rootGitignore = join(targetRoot, ".gitignore");
|
|
183
176
|
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
|
|
178
|
+
if (isDirectory) continue;
|
|
179
|
+
if (basename(path) === ".gitignore" && path !== rootGitignore) {
|
|
180
|
+
gitignores.push(path);
|
|
181
|
+
}
|
|
186
182
|
}
|
|
187
|
-
|
|
183
|
+
|
|
184
|
+
return gitignores;
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
/** Parse gitignore file content into patterns */
|
|
@@ -211,17 +208,20 @@ function parseGitignoreContent(
|
|
|
211
208
|
return { patterns, negationWarnings };
|
|
212
209
|
}
|
|
213
210
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
/** Parse a single gitignore line */
|
|
212
|
+
function parseLine(
|
|
213
|
+
trimmed: string,
|
|
214
|
+
source: string,
|
|
215
|
+
basePath: string,
|
|
216
|
+
): { pattern?: GitignorePattern; negation?: string } {
|
|
217
|
+
// Skip empty lines and comments
|
|
218
|
+
if (!trimmed || trimmed.startsWith("#")) return {};
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (basename(path) === ".gitignore" && path !== rootGitignore) {
|
|
222
|
-
gitignores.push(path);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
220
|
+
const isNegation = trimmed.startsWith("!");
|
|
221
|
+
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
225
222
|
|
|
226
|
-
|
|
223
|
+
if (isNegation) {
|
|
224
|
+
return { negation: pattern };
|
|
225
|
+
}
|
|
226
|
+
return { pattern: { pattern, basePath, negation: false, source } };
|
|
227
227
|
}
|
package/src/Mapper.ts
CHANGED
|
@@ -12,6 +12,19 @@ export interface ScopeOptions {
|
|
|
12
12
|
worktree?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Result of mapping an overlay path to a target path */
|
|
16
|
+
export interface TargetMapping {
|
|
17
|
+
targetPath: string;
|
|
18
|
+
scope: Scope;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** params for mapping from the overlay repo to the target project repo */
|
|
22
|
+
export interface MapperContext {
|
|
23
|
+
overlayRoot: string;
|
|
24
|
+
targetRoot: string;
|
|
25
|
+
gitContext: GitContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/** Resolve scope from CLI options
|
|
16
29
|
* @param options - The CLI options
|
|
17
30
|
* @param defaultScope - Default scope if none specified, or "require" to throw
|
|
@@ -32,19 +45,6 @@ export function resolveScopeFromOptions(
|
|
|
32
45
|
return defaultScope;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
/** Result of mapping an overlay path to a target path */
|
|
36
|
-
export interface TargetMapping {
|
|
37
|
-
targetPath: string;
|
|
38
|
-
scope: Scope;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** params for mapping from the overlay repo to the target project repo */
|
|
42
|
-
export interface MapperContext {
|
|
43
|
-
overlayRoot: string;
|
|
44
|
-
targetRoot: string;
|
|
45
|
-
gitContext: GitContext;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
48
|
/** Get overlay path for a project: overlay/targets/{projectName} */
|
|
49
49
|
export function overlayProjectDir(
|
|
50
50
|
overlayRoot: string,
|
|
@@ -132,34 +132,6 @@ export function targetToOverlay(
|
|
|
132
132
|
return encodeTargetPath(relPath, overlayBase);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** Encode a target-relative path to an overlay path */
|
|
136
|
-
function encodeTargetPath(relPath: string, overlayBase: string): string {
|
|
137
|
-
// agents.md stays at natural path
|
|
138
|
-
if (basename(relPath) === "agents.md") {
|
|
139
|
-
return join(overlayBase, relPath);
|
|
140
|
-
}
|
|
141
|
-
// .claude/prompts/ and .gemini/prompts/ → prompts/ in overlay (agent-agnostic)
|
|
142
|
-
for (const agentDir of managedAgentDirs) {
|
|
143
|
-
const prefix = `${agentDir}/prompts/`;
|
|
144
|
-
if (relPath.startsWith(prefix)) {
|
|
145
|
-
return join(overlayBase, "prompts", relPath.slice(prefix.length));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// .claude/* and .gemini/* → claude/*, gemini/* in overlay (agent-specific)
|
|
149
|
-
for (const agentDir of managedAgentDirs) {
|
|
150
|
-
if (relPath.startsWith(`${agentDir}/`)) {
|
|
151
|
-
const subPath = relPath.slice(agentDir.length + 1);
|
|
152
|
-
return join(overlayBase, agentDir.slice(1), subPath); // strip leading dot
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Files with clank/ in path → preserve structure
|
|
156
|
-
if (isClankPath(relPath)) {
|
|
157
|
-
return join(overlayBase, relPath);
|
|
158
|
-
}
|
|
159
|
-
// Plain files → add clank/ prefix
|
|
160
|
-
return join(overlayBase, "clank", relPath);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
135
|
/**
|
|
164
136
|
* Normalize file path argument from clank add command
|
|
165
137
|
* All files go to clank/ in target (except .claude/ files and agent files)
|
|
@@ -276,6 +248,64 @@ function mapGlobalOverlay(
|
|
|
276
248
|
return decodeOverlayPath(relPath, targetRoot, "global");
|
|
277
249
|
}
|
|
278
250
|
|
|
251
|
+
/** Map project overlay files to target */
|
|
252
|
+
function mapProjectOverlay(
|
|
253
|
+
overlayPath: string,
|
|
254
|
+
projectPrefix: string,
|
|
255
|
+
context: MapperContext,
|
|
256
|
+
): TargetMapping | null {
|
|
257
|
+
const { targetRoot, gitContext } = context;
|
|
258
|
+
const relPath = relative(projectPrefix, overlayPath);
|
|
259
|
+
|
|
260
|
+
// Worktree-specific files
|
|
261
|
+
const worktreePrefix = join("worktrees", gitContext.worktreeName);
|
|
262
|
+
if (relPath.startsWith(`${worktreePrefix}/`)) {
|
|
263
|
+
const innerPath = relative(worktreePrefix, relPath);
|
|
264
|
+
return decodeOverlayPath(innerPath, targetRoot, "worktree");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Skip other worktrees
|
|
268
|
+
if (relPath.startsWith("worktrees/")) return null;
|
|
269
|
+
|
|
270
|
+
// Project settings.json (project-only, before shared logic)
|
|
271
|
+
if (relPath === "claude/settings.json") {
|
|
272
|
+
return {
|
|
273
|
+
targetPath: join(targetRoot, ".claude/settings.json"),
|
|
274
|
+
scope: "project",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return decodeOverlayPath(relPath, targetRoot, "project");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Encode a target-relative path to an overlay path */
|
|
282
|
+
function encodeTargetPath(relPath: string, overlayBase: string): string {
|
|
283
|
+
// agents.md stays at natural path
|
|
284
|
+
if (basename(relPath) === "agents.md") {
|
|
285
|
+
return join(overlayBase, relPath);
|
|
286
|
+
}
|
|
287
|
+
// .claude/prompts/ and .gemini/prompts/ → prompts/ in overlay (agent-agnostic)
|
|
288
|
+
for (const agentDir of managedAgentDirs) {
|
|
289
|
+
const prefix = `${agentDir}/prompts/`;
|
|
290
|
+
if (relPath.startsWith(prefix)) {
|
|
291
|
+
return join(overlayBase, "prompts", relPath.slice(prefix.length));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// .claude/* and .gemini/* → claude/*, gemini/* in overlay (agent-specific)
|
|
295
|
+
for (const agentDir of managedAgentDirs) {
|
|
296
|
+
if (relPath.startsWith(`${agentDir}/`)) {
|
|
297
|
+
const subPath = relPath.slice(agentDir.length + 1);
|
|
298
|
+
return join(overlayBase, agentDir.slice(1), subPath); // strip leading dot
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Files with clank/ in path → preserve structure
|
|
302
|
+
if (isClankPath(relPath)) {
|
|
303
|
+
return join(overlayBase, relPath);
|
|
304
|
+
}
|
|
305
|
+
// Plain files → add clank/ prefix
|
|
306
|
+
return join(overlayBase, "clank", relPath);
|
|
307
|
+
}
|
|
308
|
+
|
|
279
309
|
/** Decode an overlay-relative path to target (shared by all scopes) */
|
|
280
310
|
function decodeOverlayPath(
|
|
281
311
|
relPath: string,
|
|
@@ -312,33 +342,3 @@ function decodeOverlayPath(
|
|
|
312
342
|
|
|
313
343
|
return null;
|
|
314
344
|
}
|
|
315
|
-
|
|
316
|
-
/** Map project overlay files to target */
|
|
317
|
-
function mapProjectOverlay(
|
|
318
|
-
overlayPath: string,
|
|
319
|
-
projectPrefix: string,
|
|
320
|
-
context: MapperContext,
|
|
321
|
-
): TargetMapping | null {
|
|
322
|
-
const { targetRoot, gitContext } = context;
|
|
323
|
-
const relPath = relative(projectPrefix, overlayPath);
|
|
324
|
-
|
|
325
|
-
// Worktree-specific files
|
|
326
|
-
const worktreePrefix = join("worktrees", gitContext.worktreeName);
|
|
327
|
-
if (relPath.startsWith(`${worktreePrefix}/`)) {
|
|
328
|
-
const innerPath = relative(worktreePrefix, relPath);
|
|
329
|
-
return decodeOverlayPath(innerPath, targetRoot, "worktree");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Skip other worktrees
|
|
333
|
-
if (relPath.startsWith("worktrees/")) return null;
|
|
334
|
-
|
|
335
|
-
// Project settings.json (project-only, before shared logic)
|
|
336
|
-
if (relPath === "claude/settings.json") {
|
|
337
|
-
return {
|
|
338
|
-
targetPath: join(targetRoot, ".claude/settings.json"),
|
|
339
|
-
scope: "project",
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return decodeOverlayPath(relPath, targetRoot, "project");
|
|
344
|
-
}
|