@cmkk/agentlink 0.1.0-beta.1

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/dist/cli.js ADDED
@@ -0,0 +1,1322 @@
1
+ // src/cli.ts
2
+ import { cac } from "cac";
3
+ import pc7 from "picocolors";
4
+
5
+ // src/commands/clean.ts
6
+ import pc2 from "picocolors";
7
+
8
+ // src/core/linker.ts
9
+ import { promises as fs } from "fs";
10
+ import path from "path";
11
+ async function applyPlans(plans, options = {}) {
12
+ const outcomes = [];
13
+ for (const plan of plans) {
14
+ outcomes.push(await applyOne(plan, options));
15
+ }
16
+ return outcomes;
17
+ }
18
+ async function applyOne(plan, options) {
19
+ const { dryRun = false, force = false } = options;
20
+ const targetDir = path.dirname(plan.target);
21
+ const expected = path.relative(targetDir, plan.source);
22
+ try {
23
+ const lstat = await fs.lstat(plan.target).catch(() => null);
24
+ if (lstat?.isSymbolicLink()) {
25
+ const current = await fs.readlink(plan.target);
26
+ if (current === expected) return { plan, status: "skipped" };
27
+ if (force) {
28
+ if (dryRun) {
29
+ return { plan, status: "dry-fix", message: `${current} -> ${expected}` };
30
+ }
31
+ await fs.unlink(plan.target);
32
+ await fs.symlink(expected, plan.target);
33
+ return { plan, status: "fixed", message: `${current} -> ${expected}` };
34
+ }
35
+ return {
36
+ plan,
37
+ status: "warned",
38
+ message: `points to ${current} (use --force to repair)`
39
+ };
40
+ }
41
+ if (lstat) {
42
+ const kind = lstat.isDirectory() ? "directory" : "file";
43
+ return { plan, status: "warned", message: `${kind} already exists at target` };
44
+ }
45
+ if (dryRun) {
46
+ return { plan, status: "dry-create" };
47
+ }
48
+ await fs.mkdir(targetDir, { recursive: true });
49
+ await fs.symlink(expected, plan.target);
50
+ return { plan, status: "created" };
51
+ } catch (err) {
52
+ return { plan, status: "errored", message: err instanceof Error ? err.message : String(err) };
53
+ }
54
+ }
55
+ async function findStaleLinks(targetDir) {
56
+ const entries = await fs.readdir(targetDir, { withFileTypes: true }).catch(() => null);
57
+ if (!entries) return [];
58
+ const stale = [];
59
+ for (const entry of entries) {
60
+ if (!entry.isSymbolicLink()) continue;
61
+ const full = path.join(targetDir, entry.name);
62
+ const exists = await fs.stat(full).catch(() => null);
63
+ if (!exists) stale.push(full);
64
+ }
65
+ return stale;
66
+ }
67
+ async function findManagedLinks(targetDir, sourceRoot, options = {}) {
68
+ const sourceAbs = path.resolve(sourceRoot);
69
+ const out = [];
70
+ await (async function descend(dir) {
71
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
72
+ if (!entries) return;
73
+ for (const entry of entries) {
74
+ const full = path.join(dir, entry.name);
75
+ if (entry.isSymbolicLink()) {
76
+ const raw = await fs.readlink(full).catch(() => null);
77
+ if (!raw) continue;
78
+ const resolved = path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(dir, raw);
79
+ if (resolved === sourceAbs || resolved.startsWith(sourceAbs + path.sep)) {
80
+ out.push(full);
81
+ }
82
+ } else if (options.recursive && entry.isDirectory()) {
83
+ await descend(full);
84
+ }
85
+ }
86
+ })(targetDir);
87
+ return out;
88
+ }
89
+ async function removeLinks(paths, options = {}) {
90
+ const { dryRun = false } = options;
91
+ const out = [];
92
+ for (const p of paths) {
93
+ if (dryRun) {
94
+ out.push({ path: p, status: "dry-remove" });
95
+ continue;
96
+ }
97
+ try {
98
+ await fs.unlink(p);
99
+ out.push({ path: p, status: "removed" });
100
+ } catch (err) {
101
+ out.push({
102
+ path: p,
103
+ status: "errored",
104
+ message: err instanceof Error ? err.message : String(err)
105
+ });
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ async function pruneEmptyDirs(dirs) {
111
+ for (const dir of dirs) {
112
+ await fs.rmdir(dir).catch(() => void 0);
113
+ }
114
+ }
115
+
116
+ // src/commands/_shared.ts
117
+ import { promises as fs4 } from "fs";
118
+ import path4 from "path";
119
+ import pc from "picocolors";
120
+
121
+ // src/adapters/claude.ts
122
+ var claude = {
123
+ name: "claude",
124
+ rootDir: ".claude",
125
+ layout: {
126
+ skills: { targetSubdir: "skills" },
127
+ rules: { targetSubdir: "rules" },
128
+ commands: { targetSubdir: "commands" }
129
+ }
130
+ };
131
+
132
+ // src/adapters/codebuddy.ts
133
+ var codebuddy = {
134
+ name: "codebuddy",
135
+ rootDir: ".codebuddy",
136
+ layout: {
137
+ skills: { targetSubdir: "skills" },
138
+ rules: { targetSubdir: "rules" },
139
+ commands: { targetSubdir: "commands" }
140
+ }
141
+ };
142
+
143
+ // src/adapters/cursor.ts
144
+ var cursor = {
145
+ name: "cursor",
146
+ rootDir: ".cursor",
147
+ layout: {
148
+ skills: { targetSubdir: "skills" },
149
+ rules: { targetSubdir: "rules", flatten: true, ext: ".mdc" },
150
+ commands: { targetSubdir: "commands", flatten: true },
151
+ prompts: false
152
+ }
153
+ };
154
+
155
+ // src/adapters/index.ts
156
+ var adapterRegistry = {
157
+ cursor,
158
+ claude,
159
+ codebuddy
160
+ };
161
+
162
+ // src/core/config.ts
163
+ import { promises as fs2 } from "fs";
164
+ import os from "os";
165
+ import path2 from "path";
166
+ import stripJsonComments from "strip-json-comments";
167
+ import { z } from "zod";
168
+
169
+ // src/core/errors.ts
170
+ var AgentlinkError = class extends Error {
171
+ constructor(message) {
172
+ super(message);
173
+ this.name = new.target.name;
174
+ }
175
+ };
176
+ var SourceNotFoundError = class extends AgentlinkError {
177
+ searched;
178
+ constructor(searched) {
179
+ super("no source .agents/ directory found");
180
+ this.searched = searched;
181
+ }
182
+ };
183
+ var InvalidPathError = class _InvalidPathError extends AgentlinkError {
184
+ flag;
185
+ path;
186
+ reason;
187
+ constructor(flag, p, reason) {
188
+ super(_InvalidPathError.message(flag, p, reason));
189
+ this.flag = flag;
190
+ this.path = p;
191
+ this.reason = reason;
192
+ }
193
+ static message(flag, p, reason) {
194
+ if (reason === "missing") return `${flag} does not exist: ${p}`;
195
+ if (reason === "not-a-directory") return `${flag} is not a directory: ${p}`;
196
+ return `${flag} is not a file: ${p}`;
197
+ }
198
+ };
199
+ var ConflictError = class extends AgentlinkError {
200
+ };
201
+
202
+ // src/core/types.ts
203
+ var DEFAULT_INCLUDE = ["skills", "rules", "commands"];
204
+
205
+ // src/core/config.ts
206
+ var CONFIG_FILENAME = "agentlink.config.json";
207
+ var ENV_CONFIG_KEY = "AGENTLINK_CONFIG";
208
+ var ASSET_TYPE_VALUES = ["skills", "rules", "commands", "prompts", "templates", "agents"];
209
+ var ASSET_TYPE_SET = new Set(ASSET_TYPE_VALUES);
210
+ var assetTypeSchema = z.enum(ASSET_TYPE_VALUES);
211
+ var targetSubdirSchema = z.string().min(1).refine((s) => !path2.isAbsolute(s), {
212
+ message: "targetSubdir must be relative (no leading `/` or drive letter)"
213
+ }).refine((s) => !s.split(/[\\/]/).some((seg) => seg === ".."), {
214
+ message: "targetSubdir cannot traverse upward (`..` segment forbidden)"
215
+ });
216
+ var assetLayoutSchema = z.object({
217
+ targetSubdir: targetSubdirSchema.optional(),
218
+ flatten: z.boolean().optional(),
219
+ include: z.array(z.string().min(1)).optional(),
220
+ exclude: z.array(z.string().min(1)).optional(),
221
+ ext: z.string().min(1).optional()
222
+ });
223
+ var adapterOverrideSchema = z.union([
224
+ z.boolean(),
225
+ z.object({
226
+ // partialRecord: zod v4 z.record(enum,…) is exhaustive by default;
227
+ // partialRecord lets each key be optional, which is what users expect.
228
+ layout: z.partialRecord(assetTypeSchema, assetLayoutSchema).optional()
229
+ }).strict()
230
+ ]);
231
+ var adapterNameSchema = z.enum(
232
+ Object.keys(adapterRegistry)
233
+ );
234
+ var configSchema = z.object({
235
+ include: z.array(assetTypeSchema).optional(),
236
+ exclude: z.array(assetTypeSchema).optional(),
237
+ adapters: z.partialRecord(adapterNameSchema, adapterOverrideSchema).optional()
238
+ }).strict();
239
+ var ConfigParseError = class extends AgentlinkError {
240
+ issues;
241
+ constructor(filePath, issues) {
242
+ const lines = issues.map(({ path: p, message }) => ` at ${p || "<root>"}: ${message}`);
243
+ super(`invalid agentlink config at ${filePath}
244
+ ${lines.join("\n")}`);
245
+ this.issues = issues;
246
+ }
247
+ };
248
+ async function loadConfig(opts) {
249
+ const location = await resolveConfigPath(opts);
250
+ const userConfig = location.path ? await readUserConfig(location.path) : {};
251
+ const adapters = mergeAdapters(userConfig.adapters);
252
+ const assetTypes = resolveAssetTypes(userConfig.include, userConfig.exclude);
253
+ return {
254
+ source: opts.source,
255
+ adapters,
256
+ assetTypes,
257
+ configPath: location.path,
258
+ configOrigin: location.origin
259
+ };
260
+ }
261
+ async function resolveConfigPath(opts = {}) {
262
+ const cwd = opts.cwd ?? process.cwd();
263
+ const home = opts.home ?? os.homedir();
264
+ const env = opts.env ?? process.env;
265
+ if (opts.config !== void 0 && opts.config !== "") {
266
+ const abs = await assertConfigFile(expandTilde(opts.config, home), "--config", cwd);
267
+ return { path: abs, origin: "flag" };
268
+ }
269
+ const envValue = env[ENV_CONFIG_KEY];
270
+ if (envValue !== void 0 && envValue !== "") {
271
+ const abs = await assertConfigFile(expandTilde(envValue, home), `$${ENV_CONFIG_KEY}`, cwd);
272
+ return { path: abs, origin: "env" };
273
+ }
274
+ const ancestor = await searchAncestors(path2.resolve(cwd), home);
275
+ if (ancestor) {
276
+ return { path: ancestor, origin: "cwd-ancestor" };
277
+ }
278
+ const homeConfig = path2.join(home, CONFIG_FILENAME);
279
+ if (await isFile(homeConfig)) {
280
+ return { path: homeConfig, origin: "home-global" };
281
+ }
282
+ return { path: null, origin: "defaults" };
283
+ }
284
+ async function searchAncestors(start, home) {
285
+ const homeAbs = path2.resolve(home);
286
+ let current = start;
287
+ while (true) {
288
+ if (current === homeAbs) {
289
+ return null;
290
+ }
291
+ const candidate = path2.join(current, CONFIG_FILENAME);
292
+ if (await isFile(candidate)) return candidate;
293
+ const parent = path2.dirname(current);
294
+ if (parent === current) return null;
295
+ if (isInside(current, homeAbs) && !isInside(parent, homeAbs) && parent !== homeAbs) {
296
+ return null;
297
+ }
298
+ current = parent;
299
+ }
300
+ }
301
+ async function assertConfigFile(input, flag, cwd) {
302
+ const abs = path2.isAbsolute(input) ? input : path2.resolve(cwd, input);
303
+ const stat = await fs2.stat(abs).catch(() => null);
304
+ if (!stat) throw new InvalidPathError(flag, abs, "missing");
305
+ if (!stat.isFile()) throw new InvalidPathError(flag, abs, "not-a-file");
306
+ return abs;
307
+ }
308
+ async function isFile(p) {
309
+ const stat = await fs2.stat(p).catch(() => null);
310
+ return stat?.isFile() ?? false;
311
+ }
312
+ function expandTilde(p, home) {
313
+ if (p === "~") return home;
314
+ if (p.startsWith("~/")) return path2.join(home, p.slice(2));
315
+ return p;
316
+ }
317
+ function isInside(child, parent) {
318
+ const c = path2.resolve(child);
319
+ const p = path2.resolve(parent);
320
+ if (c === p) return true;
321
+ const rel = path2.relative(p, c);
322
+ return rel !== "" && !rel.startsWith("..") && !path2.isAbsolute(rel);
323
+ }
324
+ async function readExplicitAdapters(configPath) {
325
+ if (!configPath) return /* @__PURE__ */ new Set();
326
+ const raw = await fs2.readFile(configPath, "utf8").catch(() => null);
327
+ if (raw === null) return /* @__PURE__ */ new Set();
328
+ try {
329
+ const parsed = JSON.parse(stripJsonComments(raw, { trailingCommas: true }));
330
+ if (!parsed.adapters) return /* @__PURE__ */ new Set();
331
+ return new Set(Object.keys(parsed.adapters).filter((k) => parsed.adapters?.[k] !== false));
332
+ } catch {
333
+ return /* @__PURE__ */ new Set();
334
+ }
335
+ }
336
+ async function readUserConfig(filePath) {
337
+ const raw = await fs2.readFile(filePath, "utf8").catch((err) => {
338
+ if (err.code === "ENOENT") return null;
339
+ throw err;
340
+ });
341
+ if (raw === null) return {};
342
+ let parsed;
343
+ try {
344
+ parsed = JSON.parse(stripJsonComments(raw, { trailingCommas: true }));
345
+ } catch (err) {
346
+ const message = err instanceof Error ? err.message : String(err);
347
+ throw new ConfigParseError(filePath, [{ path: "", message: `not valid JSON: ${message}` }]);
348
+ }
349
+ const result = configSchema.safeParse(parsed);
350
+ if (!result.success) {
351
+ const issues = result.error.issues.map((issue) => ({
352
+ path: issue.path.map((seg) => String(seg)).join("."),
353
+ message: issue.message
354
+ }));
355
+ throw new ConfigParseError(filePath, issues);
356
+ }
357
+ return result.data;
358
+ }
359
+ function mergeAdapters(overrides) {
360
+ const result = [];
361
+ for (const [name, preset] of Object.entries(adapterRegistry)) {
362
+ const override = overrides?.[name];
363
+ if (override === false) continue;
364
+ if (override === void 0 || override === true) {
365
+ result.push(preset);
366
+ continue;
367
+ }
368
+ result.push({
369
+ name: preset.name,
370
+ rootDir: preset.rootDir,
371
+ layout: mergeLayout(preset.layout, override.layout)
372
+ });
373
+ }
374
+ return result;
375
+ }
376
+ function mergeLayout(base, patch) {
377
+ if (!patch) return base;
378
+ const merged = { ...base };
379
+ for (const key of Object.keys(patch)) {
380
+ const value = patch[key];
381
+ if (value === void 0) continue;
382
+ const baseLayout = base[key];
383
+ if (baseLayout === void 0 || baseLayout === false) {
384
+ const targetSubdir = value.targetSubdir;
385
+ if (targetSubdir === void 0) {
386
+ throw new ConfigPatchError(
387
+ `adapter override for "${key}" needs a targetSubdir because the built-in adapter does not define one`
388
+ );
389
+ }
390
+ merged[key] = compactLayout(targetSubdir, value);
391
+ continue;
392
+ }
393
+ merged[key] = mergeIntoBase(baseLayout, value);
394
+ }
395
+ return merged;
396
+ }
397
+ function compactLayout(targetSubdir, v) {
398
+ const out = { targetSubdir };
399
+ if (v.flatten !== void 0) out.flatten = v.flatten;
400
+ if (v.include !== void 0) out.include = v.include;
401
+ if (v.exclude !== void 0) out.exclude = v.exclude;
402
+ if (v.ext !== void 0) out.ext = v.ext;
403
+ return out;
404
+ }
405
+ function mergeIntoBase(base, patch) {
406
+ const out = { ...base };
407
+ if (patch.targetSubdir !== void 0) out.targetSubdir = patch.targetSubdir;
408
+ if (patch.flatten !== void 0) out.flatten = patch.flatten;
409
+ if (patch.include !== void 0) out.include = patch.include;
410
+ if (patch.exclude !== void 0) out.exclude = patch.exclude;
411
+ if (patch.ext !== void 0) out.ext = patch.ext;
412
+ return out;
413
+ }
414
+ var ConfigPatchError = class extends AgentlinkError {
415
+ };
416
+ function resolveAssetTypes(include, exclude) {
417
+ const candidates = new Set(include ?? DEFAULT_INCLUDE);
418
+ for (const denied of exclude ?? []) {
419
+ candidates.delete(denied);
420
+ }
421
+ return candidates;
422
+ }
423
+
424
+ // src/core/resolve.ts
425
+ import { promises as fs3 } from "fs";
426
+ import os2 from "os";
427
+ import path3 from "path";
428
+ var ENV_SOURCE_KEY = "AGENTLINK_SOURCE";
429
+ var AGENTS_DIRNAME = ".agents";
430
+ async function resolvePaths(opts = {}) {
431
+ const [src, dst] = await Promise.all([resolveSource(opts), resolveDestRoot(opts)]);
432
+ if (isInside2(dst.path, src.path)) {
433
+ process.emitWarning(
434
+ `destination root "${dst.path}" lives inside source "${src.path}" \u2014 this may create self-references.`,
435
+ { code: "AGENTLINK_NESTED_DEST" }
436
+ );
437
+ }
438
+ return {
439
+ source: src.path,
440
+ sourceOrigin: src.origin,
441
+ destRoot: dst.path,
442
+ destOrigin: dst.origin
443
+ };
444
+ }
445
+ async function resolveSource(opts = {}) {
446
+ const cwd = opts.cwd ?? process.cwd();
447
+ const home = opts.home ?? os2.homedir();
448
+ const env = opts.env ?? process.env;
449
+ if (opts.source !== void 0 && opts.source !== "") {
450
+ const abs = await assertDir(expandTilde2(opts.source, home), "--source", cwd);
451
+ return { path: abs, origin: "flag" };
452
+ }
453
+ const envValue = env[ENV_SOURCE_KEY];
454
+ if (envValue !== void 0 && envValue !== "") {
455
+ const abs = await assertDir(expandTilde2(envValue, home), `$${ENV_SOURCE_KEY}`, cwd);
456
+ return { path: abs, origin: "env" };
457
+ }
458
+ const local = path3.join(cwd, AGENTS_DIRNAME);
459
+ if (await isDir(local)) {
460
+ return { path: local, origin: "cwd-local" };
461
+ }
462
+ const homeAgents = path3.join(home, AGENTS_DIRNAME);
463
+ if (await isDir(homeAgents)) {
464
+ return { path: homeAgents, origin: "home-global" };
465
+ }
466
+ throw new SourceNotFoundError([
467
+ { origin: "--source flag", status: "not provided" },
468
+ { origin: `$${ENV_SOURCE_KEY}`, status: "not set" },
469
+ { origin: local, status: "missing" },
470
+ { origin: homeAgents, status: "missing" }
471
+ ]);
472
+ }
473
+ async function resolveDestRoot(opts = {}) {
474
+ const cwd = opts.cwd ?? process.cwd();
475
+ const home = opts.home ?? os2.homedir();
476
+ if (opts.target !== void 0 && opts.target !== "" && opts.user) {
477
+ throw new ConflictError("--target and --user are mutually exclusive");
478
+ }
479
+ if (opts.target !== void 0 && opts.target !== "") {
480
+ const abs = await assertDir(expandTilde2(opts.target, home), "--target", cwd);
481
+ return { path: abs, origin: "flag" };
482
+ }
483
+ if (opts.user) {
484
+ return { path: home, origin: "user" };
485
+ }
486
+ return { path: cwd, origin: "cwd" };
487
+ }
488
+ function expandTilde2(p, home) {
489
+ if (p === "~") return home;
490
+ if (p.startsWith("~/")) return path3.join(home, p.slice(2));
491
+ return p;
492
+ }
493
+ async function assertDir(input, flag, cwd) {
494
+ const abs = path3.isAbsolute(input) ? input : path3.resolve(cwd, input);
495
+ const stat = await fs3.stat(abs).catch(() => null);
496
+ if (!stat) throw new InvalidPathError(flag, abs, "missing");
497
+ if (!stat.isDirectory()) throw new InvalidPathError(flag, abs, "not-a-directory");
498
+ return abs;
499
+ }
500
+ async function isDir(p) {
501
+ const stat = await fs3.stat(p).catch(() => null);
502
+ return stat?.isDirectory() ?? false;
503
+ }
504
+ function isInside2(child, parent) {
505
+ const c = path3.resolve(child);
506
+ const p = path3.resolve(parent);
507
+ if (c === p) return true;
508
+ const rel = path3.relative(p, c);
509
+ return rel !== "" && !rel.startsWith("..") && !path3.isAbsolute(rel);
510
+ }
511
+
512
+ // src/commands/_shared.ts
513
+ async function loadContext(flags) {
514
+ const paths = await resolvePaths({
515
+ ...flags.source !== void 0 ? { source: flags.source } : {},
516
+ ...flags.target !== void 0 ? { target: flags.target } : {},
517
+ ...flags.user !== void 0 ? { user: flags.user } : {}
518
+ });
519
+ const rawConfig = await loadConfig({
520
+ source: paths.source,
521
+ ...flags.config !== void 0 ? { config: flags.config } : {}
522
+ });
523
+ const config = await gateAdapters(rawConfig, paths.destRoot);
524
+ return { paths, config };
525
+ }
526
+ async function gateAdapters(config, destRoot) {
527
+ const live = [];
528
+ for (const adapter of config.adapters) {
529
+ const root = path4.join(destRoot, adapter.rootDir);
530
+ const stat = await fs4.stat(root).catch(() => null);
531
+ if (stat?.isDirectory()) live.push(adapter);
532
+ }
533
+ if (live.length === config.adapters.length) return config;
534
+ return { ...config, adapters: live };
535
+ }
536
+ function printHeader(ctx, mode, flags) {
537
+ if (flags.quiet) return;
538
+ console.log("");
539
+ console.log(pc.bold(`[agentlink] ${mode}`));
540
+ console.log(` source: ${ctx.paths.source} ${pc.dim(`(${ctx.paths.sourceOrigin})`)}`);
541
+ console.log(` target: ${ctx.paths.destRoot} ${pc.dim(`(${ctx.paths.destOrigin})`)}`);
542
+ const configLine = ctx.config.configPath ? `${ctx.config.configPath} ${pc.dim(`(${ctx.config.configOrigin})`)}` : pc.dim("(defaults)");
543
+ console.log(` config: ${configLine}`);
544
+ console.log(` adapters: ${ctx.config.adapters.map((a) => a.name).join(", ") || pc.dim("(none)")}`);
545
+ if (flags.dryRun) console.log(pc.yellow(" mode: dry-run"));
546
+ console.log("");
547
+ }
548
+ function enumerateTargetDirs(ctx) {
549
+ const out = [];
550
+ const root = ctx.paths.destRoot;
551
+ for (const adapter of ctx.config.adapters) {
552
+ for (const assetType of ctx.config.assetTypes) {
553
+ const slot = adapter.layout[assetType];
554
+ if (slot === void 0 || slot === false) continue;
555
+ out.push({
556
+ adapter,
557
+ assetType,
558
+ targetDir: path4.join(root, adapter.rootDir, slot.targetSubdir),
559
+ rootDir: path4.join(root, adapter.rootDir)
560
+ });
561
+ }
562
+ }
563
+ return out;
564
+ }
565
+ async function enumerateAllAdapterRoots(destRoot) {
566
+ const out = [];
567
+ for (const [name, preset] of Object.entries(adapterRegistry)) {
568
+ const rootDir = path4.join(destRoot, preset.rootDir);
569
+ const stat = await fs4.stat(rootDir).catch(() => null);
570
+ if (stat?.isDirectory()) out.push({ name, rootDir });
571
+ }
572
+ return out;
573
+ }
574
+
575
+ // src/commands/clean.ts
576
+ async function runClean(flags = {}) {
577
+ const ctx = await loadContext(flags);
578
+ printHeader(ctx, "clean", flags);
579
+ const targets = enumerateTargetDirs(ctx).map((t) => t.targetDir);
580
+ const stale = (await Promise.all(targets.map(findStaleLinks))).flat();
581
+ if (stale.length === 0) {
582
+ console.log(pc2.dim(" no dangling symlinks found"));
583
+ return;
584
+ }
585
+ const outcomes = await removeLinks(stale, flags.dryRun ? { dryRun: true } : {});
586
+ let cleaned = 0;
587
+ let dry = 0;
588
+ let errored = 0;
589
+ for (const o of outcomes) {
590
+ if (o.status === "removed") {
591
+ cleaned++;
592
+ if (!flags.quiet) console.log(`${pc2.yellow("[CLEAN]")} ${o.path}`);
593
+ } else if (o.status === "dry-remove") {
594
+ dry++;
595
+ if (!flags.quiet) console.log(`${pc2.dim("[DRY]")} would clean ${o.path}`);
596
+ } else if (o.status === "errored") {
597
+ errored++;
598
+ console.error(`${pc2.red("[ERR]")} ${o.path} ${pc2.dim(`(${o.message ?? ""})`)}`);
599
+ }
600
+ }
601
+ console.log("");
602
+ const parts = [];
603
+ if (cleaned) parts.push(pc2.yellow(`${cleaned} cleaned`));
604
+ if (dry) parts.push(pc2.dim(`${dry} dry-run`));
605
+ if (errored) parts.push(pc2.red(`${errored} errored`));
606
+ console.log(parts.length === 0 ? pc2.dim(" no changes") : ` ${parts.join(", ")}`);
607
+ if (errored > 0) process.exitCode = 1;
608
+ }
609
+
610
+ // src/commands/doctor.ts
611
+ import { promises as fs6 } from "fs";
612
+ import path6 from "path";
613
+ import pc3 from "picocolors";
614
+
615
+ // src/core/layout.ts
616
+ import { promises as fs5 } from "fs";
617
+ import path5 from "path";
618
+ import picomatch from "picomatch";
619
+ async function buildLinkPlans(opts) {
620
+ const { source, destRoot, config } = opts;
621
+ const draft = [];
622
+ for (const adapter of config.adapters) {
623
+ for (const assetType of config.assetTypes) {
624
+ const slot = adapter.layout[assetType];
625
+ if (slot === void 0 || slot === false) continue;
626
+ const sourceAssetDir = path5.join(source, assetType);
627
+ if (!await isDir2(sourceAssetDir)) continue;
628
+ const targetAssetDir = path5.join(destRoot, adapter.rootDir, slot.targetSubdir);
629
+ const planned = slot.flatten ? await planFlattened(sourceAssetDir, targetAssetDir, slot, adapter, assetType) : await planShallow(sourceAssetDir, targetAssetDir, slot, adapter, assetType);
630
+ draft.push(...planned);
631
+ }
632
+ }
633
+ return splitByCollision(draft);
634
+ }
635
+ async function planFlattened(sourceAssetDir, targetAssetDir, layout, adapter, assetType) {
636
+ const files = await walkFiles(sourceAssetDir);
637
+ const includeMatch = makeIncludeMatcher(layout.include);
638
+ const excludeMatch = makeExcludeMatcher(layout.exclude);
639
+ const out = [];
640
+ for (const abs of files) {
641
+ const rel = path5.relative(sourceAssetDir, abs);
642
+ if (!includeMatch(rel)) continue;
643
+ if (excludeMatch(rel)) continue;
644
+ const targetName = applyRename(
645
+ rel,
646
+ layout,
647
+ /* flatten */
648
+ true
649
+ );
650
+ out.push({
651
+ source: abs,
652
+ target: path5.join(targetAssetDir, targetName),
653
+ adapterName: adapter.name,
654
+ assetType,
655
+ flattened: true
656
+ });
657
+ }
658
+ return out;
659
+ }
660
+ async function planShallow(sourceAssetDir, targetAssetDir, layout, adapter, assetType) {
661
+ const items = await listFirstLevel(sourceAssetDir);
662
+ const includeMatch = makeIncludeMatcher(layout.include);
663
+ const excludeMatch = makeExcludeMatcher(layout.exclude);
664
+ const out = [];
665
+ for (const entry of items) {
666
+ if (!includeMatch(entry.name)) continue;
667
+ if (excludeMatch(entry.name)) continue;
668
+ const targetName = applyRename(
669
+ entry.name,
670
+ layout,
671
+ /* flatten */
672
+ false
673
+ );
674
+ out.push({
675
+ source: path5.join(sourceAssetDir, entry.name),
676
+ target: path5.join(targetAssetDir, targetName),
677
+ adapterName: adapter.name,
678
+ assetType,
679
+ flattened: false
680
+ });
681
+ }
682
+ return out;
683
+ }
684
+ var PATH_SEPARATOR_RE = /[\\/]/g;
685
+ function applyRename(relPath, layout, flatten) {
686
+ if (layout.rename) return layout.rename(relPath);
687
+ let name = flatten ? relPath.replace(PATH_SEPARATOR_RE, "__") : relPath;
688
+ if (layout.ext) {
689
+ const ext = path5.extname(name);
690
+ name = ext ? name.slice(0, -ext.length) + layout.ext : name + layout.ext;
691
+ }
692
+ return name;
693
+ }
694
+ async function walkFiles(root) {
695
+ const out = [];
696
+ await (async function descend(dir) {
697
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
698
+ for (const entry of entries) {
699
+ if (entry.name.startsWith(".")) continue;
700
+ const full = path5.join(dir, entry.name);
701
+ if (entry.isDirectory()) {
702
+ await descend(full);
703
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
704
+ out.push(full);
705
+ }
706
+ }
707
+ })(root);
708
+ return out;
709
+ }
710
+ async function listFirstLevel(root) {
711
+ const entries = await fs5.readdir(root, { withFileTypes: true });
712
+ return entries.filter((e) => !e.name.startsWith(".")).map((e) => ({ name: e.name, isDir: e.isDirectory() }));
713
+ }
714
+ async function isDir2(p) {
715
+ const stat = await fs5.stat(p).catch(() => null);
716
+ return stat?.isDirectory() ?? false;
717
+ }
718
+ function makeIncludeMatcher(patterns) {
719
+ if (!patterns || patterns.length === 0) return () => true;
720
+ const fns = patterns.map((p) => picomatch(p, { dot: true }));
721
+ return (rel) => fns.some((fn) => fn(rel));
722
+ }
723
+ function makeExcludeMatcher(patterns) {
724
+ if (!patterns || patterns.length === 0) return () => false;
725
+ const fns = patterns.map((p) => picomatch(p, { dot: true }));
726
+ return (rel) => fns.some((fn) => fn(rel));
727
+ }
728
+ function splitByCollision(plans) {
729
+ const byTarget = /* @__PURE__ */ new Map();
730
+ for (const plan of plans) {
731
+ const bucket = byTarget.get(plan.target);
732
+ if (bucket) bucket.push(plan);
733
+ else byTarget.set(plan.target, [plan]);
734
+ }
735
+ const accepted = [];
736
+ const collisions = [];
737
+ for (const [target, bucket] of byTarget) {
738
+ if (bucket.length === 1) {
739
+ accepted.push(bucket[0]);
740
+ continue;
741
+ }
742
+ const head = bucket[0];
743
+ collisions.push({
744
+ target,
745
+ sources: bucket.map((p) => p.source),
746
+ adapterName: head.adapterName,
747
+ assetType: head.assetType
748
+ });
749
+ }
750
+ return { plans: accepted, collisions };
751
+ }
752
+
753
+ // src/commands/doctor.ts
754
+ async function runDoctor(flags = {}) {
755
+ const ctx = await loadContext(flags);
756
+ printHeader(ctx, "doctor", flags);
757
+ let warnings = 0;
758
+ let errors = 0;
759
+ const explicitlyEnabled = await readExplicitAdapters(ctx.config.configPath);
760
+ console.log(pc3.bold("Adapters"));
761
+ for (const [name, preset] of Object.entries(adapterRegistry)) {
762
+ const root = path6.join(ctx.paths.destRoot, preset.rootDir);
763
+ const stat = await fs6.stat(root).catch(() => null);
764
+ if (stat?.isDirectory()) {
765
+ console.log(
766
+ ` ${pc3.green("\u2713")} ${name.padEnd(11)} ${pc3.dim(preset.rootDir + "/")} ${pc3.dim("(active)")}`
767
+ );
768
+ } else if (explicitlyEnabled.has(name)) {
769
+ warnings++;
770
+ console.log(
771
+ ` ${pc3.yellow("\u26A0")} ${name.padEnd(11)} ${pc3.dim(preset.rootDir + "/")} ${pc3.yellow("rootDir not found \u2014 sync will skip")}`
772
+ );
773
+ } else {
774
+ console.log(
775
+ ` ${pc3.dim("\u2014")} ${name.padEnd(11)} ${pc3.dim(preset.rootDir + "/")} ${pc3.dim("not in use on this machine")}`
776
+ );
777
+ }
778
+ }
779
+ console.log("");
780
+ const { plans, collisions } = await buildLinkPlans({
781
+ source: ctx.paths.source,
782
+ destRoot: ctx.paths.destRoot,
783
+ config: ctx.config
784
+ });
785
+ console.log(pc3.bold("Link plan"));
786
+ console.log(` ${plans.length} plan${plans.length === 1 ? "" : "s"}`);
787
+ if (collisions.length === 0) {
788
+ console.log(` ${pc3.green("\u2713")} no collisions`);
789
+ } else {
790
+ warnings += collisions.length;
791
+ console.log(` ${pc3.yellow("\u26A0")} ${collisions.length} collision(s):`);
792
+ for (const c of collisions) {
793
+ console.log(` ${pc3.dim(c.adapterName + "/" + c.assetType)} ${c.target}`);
794
+ for (const s of c.sources) console.log(` ${pc3.dim("\u2190")} ${s}`);
795
+ }
796
+ }
797
+ console.log("");
798
+ const targetDirs = enumerateTargetDirs(ctx).map((t) => t.targetDir);
799
+ const stale = (await Promise.all(targetDirs.map(findStaleLinks))).flat();
800
+ const allRoots = await enumerateAllAdapterRoots(ctx.paths.destRoot);
801
+ const managed = (await Promise.all(
802
+ allRoots.map((r) => findManagedLinks(r.rootDir, ctx.paths.source, { recursive: true }))
803
+ )).flat();
804
+ const staleSet = new Set(stale);
805
+ const planTargets = new Set(plans.map((p) => p.target));
806
+ const orphans = managed.filter((p) => !planTargets.has(p) && !staleSet.has(p));
807
+ console.log(pc3.bold("Existing links"));
808
+ if (stale.length === 0 && orphans.length === 0) {
809
+ console.log(` ${pc3.green("\u2713")} all clear`);
810
+ } else {
811
+ if (stale.length > 0) {
812
+ warnings += stale.length;
813
+ console.log(
814
+ ` ${pc3.yellow("\u26A0")} ${stale.length} dangling ${pc3.dim("(run `agentlink clean` to drop):")}`
815
+ );
816
+ for (const p of stale) console.log(` ${p}`);
817
+ }
818
+ if (orphans.length > 0) {
819
+ warnings += orphans.length;
820
+ console.log(
821
+ ` ${pc3.yellow("\u26A0")} ${orphans.length} orphan ${pc3.dim("(linked into source but not in current plan; `unlink` then `sync` will rebuild):")}`
822
+ );
823
+ for (const p of orphans) console.log(` ${p}`);
824
+ }
825
+ }
826
+ console.log("");
827
+ if (warnings + errors === 0) {
828
+ console.log(pc3.green("Healthy"));
829
+ return;
830
+ }
831
+ const parts = [];
832
+ if (warnings > 0) parts.push(pc3.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`));
833
+ if (errors > 0) parts.push(pc3.red(`${errors} error${errors === 1 ? "" : "s"}`));
834
+ console.log(`Issues: ${parts.join(", ")}`);
835
+ if (errors > 0) process.exitCode = 1;
836
+ }
837
+
838
+ // src/commands/init.ts
839
+ import { promises as fs7 } from "fs";
840
+ import os3 from "os";
841
+ import path8 from "path";
842
+ import { fileURLToPath } from "url";
843
+ import pc5 from "picocolors";
844
+ import prompts from "prompts";
845
+
846
+ // src/commands/sync.ts
847
+ import path7 from "path";
848
+ import pc4 from "picocolors";
849
+ var StrictModeViolation = class extends AgentlinkError {
850
+ constructor(conflicts) {
851
+ super(`${conflicts.length} link target collision(s) under --strict`);
852
+ this.conflicts = conflicts;
853
+ }
854
+ conflicts;
855
+ };
856
+ async function runSync(flags = {}) {
857
+ const ctx = await loadContext(flags);
858
+ printHeader(ctx, "sync", flags);
859
+ const { plans, collisions } = await buildLinkPlans({
860
+ source: ctx.paths.source,
861
+ destRoot: ctx.paths.destRoot,
862
+ config: ctx.config
863
+ });
864
+ if (collisions.length > 0) {
865
+ reportCollisions(collisions, flags.quiet ?? false);
866
+ if (flags.strict) {
867
+ throw new StrictModeViolation(collisions);
868
+ }
869
+ }
870
+ const outcomes = await applyPlans(plans, {
871
+ ...flags.dryRun !== void 0 ? { dryRun: flags.dryRun } : {},
872
+ ...flags.force !== void 0 ? { force: flags.force } : {}
873
+ });
874
+ const targetDirs = enumerateTargetDirs(ctx).map((t) => t.targetDir);
875
+ const stale = (await Promise.all(targetDirs.map(findStaleLinks))).flat();
876
+ const removed = await removeLinks(stale, flags.dryRun ? { dryRun: true } : {});
877
+ reportOutcomes(outcomes, ctx.paths.destRoot, flags.quiet ?? false);
878
+ reportRemovals(removed, flags.quiet ?? false);
879
+ printSummary(outcomes, removed, collisions);
880
+ const errored = outcomes.filter((o) => o.status === "errored").length + removed.filter((r) => r.status === "errored").length;
881
+ if (errored > 0) process.exitCode = 1;
882
+ }
883
+ function reportCollisions(collisions, quiet) {
884
+ if (quiet) return;
885
+ for (const c of collisions) {
886
+ console.warn(
887
+ pc4.yellow(`[WARN] target collision under ${c.adapterName}/${c.assetType}: ${c.target}`)
888
+ );
889
+ for (const s of c.sources) console.warn(pc4.dim(` source: ${s}`));
890
+ }
891
+ }
892
+ function reportOutcomes(outcomes, destRoot, quiet) {
893
+ for (const o of outcomes) {
894
+ const display = path7.relative(destRoot, o.plan.target);
895
+ switch (o.status) {
896
+ case "created":
897
+ if (!quiet) console.log(`${pc4.green("[OK]")} ${display}`);
898
+ break;
899
+ case "fixed":
900
+ console.log(`${pc4.green("[FIX]")} ${display} ${pc4.dim(`(${o.message})`)}`);
901
+ break;
902
+ case "skipped":
903
+ if (!quiet) console.log(`${pc4.cyan("[SKIP]")} ${display}`);
904
+ break;
905
+ case "warned":
906
+ console.warn(`${pc4.yellow("[WARN]")} ${display} ${pc4.dim(`(${o.message ?? ""})`)}`);
907
+ break;
908
+ case "errored":
909
+ console.error(`${pc4.red("[ERR]")} ${display} ${pc4.dim(`(${o.message ?? ""})`)}`);
910
+ break;
911
+ case "dry-create":
912
+ if (!quiet) console.log(`${pc4.dim("[DRY]")} ${display}`);
913
+ break;
914
+ case "dry-fix":
915
+ console.log(`${pc4.dim("[DRY]")} ${display} ${pc4.dim(`(would fix: ${o.message})`)}`);
916
+ break;
917
+ }
918
+ }
919
+ }
920
+ function reportRemovals(removals, quiet) {
921
+ for (const r of removals) {
922
+ if (r.status === "removed" && !quiet) {
923
+ console.log(`${pc4.yellow("[CLEAN]")} ${r.path}`);
924
+ } else if (r.status === "dry-remove" && !quiet) {
925
+ console.log(`${pc4.dim("[DRY]")} would clean ${r.path}`);
926
+ } else if (r.status === "errored") {
927
+ console.error(`${pc4.red("[ERR]")} ${r.path} ${pc4.dim(`(${r.message ?? ""})`)}`);
928
+ }
929
+ }
930
+ }
931
+ function printSummary(outcomes, removals, collisions) {
932
+ const counts = {};
933
+ for (const o of outcomes) counts[o.status] = (counts[o.status] ?? 0) + 1;
934
+ const cleaned = removals.filter((r) => r.status === "removed" || r.status === "dry-remove").length;
935
+ const parts = [];
936
+ if (counts["created"]) parts.push(pc4.green(`${counts["created"]} created`));
937
+ if (counts["fixed"]) parts.push(pc4.green(`${counts["fixed"]} fixed`));
938
+ if (counts["skipped"]) parts.push(pc4.cyan(`${counts["skipped"]} skipped`));
939
+ if (counts["warned"]) parts.push(pc4.yellow(`${counts["warned"]} warned`));
940
+ if (counts["errored"]) parts.push(pc4.red(`${counts["errored"]} errored`));
941
+ if (counts["dry-create"] || counts["dry-fix"]) {
942
+ parts.push(pc4.dim(`${(counts["dry-create"] ?? 0) + (counts["dry-fix"] ?? 0)} dry-run`));
943
+ }
944
+ if (cleaned) parts.push(pc4.yellow(`${cleaned} cleaned`));
945
+ if (collisions.length) parts.push(pc4.yellow(`${collisions.length} collision(s)`));
946
+ console.log("");
947
+ console.log(parts.length === 0 ? pc4.dim(" no changes") : ` ${parts.join(", ")}`);
948
+ }
949
+
950
+ // src/commands/init.ts
951
+ var InitTargetExistsError = class extends AgentlinkError {
952
+ constructor(targetPath) {
953
+ super(`refusing to scaffold into a non-empty directory: ${targetPath}
954
+ use --force to merge into the existing files`);
955
+ }
956
+ };
957
+ var ALL_ASSET_TYPES = ["skills", "rules", "commands", "prompts", "templates", "agents"];
958
+ var DEFAULT_ASSET_TYPES = ["skills", "rules", "commands"];
959
+ var ALL_ADAPTER_NAMES = Object.keys(adapterRegistry);
960
+ var HERE = path8.dirname(fileURLToPath(import.meta.url));
961
+ var TEMPLATE_CANDIDATES = [
962
+ path8.resolve(HERE, "..", "templates", "v1"),
963
+ path8.resolve(HERE, "..", "..", "templates", "v1")
964
+ ];
965
+ async function findTemplatesDir() {
966
+ for (const candidate of TEMPLATE_CANDIDATES) {
967
+ const stat = await fs7.stat(candidate).catch(() => null);
968
+ if (stat?.isDirectory()) return candidate;
969
+ }
970
+ throw new AgentlinkError(
971
+ `agentlink templates dir not found; searched:
972
+ ${TEMPLATE_CANDIDATES.join("\n ")}`
973
+ );
974
+ }
975
+ async function runInit(flags = {}) {
976
+ const answers = await collectAnswers(flags);
977
+ if (!flags.quiet) {
978
+ console.log("");
979
+ console.log(pc5.bold("[agentlink] init"));
980
+ console.log(` target: ${answers.target}`);
981
+ console.log("");
982
+ }
983
+ const created = await scaffoldInto(answers, {
984
+ force: flags.force ?? false,
985
+ quiet: flags.quiet ?? false,
986
+ dryRun: flags.dryRun ?? false
987
+ });
988
+ if (!created.touched && !flags.quiet) {
989
+ console.log(pc5.dim(" no files written; .agents/ already populated"));
990
+ }
991
+ if (answers.targetCategory === "custom" && !flags.quiet) {
992
+ printNonDefaultLocationHint(answers.target);
993
+ }
994
+ if (!flags.quiet) console.log("");
995
+ if (flags.dryRun) {
996
+ if (!flags.quiet) {
997
+ console.log(
998
+ pc5.dim(
999
+ ` (dry-run) would run \`agentlink sync\` against ${answers.target} after scaffolding`
1000
+ )
1001
+ );
1002
+ }
1003
+ return;
1004
+ }
1005
+ await runSync({
1006
+ source: answers.target,
1007
+ ...flags.target !== void 0 ? { target: flags.target } : {},
1008
+ ...flags.user !== void 0 ? { user: flags.user } : {},
1009
+ ...flags.quiet !== void 0 ? { quiet: flags.quiet } : {}
1010
+ });
1011
+ }
1012
+ async function collectAnswers(flags) {
1013
+ const skipPrompts = flags.yes || !process.stdin.isTTY;
1014
+ let target;
1015
+ let targetCategory;
1016
+ if (flags.source !== void 0 && flags.source !== "") {
1017
+ target = path8.resolve(expandTilde3(flags.source));
1018
+ targetCategory = categorizeTarget(target);
1019
+ } else if (flags.local) {
1020
+ target = path8.resolve(process.cwd(), ".agents");
1021
+ targetCategory = "cwd";
1022
+ } else if (skipPrompts) {
1023
+ target = path8.resolve(os3.homedir(), ".agents");
1024
+ targetCategory = "home";
1025
+ } else {
1026
+ const choice = await ask({
1027
+ type: "select",
1028
+ name: "kind",
1029
+ message: "Where should agentlink put your .agents/ directory?",
1030
+ choices: [
1031
+ {
1032
+ title: "Personal global (~/.agents)",
1033
+ description: "Recommended \u2014 share assets across every project",
1034
+ value: "home"
1035
+ },
1036
+ {
1037
+ title: "Repo-local (./.agents)",
1038
+ description: "Ship a curated .agents/ inside this repository",
1039
+ value: "cwd"
1040
+ }
1041
+ ],
1042
+ initial: 0
1043
+ });
1044
+ target = choice.kind === "home" ? path8.resolve(os3.homedir(), ".agents") : path8.resolve(process.cwd(), ".agents");
1045
+ targetCategory = choice.kind;
1046
+ }
1047
+ let include;
1048
+ if (skipPrompts) {
1049
+ include = DEFAULT_ASSET_TYPES;
1050
+ } else {
1051
+ const { values } = await ask({
1052
+ type: "multiselect",
1053
+ name: "values",
1054
+ message: "Which asset types do you want to manage?",
1055
+ hint: "space to toggle, enter to confirm",
1056
+ choices: ALL_ASSET_TYPES.map((name) => ({
1057
+ title: name,
1058
+ value: name,
1059
+ selected: DEFAULT_ASSET_TYPES.includes(name)
1060
+ })),
1061
+ min: 1,
1062
+ instructions: false
1063
+ });
1064
+ include = values;
1065
+ }
1066
+ let adapters;
1067
+ if (skipPrompts) {
1068
+ adapters = ALL_ADAPTER_NAMES;
1069
+ } else {
1070
+ const { values } = await ask({
1071
+ type: "multiselect",
1072
+ name: "values",
1073
+ message: "Which AI agents do you want to target?",
1074
+ hint: "space to toggle, enter to confirm",
1075
+ choices: ALL_ADAPTER_NAMES.map((name) => ({
1076
+ title: name,
1077
+ value: name,
1078
+ selected: true
1079
+ })),
1080
+ min: 1,
1081
+ instructions: false
1082
+ });
1083
+ adapters = values;
1084
+ }
1085
+ const configCategory = targetCategory === "cwd" ? "cwd" : "home";
1086
+ const configPath = configCategory === "home" ? path8.resolve(os3.homedir(), "agentlink.config.json") : path8.resolve(process.cwd(), "agentlink.config.json");
1087
+ return { target, targetCategory, configPath, configCategory, include, adapters };
1088
+ }
1089
+ async function ask(question) {
1090
+ let cancelled = false;
1091
+ const answer = await prompts(question, {
1092
+ onCancel: () => {
1093
+ cancelled = true;
1094
+ return false;
1095
+ }
1096
+ });
1097
+ if (cancelled) throw new AgentlinkError("init cancelled");
1098
+ return answer;
1099
+ }
1100
+ function categorizeTarget(target) {
1101
+ const home = path8.resolve(os3.homedir(), ".agents");
1102
+ const cwd = path8.resolve(process.cwd(), ".agents");
1103
+ if (target === home) return "home";
1104
+ if (target === cwd) return "cwd";
1105
+ return "custom";
1106
+ }
1107
+ function expandTilde3(p) {
1108
+ if (p === "~") return os3.homedir();
1109
+ if (p.startsWith("~/")) return path8.join(os3.homedir(), p.slice(2));
1110
+ return p;
1111
+ }
1112
+ async function scaffoldInto(answers, options) {
1113
+ const { force, quiet, dryRun } = options;
1114
+ const target = answers.target;
1115
+ const stat = await fs7.stat(target).catch(() => null);
1116
+ if (stat && !stat.isDirectory()) {
1117
+ throw new AgentlinkError(`init target exists but is not a directory: ${target}`);
1118
+ }
1119
+ if (stat && !force) {
1120
+ const entries = await fs7.readdir(target);
1121
+ if (entries.length > 0) throw new InitTargetExistsError(target);
1122
+ }
1123
+ if (!dryRun) {
1124
+ await fs7.mkdir(target, { recursive: true });
1125
+ } else if (!stat && !quiet) {
1126
+ console.log(`${pc5.dim("[DRY]")} would create ${target}/`);
1127
+ }
1128
+ const templatesDir = await findTemplatesDir();
1129
+ let written = 0;
1130
+ written += await copyStaticAssets(templatesDir, target, answers.include, quiet, dryRun);
1131
+ written += await writeConfigFile(answers, quiet, dryRun);
1132
+ return { touched: written > 0 };
1133
+ }
1134
+ async function copyStaticAssets(templatesDir, target, include, quiet, dryRun) {
1135
+ let written = 0;
1136
+ const readmeSrc = path8.join(templatesDir, "README.md");
1137
+ const readmeDest = path8.join(target, "README.md");
1138
+ if (await fs7.stat(readmeSrc).catch(() => null) && !await fs7.stat(readmeDest).catch(() => null)) {
1139
+ if (dryRun) {
1140
+ if (!quiet) console.log(`${pc5.dim("[DRY]")} would create ${readmeDest}`);
1141
+ } else {
1142
+ await fs7.copyFile(readmeSrc, readmeDest);
1143
+ if (!quiet) console.log(`${pc5.green("[NEW]")} ${readmeDest}`);
1144
+ }
1145
+ written++;
1146
+ }
1147
+ for (const assetType of include) {
1148
+ const dir = path8.join(target, assetType);
1149
+ const keep = path8.join(dir, ".gitkeep");
1150
+ const keepExists = await fs7.stat(keep).catch(() => null);
1151
+ if (keepExists) continue;
1152
+ if (dryRun) {
1153
+ if (!quiet) console.log(`${pc5.dim("[DRY]")} would create ${keep}`);
1154
+ } else {
1155
+ await fs7.mkdir(dir, { recursive: true });
1156
+ await fs7.writeFile(keep, "");
1157
+ if (!quiet) console.log(`${pc5.green("[NEW]")} ${keep}`);
1158
+ }
1159
+ written++;
1160
+ }
1161
+ return written;
1162
+ }
1163
+ async function writeConfigFile(answers, quiet, dryRun) {
1164
+ const configPath = answers.configPath;
1165
+ if (await fs7.stat(configPath).catch(() => null)) {
1166
+ if (!quiet) console.log(`${pc5.cyan("[SKIP]")} ${configPath} (already exists)`);
1167
+ return 0;
1168
+ }
1169
+ if (dryRun) {
1170
+ if (!quiet) console.log(`${pc5.dim("[DRY]")} would create ${configPath}`);
1171
+ return 1;
1172
+ }
1173
+ await fs7.mkdir(path8.dirname(configPath), { recursive: true });
1174
+ await fs7.writeFile(configPath, renderConfig(answers), "utf8");
1175
+ if (!quiet) console.log(`${pc5.green("[NEW]")} ${configPath}`);
1176
+ return 1;
1177
+ }
1178
+ function renderConfig(answers) {
1179
+ const includeJson = JSON.stringify(answers.include);
1180
+ const adapterLines = ALL_ADAPTER_NAMES.map((name) => {
1181
+ const value = answers.adapters.includes(name) ? "true" : "false";
1182
+ return ` "${name}": ${value}`;
1183
+ }).join(",\n");
1184
+ return `{
1185
+ // Asset type allowlist. Default: ["skills", "rules", "commands"].
1186
+ // Add "prompts", "templates" or "agents" here to opt them in.
1187
+ "include": ${includeJson},
1188
+
1189
+ // Asset type denylist (always wins over \`include\`).
1190
+ "exclude": [],
1191
+
1192
+ // Per-adapter overrides. Each value can be:
1193
+ // - true : enable with built-in defaults
1194
+ // - false : disable even if its rootDir exists
1195
+ // - { "layout": { \u2026 } } : enable + override specific asset layouts
1196
+ //
1197
+ // Example: keep cursor commands nested instead of flattened
1198
+ // "cursor": { "layout": { "commands": { "flatten": false } } }
1199
+ "adapters": {
1200
+ ${adapterLines}
1201
+ }
1202
+ }
1203
+ `;
1204
+ }
1205
+ function printNonDefaultLocationHint(target) {
1206
+ console.log("");
1207
+ console.log(pc5.bold("Tip \u2014 your source is at a non-default location:"));
1208
+ console.log(` ${target}`);
1209
+ console.log("");
1210
+ console.log("Future invocations of `agentlink sync` need to know about it. Either:");
1211
+ console.log(` - pass ${pc5.cyan(`--source ${target}`)} each time, or`);
1212
+ console.log(` - export ${pc5.cyan(`AGENTLINK_SOURCE=${target}`)} in your shell rc, or`);
1213
+ console.log(` - symlink it: ${pc5.cyan(`ln -s ${target} ~/.agents`)}`);
1214
+ }
1215
+
1216
+ // src/commands/unlink.ts
1217
+ import pc6 from "picocolors";
1218
+ async function runUnlink(flags = {}) {
1219
+ const ctx = await loadContext(flags);
1220
+ printHeader(ctx, "unlink", flags);
1221
+ const roots = await enumerateAllAdapterRoots(ctx.paths.destRoot);
1222
+ const sourceRoot = ctx.paths.source;
1223
+ const owned = (await Promise.all(
1224
+ roots.map(({ rootDir }) => findManagedLinks(rootDir, sourceRoot, { recursive: true }))
1225
+ )).flat();
1226
+ if (owned.length === 0) {
1227
+ console.log(pc6.dim(" no agentlink-managed links found"));
1228
+ return;
1229
+ }
1230
+ const outcomes = await removeLinks(owned, flags.dryRun ? { dryRun: true } : {});
1231
+ let removed = 0;
1232
+ let dry = 0;
1233
+ let errored = 0;
1234
+ for (const o of outcomes) {
1235
+ if (o.status === "removed") {
1236
+ removed++;
1237
+ if (!flags.quiet) console.log(`${pc6.yellow("[UNLINK]")} ${o.path}`);
1238
+ } else if (o.status === "dry-remove") {
1239
+ dry++;
1240
+ if (!flags.quiet) console.log(`${pc6.dim("[DRY]")} would remove ${o.path}`);
1241
+ } else if (o.status === "errored") {
1242
+ errored++;
1243
+ console.error(`${pc6.red("[ERR]")} ${o.path} ${pc6.dim(`(${o.message ?? ""})`)}`);
1244
+ }
1245
+ }
1246
+ if (!flags.dryRun) {
1247
+ await pruneEmptyDirs(roots.map((r) => r.rootDir));
1248
+ }
1249
+ console.log("");
1250
+ const parts = [];
1251
+ if (removed) parts.push(pc6.yellow(`${removed} removed`));
1252
+ if (dry) parts.push(pc6.dim(`${dry} dry-run`));
1253
+ if (errored) parts.push(pc6.red(`${errored} errored`));
1254
+ console.log(parts.length === 0 ? pc6.dim(" no changes") : ` ${parts.join(", ")}`);
1255
+ if (errored > 0) process.exitCode = 1;
1256
+ }
1257
+
1258
+ // src/cli.ts
1259
+ var PKG_NAME = "@cmkk/agentlink";
1260
+ var PKG_VERSION = "0.1.0";
1261
+ function applyGlobalOptions(cli) {
1262
+ cli.option("--source <path>", "Override the source .agents/ location").option("-t, --target <dir>", "Run against a specific destination root").option("-u, --user", "Link into $HOME (user-level install target)").option("--config <path>", "Load a specific agentlink.config.json").option("-n, --dry-run", "Preview changes without touching the filesystem").option("-f, --force", "Replace symlinks that point somewhere unexpected").option("-q, --quiet", "Only print changes and errors").option("-v, --verbose", "Print debug-level output").option("--strict", "Treat collisions as errors instead of warnings");
1263
+ }
1264
+ async function main() {
1265
+ const cli = cac("agentlink");
1266
+ cli.command("init", "Create an empty .agents/ scaffold and run sync once").option("--local", "Scaffold into ./.agents instead of ~/.agents").option("-y, --yes", "Skip interactive prompts and use defaults (CI-friendly)").action((flags) => runInit(flags));
1267
+ cli.command("sync", "Refresh all symlinks (default command)").action((flags) => runSync(flags));
1268
+ cli.command("clean", "Remove dangling agentlink-managed symlinks").action((flags) => runClean(flags));
1269
+ cli.command("unlink", "Remove every symlink agentlink ever created").action((flags) => runUnlink(flags));
1270
+ cli.command("doctor", "Diagnose configuration, adapters, and link health").action((flags) => runDoctor(flags));
1271
+ cli.command("", "Default command (alias for `sync`)").action((flags) => runSync(flags));
1272
+ applyGlobalOptions(cli);
1273
+ cli.help();
1274
+ cli.version(PKG_VERSION);
1275
+ try {
1276
+ cli.parse(process.argv, { run: false });
1277
+ await cli.runMatchedCommand();
1278
+ } catch (err) {
1279
+ renderError(err);
1280
+ process.exit(1);
1281
+ }
1282
+ }
1283
+ function renderError(err) {
1284
+ if (err instanceof SourceNotFoundError) {
1285
+ console.error(pc7.red(`[${PKG_NAME}] no source .agents/ directory found.
1286
+ `));
1287
+ console.error("Searched:");
1288
+ for (const { origin, status } of err.searched) {
1289
+ console.error(` ${origin.padEnd(40)} ${pc7.dim(status)}`);
1290
+ }
1291
+ console.error("");
1292
+ console.error("Hints:");
1293
+ console.error(" - run `agentlink init` to scaffold ~/.agents/");
1294
+ console.error(" - run `agentlink init --local` to scaffold ./.agents/");
1295
+ console.error(" - or pass `--source <path>` explicitly");
1296
+ return;
1297
+ }
1298
+ if (err instanceof StrictModeViolation) {
1299
+ console.error(pc7.red(`[${PKG_NAME}] aborted: ${err.message}`));
1300
+ console.error(pc7.dim(" rerun without --strict to accept the warnings, or fix the source."));
1301
+ return;
1302
+ }
1303
+ if (err instanceof ConfigParseError) {
1304
+ console.error(pc7.red(`[${PKG_NAME}] ${err.message}`));
1305
+ return;
1306
+ }
1307
+ if (err instanceof InvalidPathError || err instanceof ConflictError) {
1308
+ console.error(pc7.red(`[${PKG_NAME}] ${err.message}`));
1309
+ return;
1310
+ }
1311
+ if (err instanceof AgentlinkError) {
1312
+ console.error(pc7.red(`[${PKG_NAME}] ${err.message}`));
1313
+ return;
1314
+ }
1315
+ const msg = err instanceof Error ? err.message : String(err);
1316
+ console.error(pc7.red(`[${PKG_NAME}] ${msg}`));
1317
+ if (process.env["AGENTLINK_DEBUG"]) {
1318
+ console.error(err);
1319
+ }
1320
+ }
1321
+ await main();
1322
+ //# sourceMappingURL=cli.js.map