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 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 by default in `~/.config/clank/config.js`:
246
+ Global configuration is stored in `~/.config/clank/clank.config.js`:
247
247
 
248
248
  ```javascript
249
249
  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.61",
4
+ "version": "0.1.62",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -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
- /** Parse a single gitignore line */
173
- function parseLine(
174
- trimmed: string,
175
- source: string,
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
- if (isNegation) {
185
- return { negation: pattern };
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
- return { pattern: { pattern, basePath, negation: false, source } };
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
- /** Find all nested .gitignore files (excluding root) */
215
- async function findNestedGitignores(targetRoot: string): Promise<string[]> {
216
- const gitignores: string[] = [];
217
- const rootGitignore = join(targetRoot, ".gitignore");
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
- for await (const { path, isDirectory } of walkDirectory(targetRoot)) {
220
- if (isDirectory) continue;
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
- return gitignores;
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
- }