clank-cli 0.1.67 → 0.1.74
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 +9 -1
- package/package.json +1 -1
- package/src/AgentFiles.ts +1 -1
- package/src/ClassifyFiles.ts +9 -3
- package/src/Cli.ts +23 -23
- package/src/Consolidate.ts +250 -0
- package/src/FsUtil.ts +54 -4
- package/src/Git.ts +5 -2
- package/src/Gitignore.ts +20 -2
- package/src/Mapper.ts +47 -27
- package/src/OverlayLinks.ts +18 -18
- package/src/ScopeFromSymlink.ts +3 -2
- package/src/commands/Add.ts +142 -152
- package/src/commands/Check.ts +144 -61
- package/src/commands/Init.ts +1 -0
- package/src/commands/Link.ts +49 -32
- package/src/commands/Move.ts +2 -1
- package/src/commands/Rm.ts +2 -2
- package/src/commands/Unlink.ts +7 -2
- package/src/commands/VsCode.ts +2 -1
- package/src/commands/files/Dedupe.ts +5 -6
- package/src/commands/files/Scan.ts +21 -18
package/src/commands/Add.ts
CHANGED
|
@@ -8,14 +8,16 @@ import {
|
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
import * as readline from "node:readline";
|
|
10
10
|
import { forEachAgentPath } from "../AgentFiles.ts";
|
|
11
|
+
import { classifyAgentFiles } from "../ClassifyFiles.ts";
|
|
11
12
|
import { expandPath, loadConfig, validateOverlayExists } from "../Config.ts";
|
|
12
13
|
import {
|
|
13
14
|
createSymlink,
|
|
14
15
|
ensureDir,
|
|
15
16
|
fileExists,
|
|
17
|
+
getCwd,
|
|
16
18
|
getLinkTarget,
|
|
17
19
|
isSymlink,
|
|
18
|
-
|
|
20
|
+
isTrackedRealFile,
|
|
19
21
|
walkDirectory,
|
|
20
22
|
} from "../FsUtil.ts";
|
|
21
23
|
import { type GitContext, getGitContext } from "../Git.ts";
|
|
@@ -46,7 +48,6 @@ interface AddContext {
|
|
|
46
48
|
overlayRoot: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/** Create agent symlinks (CLAUDE.md, GEMINI.md, AGENTS.md → agents.md) */
|
|
50
51
|
interface AgentLinkParams {
|
|
51
52
|
overlayPath: string;
|
|
52
53
|
symlinkDir: string;
|
|
@@ -70,6 +71,17 @@ type ScopeCounts = {
|
|
|
70
71
|
skip: number;
|
|
71
72
|
};
|
|
72
73
|
|
|
74
|
+
interface CreateSymlinksParams {
|
|
75
|
+
filePath: string;
|
|
76
|
+
normalizedPath: string;
|
|
77
|
+
overlayPath: string;
|
|
78
|
+
config: { agents: string[] };
|
|
79
|
+
gitRoot: string;
|
|
80
|
+
overlayRoot: string;
|
|
81
|
+
cwd: string;
|
|
82
|
+
quiet?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
const scopeLabels: Record<Scope, string> = {
|
|
74
86
|
global: "global",
|
|
75
87
|
project: "project",
|
|
@@ -81,7 +93,7 @@ export async function addCommand(
|
|
|
81
93
|
filePaths: string[],
|
|
82
94
|
options: AddOptions = {},
|
|
83
95
|
): Promise<void> {
|
|
84
|
-
const cwd =
|
|
96
|
+
const cwd = await getCwd();
|
|
85
97
|
const gitContext = await getGitContext(cwd);
|
|
86
98
|
const config = await loadConfig();
|
|
87
99
|
const overlayRoot = expandPath(config.overlayRepo);
|
|
@@ -90,17 +102,11 @@ export async function addCommand(
|
|
|
90
102
|
|
|
91
103
|
const ctx = { cwd, gitContext, config, overlayRoot };
|
|
92
104
|
|
|
93
|
-
if (options.interactive) {
|
|
105
|
+
if (options.interactive || filePaths.length === 0) {
|
|
94
106
|
await addAllInteractive(ctx);
|
|
95
107
|
return;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
if (filePaths.length === 0) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
"No files specified. Use --interactive for interactive mode.",
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
110
|
for (const filePath of filePaths) {
|
|
105
111
|
const inputPath = join(cwd, filePath);
|
|
106
112
|
|
|
@@ -135,28 +141,40 @@ async function validateAddOptions(
|
|
|
135
141
|
/** Interactive mode: add all unadded files with per-file scope selection */
|
|
136
142
|
async function addAllInteractive(ctx: AddContext): Promise<void> {
|
|
137
143
|
const { cwd, gitContext, overlayRoot } = ctx;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
targetRoot: gitContext.gitRoot,
|
|
141
|
-
gitContext,
|
|
142
|
-
};
|
|
144
|
+
const { gitRoot: targetRoot } = gitContext;
|
|
145
|
+
const context: MapperContext = { overlayRoot, targetRoot, gitContext };
|
|
143
146
|
|
|
144
147
|
const unadded = await findUnaddedFiles(context);
|
|
145
148
|
const regularFiles = unadded.filter((f) => f.kind === "unadded");
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
// Also find untracked agent files outside managed dirs (e.g. packages/foo/CLAUDE.md)
|
|
151
|
+
const agentClassification = await classifyAgentFiles(
|
|
152
|
+
targetRoot,
|
|
153
|
+
overlayRoot,
|
|
154
|
+
gitContext,
|
|
155
|
+
);
|
|
156
|
+
const regularPaths = new Set(regularFiles.map((f) => f.targetPath));
|
|
157
|
+
const extraAgentPaths = agentClassification.untracked.filter(
|
|
158
|
+
(p) => !regularPaths.has(p),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const allPaths = [
|
|
162
|
+
...regularFiles.map((f) => f.targetPath),
|
|
163
|
+
...extraAgentPaths,
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
if (allPaths.length === 0) {
|
|
148
167
|
console.log("No unadded files found.");
|
|
149
168
|
return;
|
|
150
169
|
}
|
|
151
170
|
|
|
152
|
-
console.log(`Found ${
|
|
171
|
+
console.log(`Found ${allPaths.length} unadded file(s):\n`);
|
|
153
172
|
|
|
154
173
|
const counts: ScopeCounts = { project: 0, worktree: 0, global: 0, skip: 0 };
|
|
155
174
|
|
|
156
|
-
for (let i = 0; i <
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const result = await promptAndAddFile(relPath, i, regularFiles.length, ctx);
|
|
175
|
+
for (let i = 0; i < allPaths.length; i++) {
|
|
176
|
+
const relPath = relative(cwd, allPaths[i]);
|
|
177
|
+
const result = await promptAndAddFile(relPath, i, allPaths.length, ctx);
|
|
160
178
|
if (result === "quit") break;
|
|
161
179
|
if (result !== "error") counts[result]++;
|
|
162
180
|
}
|
|
@@ -183,7 +201,6 @@ async function addSingleFile(
|
|
|
183
201
|
const { quiet } = options;
|
|
184
202
|
|
|
185
203
|
const scope = resolveScopeFromOptions(options);
|
|
186
|
-
/** Absolute path where symlink will be created in target repo */
|
|
187
204
|
const normalizedPath = normalizeAddPath(filePath, cwd, gitRoot);
|
|
188
205
|
const context: MapperContext = {
|
|
189
206
|
overlayRoot,
|
|
@@ -199,7 +216,6 @@ async function addSingleFile(
|
|
|
199
216
|
// Only check barePath for symlink - we want the user's input file, not clank/foo.md
|
|
200
217
|
const barePath = join(cwd, filePath);
|
|
201
218
|
|
|
202
|
-
// Check if already in overlay at a different scope
|
|
203
219
|
await checkScopeConflict(barePath, scope, context, cwd);
|
|
204
220
|
|
|
205
221
|
if (await fileExists(overlayPath)) {
|
|
@@ -229,53 +245,6 @@ async function addSingleFile(
|
|
|
229
245
|
});
|
|
230
246
|
}
|
|
231
247
|
|
|
232
|
-
interface CreateSymlinksParams {
|
|
233
|
-
filePath: string;
|
|
234
|
-
normalizedPath: string;
|
|
235
|
-
overlayPath: string;
|
|
236
|
-
config: { agents: string[] };
|
|
237
|
-
gitRoot: string;
|
|
238
|
-
overlayRoot: string;
|
|
239
|
-
cwd: string;
|
|
240
|
-
quiet?: boolean;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Create symlinks based on file type (agent, prompt, or regular) */
|
|
244
|
-
async function createSymlinksForFile(params: CreateSymlinksParams) {
|
|
245
|
-
const {
|
|
246
|
-
filePath,
|
|
247
|
-
normalizedPath,
|
|
248
|
-
overlayPath,
|
|
249
|
-
config,
|
|
250
|
-
gitRoot,
|
|
251
|
-
overlayRoot,
|
|
252
|
-
cwd,
|
|
253
|
-
quiet,
|
|
254
|
-
} = params;
|
|
255
|
-
|
|
256
|
-
if (isAgentFile(filePath)) {
|
|
257
|
-
const symlinkDir = dirname(normalizedPath);
|
|
258
|
-
await createAgentLinks({
|
|
259
|
-
overlayPath,
|
|
260
|
-
symlinkDir,
|
|
261
|
-
gitRoot,
|
|
262
|
-
overlayRoot,
|
|
263
|
-
agents: config.agents,
|
|
264
|
-
quiet,
|
|
265
|
-
});
|
|
266
|
-
} else if (isPromptFile(normalizedPath)) {
|
|
267
|
-
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd, quiet);
|
|
268
|
-
} else {
|
|
269
|
-
await handleRegularFile(
|
|
270
|
-
normalizedPath,
|
|
271
|
-
overlayPath,
|
|
272
|
-
overlayRoot,
|
|
273
|
-
cwd,
|
|
274
|
-
quiet,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
248
|
/** Prompt for scope and add a single file. Returns the choice or "error". */
|
|
280
249
|
async function promptAndAddFile(
|
|
281
250
|
relPath: string,
|
|
@@ -315,7 +284,7 @@ async function promptAndAddFile(
|
|
|
315
284
|
}
|
|
316
285
|
|
|
317
286
|
/** Print summary of interactive add results */
|
|
318
|
-
function printSummary(counts:
|
|
287
|
+
function printSummary(counts: ScopeCounts): void {
|
|
319
288
|
const parts: string[] = [];
|
|
320
289
|
if (counts.project > 0) parts.push(`${counts.project} to project`);
|
|
321
290
|
if (counts.worktree > 0) parts.push(`${counts.worktree} to worktree`);
|
|
@@ -381,6 +350,87 @@ async function addFileToOverlay(
|
|
|
381
350
|
}
|
|
382
351
|
}
|
|
383
352
|
|
|
353
|
+
/** Create symlinks based on file type (agent, prompt, or regular) */
|
|
354
|
+
async function createSymlinksForFile(params: CreateSymlinksParams) {
|
|
355
|
+
const { filePath, normalizedPath, overlayPath, config } = params;
|
|
356
|
+
const { gitRoot, overlayRoot, cwd, quiet } = params;
|
|
357
|
+
|
|
358
|
+
if (isAgentFile(filePath)) {
|
|
359
|
+
const symlinkDir = dirname(normalizedPath);
|
|
360
|
+
await createAgentLinks({
|
|
361
|
+
overlayPath,
|
|
362
|
+
symlinkDir,
|
|
363
|
+
gitRoot,
|
|
364
|
+
overlayRoot,
|
|
365
|
+
agents: config.agents,
|
|
366
|
+
quiet,
|
|
367
|
+
});
|
|
368
|
+
} else if (isPromptFile(normalizedPath)) {
|
|
369
|
+
await handlePromptFile(normalizedPath, overlayPath, gitRoot, cwd, quiet);
|
|
370
|
+
} else {
|
|
371
|
+
await handleRegularFile(
|
|
372
|
+
normalizedPath,
|
|
373
|
+
overlayPath,
|
|
374
|
+
overlayRoot,
|
|
375
|
+
cwd,
|
|
376
|
+
quiet,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Read a single keypress for scope selection */
|
|
382
|
+
async function readScopeChoice(
|
|
383
|
+
isWorktree: boolean,
|
|
384
|
+
): Promise<InteractiveChoice> {
|
|
385
|
+
const key = await readSingleKey();
|
|
386
|
+
|
|
387
|
+
switch (key.toLowerCase()) {
|
|
388
|
+
case "p":
|
|
389
|
+
case "\r":
|
|
390
|
+
case "\n":
|
|
391
|
+
console.log("project");
|
|
392
|
+
return "project";
|
|
393
|
+
case "w":
|
|
394
|
+
if (!isWorktree) {
|
|
395
|
+
console.log("(not in worktree, using project)");
|
|
396
|
+
return "project";
|
|
397
|
+
}
|
|
398
|
+
console.log("worktree");
|
|
399
|
+
return "worktree";
|
|
400
|
+
case "g":
|
|
401
|
+
console.log("global");
|
|
402
|
+
return "global";
|
|
403
|
+
case "s":
|
|
404
|
+
console.log("skip");
|
|
405
|
+
return "skip";
|
|
406
|
+
case "q":
|
|
407
|
+
case "\x03": // Ctrl+C
|
|
408
|
+
return "quit";
|
|
409
|
+
default:
|
|
410
|
+
console.log("project");
|
|
411
|
+
return "project";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Find content from normalized path or bare input path */
|
|
416
|
+
async function findSourceContent(
|
|
417
|
+
normalizedPath: string,
|
|
418
|
+
barePath: string,
|
|
419
|
+
): Promise<string> {
|
|
420
|
+
// Try normalized path first (e.g., cwd/clank/foo.md)
|
|
421
|
+
if (
|
|
422
|
+
(await fileExists(normalizedPath)) &&
|
|
423
|
+
!(await isSymlink(normalizedPath))
|
|
424
|
+
) {
|
|
425
|
+
return await readFile(normalizedPath, "utf-8");
|
|
426
|
+
}
|
|
427
|
+
// Fall back to bare input path (e.g., cwd/foo.md)
|
|
428
|
+
if ((await fileExists(barePath)) && !(await isSymlink(barePath))) {
|
|
429
|
+
return await readFile(barePath, "utf-8");
|
|
430
|
+
}
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
|
|
384
434
|
async function createAgentLinks(p: AgentLinkParams): Promise<void> {
|
|
385
435
|
const { overlayPath, quiet, ...classifyParams } = p;
|
|
386
436
|
const { toCreate, existing, skipped } =
|
|
@@ -424,9 +474,8 @@ async function handlePromptFile(
|
|
|
424
474
|
gitRoot,
|
|
425
475
|
);
|
|
426
476
|
if (!quiet && created.length) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
477
|
+
const names = created.map((p) => relative(cwd, p)).join(", ");
|
|
478
|
+
console.log(`Created symlinks: ${names}`);
|
|
430
479
|
}
|
|
431
480
|
}
|
|
432
481
|
}
|
|
@@ -450,57 +499,27 @@ async function handleRegularFile(
|
|
|
450
499
|
}
|
|
451
500
|
}
|
|
452
501
|
|
|
453
|
-
/** Read a single keypress
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
502
|
+
/** Read a single keypress from stdin (raw mode) */
|
|
503
|
+
function readSingleKey(): Promise<string> {
|
|
504
|
+
return new Promise((resolve) => {
|
|
505
|
+
const wasRaw = process.stdin.isRaw;
|
|
506
|
+
if (process.stdin.isTTY) {
|
|
507
|
+
readline.emitKeypressEvents(process.stdin);
|
|
508
|
+
process.stdin.setRawMode(true);
|
|
509
|
+
}
|
|
510
|
+
process.stdin.resume();
|
|
458
511
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
case "\n":
|
|
463
|
-
console.log("project");
|
|
464
|
-
return "project";
|
|
465
|
-
case "w":
|
|
466
|
-
if (!isWorktree) {
|
|
467
|
-
console.log("(not in worktree, using project)");
|
|
468
|
-
return "project";
|
|
512
|
+
const onKeypress = (data: Buffer): void => {
|
|
513
|
+
if (process.stdin.isTTY) {
|
|
514
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
469
515
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return "global";
|
|
475
|
-
case "s":
|
|
476
|
-
console.log("skip");
|
|
477
|
-
return "skip";
|
|
478
|
-
case "q":
|
|
479
|
-
case "\x03": // Ctrl+C
|
|
480
|
-
return "quit";
|
|
481
|
-
default:
|
|
482
|
-
console.log("project");
|
|
483
|
-
return "project";
|
|
484
|
-
}
|
|
485
|
-
}
|
|
516
|
+
process.stdin.pause();
|
|
517
|
+
process.stdin.removeListener("data", onKeypress);
|
|
518
|
+
resolve(data.toString());
|
|
519
|
+
};
|
|
486
520
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
normalizedPath: string,
|
|
490
|
-
barePath: string,
|
|
491
|
-
): Promise<string> {
|
|
492
|
-
// Try normalized path first (e.g., cwd/clank/foo.md)
|
|
493
|
-
if (
|
|
494
|
-
(await fileExists(normalizedPath)) &&
|
|
495
|
-
!(await isSymlink(normalizedPath))
|
|
496
|
-
) {
|
|
497
|
-
return await readFile(normalizedPath, "utf-8");
|
|
498
|
-
}
|
|
499
|
-
// Fall back to bare input path (e.g., cwd/foo.md)
|
|
500
|
-
if ((await fileExists(barePath)) && !(await isSymlink(barePath))) {
|
|
501
|
-
return await readFile(barePath, "utf-8");
|
|
502
|
-
}
|
|
503
|
-
return "";
|
|
521
|
+
process.stdin.once("data", onKeypress);
|
|
522
|
+
});
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
/** Classify which agent symlinks to create vs skip */
|
|
@@ -513,18 +532,12 @@ async function classifyAgentLinks(
|
|
|
513
532
|
const toCreate: { targetPath: string; name: string }[] = [];
|
|
514
533
|
|
|
515
534
|
await forEachAgentPath(symlinkDir, agents, async (targetPath) => {
|
|
516
|
-
// Check if symlink already points to overlay
|
|
517
535
|
if (await isSymlinkToOverlay(targetPath, overlayRoot)) {
|
|
518
536
|
existing.push(basename(targetPath));
|
|
519
537
|
return;
|
|
520
538
|
}
|
|
521
539
|
|
|
522
|
-
|
|
523
|
-
(await fileExists(targetPath)) &&
|
|
524
|
-
!(await isSymlink(targetPath)) &&
|
|
525
|
-
(await isTrackedByGit(targetPath, gitRoot));
|
|
526
|
-
|
|
527
|
-
if (isTrackedFile) {
|
|
540
|
+
if (await isTrackedRealFile(targetPath, gitRoot)) {
|
|
528
541
|
skipped.push(basename(targetPath));
|
|
529
542
|
} else {
|
|
530
543
|
toCreate.push({ targetPath, name: basename(targetPath) });
|
|
@@ -533,26 +546,3 @@ async function classifyAgentLinks(
|
|
|
533
546
|
|
|
534
547
|
return { toCreate, existing, skipped };
|
|
535
548
|
}
|
|
536
|
-
|
|
537
|
-
/** Read a single keypress from stdin (raw mode) */
|
|
538
|
-
function readSingleKey(): Promise<string> {
|
|
539
|
-
return new Promise((resolve) => {
|
|
540
|
-
const wasRaw = process.stdin.isRaw;
|
|
541
|
-
if (process.stdin.isTTY) {
|
|
542
|
-
readline.emitKeypressEvents(process.stdin);
|
|
543
|
-
process.stdin.setRawMode(true);
|
|
544
|
-
}
|
|
545
|
-
process.stdin.resume();
|
|
546
|
-
|
|
547
|
-
const onKeypress = (data: Buffer): void => {
|
|
548
|
-
if (process.stdin.isTTY) {
|
|
549
|
-
process.stdin.setRawMode(wasRaw ?? false);
|
|
550
|
-
}
|
|
551
|
-
process.stdin.pause();
|
|
552
|
-
process.stdin.removeListener("data", onKeypress);
|
|
553
|
-
resolve(data.toString());
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
process.stdin.once("data", onKeypress);
|
|
557
|
-
});
|
|
558
|
-
}
|