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/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
|
-
}
|
package/src/OverlayGit.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
import picomatch from "picomatch";
|
|
2
3
|
import { managedDirs } from "./AgentFiles.ts";
|
|
3
4
|
|
|
4
5
|
/** Get git status of the overlay repository */
|
|
5
6
|
export async function getOverlayStatus(
|
|
6
7
|
overlayRoot: string,
|
|
7
|
-
|
|
8
|
+
ignorePatterns: string[] = [],
|
|
9
|
+
): Promise<string[]> {
|
|
8
10
|
const { stdout } = await execa({
|
|
9
11
|
cwd: overlayRoot,
|
|
10
12
|
})`git status --porcelain -uall`;
|
|
11
13
|
|
|
12
|
-
const
|
|
13
|
-
return
|
|
14
|
+
const allLines = stdout.trimEnd() ? stdout.trimEnd().split("\n") : [];
|
|
15
|
+
return filterIgnoredLines(allLines, ignorePatterns);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** Format git status --porcelain output into readable lines */
|
|
@@ -35,6 +37,27 @@ export function formatStatusCode(code: string): string {
|
|
|
35
37
|
return "?";
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** Filter out lines matching ignore patterns */
|
|
41
|
+
function filterIgnoredLines(
|
|
42
|
+
lines: string[],
|
|
43
|
+
ignorePatterns: string[],
|
|
44
|
+
): string[] {
|
|
45
|
+
if (ignorePatterns.length === 0) return lines;
|
|
46
|
+
|
|
47
|
+
const isIgnored = picomatch(ignorePatterns);
|
|
48
|
+
return lines.filter((line) => {
|
|
49
|
+
const filePath = line.slice(3); // Skip status code + space
|
|
50
|
+
const segments = filePath.split("/");
|
|
51
|
+
const pathBasename = segments.at(-1) ?? "";
|
|
52
|
+
|
|
53
|
+
// Check full path and basename
|
|
54
|
+
if (isIgnored(filePath) || isIgnored(pathBasename)) return false;
|
|
55
|
+
|
|
56
|
+
// Check each directory segment (for patterns like ".obsidian")
|
|
57
|
+
return !segments.some((segment) => isIgnored(segment));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
/** Parse overlay path into scope and path parts within that scope */
|
|
39
62
|
function parseScopedPath(filePath: string): {
|
|
40
63
|
scope: string;
|
|
@@ -54,10 +77,8 @@ function parseScopedPath(filePath: string): {
|
|
|
54
77
|
Math.max(0, afterWorktrees.length - 1),
|
|
55
78
|
);
|
|
56
79
|
const branch = afterWorktrees.slice(0, branchSegments).join("/");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
pathParts: afterWorktrees.slice(branchSegments),
|
|
60
|
-
};
|
|
80
|
+
const scope = `${project}/${branch}`;
|
|
81
|
+
return { scope, pathParts: afterWorktrees.slice(branchSegments) };
|
|
61
82
|
}
|
|
62
83
|
return { scope: project || "unknown", pathParts: segments.slice(2) };
|
|
63
84
|
}
|
package/src/OverlayLinks.ts
CHANGED
|
@@ -101,14 +101,10 @@ export async function* walkOverlayFiles(
|
|
|
101
101
|
ignorePatterns.length > 0 ? picomatch(ignorePatterns) : null;
|
|
102
102
|
|
|
103
103
|
const skip = (relPath: string): boolean => {
|
|
104
|
-
// Skip templates
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const basename = relPath.split("/").at(-1) ?? "";
|
|
109
|
-
if (isIgnored(relPath) || isIgnored(basename)) return true;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
104
|
+
if (relPath.startsWith("clank/init/")) return true; // Skip templates
|
|
105
|
+
if (!isIgnored) return false;
|
|
106
|
+
const basename = relPath.split("/").at(-1) ?? "";
|
|
107
|
+
return isIgnored(relPath) || isIgnored(basename);
|
|
112
108
|
};
|
|
113
109
|
|
|
114
110
|
const genEntries = walkDirectory(overlayRoot, { skip });
|
|
@@ -140,7 +136,6 @@ function isMatchingPromptPath(
|
|
|
140
136
|
canonicalPath: string,
|
|
141
137
|
actualPath: string,
|
|
142
138
|
): boolean {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
return canonicalPrompt !== null && canonicalPrompt === actualPrompt;
|
|
139
|
+
const canonical = getPromptRelPath(canonicalPath);
|
|
140
|
+
return canonical !== null && canonical === getPromptRelPath(actualPath);
|
|
146
141
|
}
|