library-skills 0.0.1 → 0.0.2

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/ts/dist/cli.js CHANGED
@@ -1,7 +1,1315 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // ts/src/cli.ts
4
+ import checkbox from "@inquirer/checkbox";
5
+ import { Command } from "commander";
6
+ import { statSync as statSync4 } from "fs";
7
+ import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve4 } from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
9
+
10
+ // ts/src/deps.ts
11
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
12
+ import { join as join2 } from "path";
13
+ import { parse as parse2 } from "smol-toml";
14
+
15
+ // ts/src/scanner.ts
16
+ import {
17
+ existsSync,
18
+ lstatSync,
19
+ readFileSync,
20
+ readdirSync,
21
+ realpathSync,
22
+ statSync
23
+ } from "fs";
2
24
  import {
3
- main
4
- } from "./chunk-L5NY3HR3.js";
25
+ basename,
26
+ dirname,
27
+ isAbsolute,
28
+ join,
29
+ relative,
30
+ resolve
31
+ } from "path";
32
+ import { fileURLToPath } from "url";
33
+ import { parse } from "csv-parse/sync";
34
+ var SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
35
+ function scanPythonDistributions(sitePackages) {
36
+ const result = { skills: [], warnings: [] };
37
+ const seenSkillDirs = /* @__PURE__ */ new Set();
38
+ if (!isDirectory(sitePackages)) {
39
+ result.warnings.push(`Site-packages directory not found: ${sitePackages}`);
40
+ return result;
41
+ }
42
+ for (const distInfo of readdirSync(sitePackages).filter((entry) => entry.endsWith(".dist-info")).sort().map((entry) => join(sitePackages, entry))) {
43
+ if (!isDirectory(distInfo)) {
44
+ continue;
45
+ }
46
+ const dist = readDistributionInfo(distInfo);
47
+ if (dist === null) {
48
+ result.warnings.push(
49
+ `Skipping invalid distribution metadata: ${distInfo}`
50
+ );
51
+ continue;
52
+ }
53
+ const found = scanDistributionRecords({
54
+ sitePackages,
55
+ distInfo,
56
+ packageName: dist.name,
57
+ packageVersion: dist.version,
58
+ seenSkillDirs
59
+ });
60
+ result.skills.push(...found.skills);
61
+ result.warnings.push(...found.warnings);
62
+ if (found.skills.length === 0) {
63
+ const fallback = scanEditableDirectUrl({
64
+ distInfo,
65
+ packageName: dist.name,
66
+ packageVersion: dist.version,
67
+ seenSkillDirs
68
+ });
69
+ result.skills.push(...fallback.skills);
70
+ result.warnings.push(...fallback.warnings);
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ function scanNodePackages(nodeModules) {
76
+ const result = { skills: [], warnings: [] };
77
+ const seenSkillDirs = /* @__PURE__ */ new Set();
78
+ if (!isDirectory(nodeModules)) {
79
+ result.warnings.push(`node_modules directory not found: ${nodeModules}`);
80
+ return result;
81
+ }
82
+ for (const packageRoot of iterNodePackageRoots(nodeModules)) {
83
+ const packageInfo = readNodePackageInfo(packageRoot);
84
+ if (packageInfo === null) {
85
+ result.warnings.push(`Skipping invalid package metadata: ${packageRoot}`);
86
+ continue;
87
+ }
88
+ for (const skillMd of findNodeSkillMarkdownFiles(packageRoot)) {
89
+ const skillDir = realpathOrResolve(dirname(skillMd));
90
+ if (seenSkillDirs.has(skillDir)) {
91
+ continue;
92
+ }
93
+ seenSkillDirs.add(skillDir);
94
+ const [skill, warning] = loadSkill({
95
+ skillDir,
96
+ skillMd: realpathOrResolve(skillMd),
97
+ packageName: packageInfo.name,
98
+ packageVersion: packageInfo.version
99
+ });
100
+ if (skill) {
101
+ result.skills.push(skill);
102
+ } else if (warning) {
103
+ result.warnings.push(warning);
104
+ }
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ function iterNodePackageRoots(nodeModules) {
110
+ const packageRoots = [];
111
+ for (const entry of readdirSync(nodeModules).sort()) {
112
+ const fullPath = join(nodeModules, entry);
113
+ if (!isDirectory(fullPath)) {
114
+ continue;
115
+ }
116
+ if (entry.startsWith("@")) {
117
+ for (const scopedEntry of readdirSync(fullPath).sort()) {
118
+ const scopedPath = join(fullPath, scopedEntry);
119
+ if (isDirectory(scopedPath)) {
120
+ packageRoots.push(scopedPath);
121
+ }
122
+ }
123
+ } else {
124
+ packageRoots.push(fullPath);
125
+ }
126
+ }
127
+ return packageRoots;
128
+ }
129
+ function readNodePackageInfo(packageRoot) {
130
+ let data;
131
+ try {
132
+ data = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
133
+ } catch {
134
+ return null;
135
+ }
136
+ if (!isRecord(data)) {
137
+ return null;
138
+ }
139
+ const name = data["name"];
140
+ if (typeof name !== "string" || name === "") {
141
+ return null;
142
+ }
143
+ const version = data["version"];
144
+ return { name, version: typeof version === "string" ? version : "" };
145
+ }
146
+ function findNodeSkillMarkdownFiles(packageRoot) {
147
+ const skillsRoot = join(packageRoot, ".agents", "skills");
148
+ if (!isDirectory(skillsRoot)) {
149
+ return [];
150
+ }
151
+ return readdirSync(skillsRoot).sort().map((entry) => join(skillsRoot, entry, "SKILL.md")).filter((skillMd) => existsSync(skillMd));
152
+ }
153
+ function readDistributionInfo(distInfo) {
154
+ let metadataText;
155
+ try {
156
+ metadataText = readFileSync(join(distInfo, "METADATA"), "utf8");
157
+ } catch {
158
+ return null;
159
+ }
160
+ const headers = parseMetadataHeaders(metadataText);
161
+ const name = headers.get("name");
162
+ if (!name) {
163
+ return null;
164
+ }
165
+ return { name, version: headers.get("version") ?? "" };
166
+ }
167
+ function parseMetadataHeaders(text) {
168
+ const headers = /* @__PURE__ */ new Map();
169
+ let currentKey;
170
+ for (const line of text.split(/\r?\n/)) {
171
+ if (line === "") {
172
+ break;
173
+ }
174
+ if (/^\s/.test(line) && currentKey) {
175
+ headers.set(
176
+ currentKey,
177
+ `${headers.get(currentKey) ?? ""} ${line.trim()}`
178
+ );
179
+ continue;
180
+ }
181
+ const index = line.indexOf(":");
182
+ if (index === -1) {
183
+ continue;
184
+ }
185
+ currentKey = line.slice(0, index).trim().toLowerCase();
186
+ headers.set(currentKey, line.slice(index + 1).trim());
187
+ }
188
+ return headers;
189
+ }
190
+ function scanDistributionRecords({
191
+ sitePackages,
192
+ distInfo,
193
+ packageName,
194
+ packageVersion,
195
+ seenSkillDirs
196
+ }) {
197
+ const result = { skills: [], warnings: [] };
198
+ let recordText;
199
+ try {
200
+ recordText = readFileSync(join(distInfo, "RECORD"), "utf8");
201
+ } catch {
202
+ return result;
203
+ }
204
+ const rows = parse(recordText, {
205
+ relaxColumnCount: true,
206
+ skipEmptyLines: true
207
+ });
208
+ for (const row of rows) {
209
+ const installedPath = row[0];
210
+ if (!isSkillFileRecord(installedPath)) {
211
+ continue;
212
+ }
213
+ const skillMd = resolve(sitePackages, installedPath);
214
+ const skillDir = dirname(skillMd);
215
+ const resolvedSkillDir = realpathOrResolve(skillDir);
216
+ if (seenSkillDirs.has(resolvedSkillDir)) {
217
+ continue;
218
+ }
219
+ seenSkillDirs.add(resolvedSkillDir);
220
+ const [skill, warning] = loadSkill({
221
+ skillDir,
222
+ skillMd,
223
+ packageName,
224
+ packageVersion
225
+ });
226
+ if (skill) {
227
+ result.skills.push(skill);
228
+ } else if (warning) {
229
+ result.warnings.push(warning);
230
+ }
231
+ }
232
+ return result;
233
+ }
234
+ function isSkillFileRecord(installedPath) {
235
+ const parts = installedPath.split("/").filter((part) => part.length > 0);
236
+ for (const [index, part] of parts.entries()) {
237
+ if (part !== ".agents") {
238
+ continue;
239
+ }
240
+ if (parts.length > index + 3 && parts[index + 1] === "skills" && parts.at(-1) === "SKILL.md") {
241
+ return true;
242
+ }
243
+ }
244
+ return false;
245
+ }
246
+ function scanEditableDirectUrl({
247
+ distInfo,
248
+ packageName,
249
+ packageVersion,
250
+ seenSkillDirs
251
+ }) {
252
+ const result = { skills: [], warnings: [] };
253
+ const sourceRoot = readEditableSourceRoot(distInfo);
254
+ if (sourceRoot === null) {
255
+ return result;
256
+ }
257
+ for (const skillMd of findSkillMarkdownFiles(sourceRoot)) {
258
+ const resolvedSkillMd = realpathOrResolve(skillMd);
259
+ if (!isRelativeTo(resolvedSkillMd, sourceRoot)) {
260
+ continue;
261
+ }
262
+ const skillDir = dirname(resolvedSkillMd);
263
+ if (seenSkillDirs.has(skillDir)) {
264
+ continue;
265
+ }
266
+ seenSkillDirs.add(skillDir);
267
+ const [skill, warning] = loadSkill({
268
+ skillDir,
269
+ skillMd: resolvedSkillMd,
270
+ packageName,
271
+ packageVersion
272
+ });
273
+ if (skill) {
274
+ result.skills.push(skill);
275
+ } else if (warning) {
276
+ result.warnings.push(warning);
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ function readEditableSourceRoot(distInfo) {
282
+ let data;
283
+ try {
284
+ data = JSON.parse(readFileSync(join(distInfo, "direct_url.json"), "utf8"));
285
+ } catch {
286
+ return null;
287
+ }
288
+ if (!isRecord(data)) {
289
+ return null;
290
+ }
291
+ const dirInfo = data["dir_info"];
292
+ if (!isRecord(dirInfo) || dirInfo["editable"] !== true) {
293
+ return null;
294
+ }
295
+ const url = data["url"];
296
+ if (typeof url !== "string") {
297
+ return null;
298
+ }
299
+ let sourceRoot;
300
+ try {
301
+ sourceRoot = fileURLToPath(url);
302
+ } catch {
303
+ return null;
304
+ }
305
+ return isDirectory(sourceRoot) ? realpathOrResolve(sourceRoot) : null;
306
+ }
307
+ function findSkillMarkdownFiles(root) {
308
+ const found = [];
309
+ function walk(directory) {
310
+ let entries;
311
+ try {
312
+ entries = readdirSync(directory).sort();
313
+ } catch {
314
+ return;
315
+ }
316
+ for (const entry of entries) {
317
+ const fullPath = join(directory, entry);
318
+ let stat;
319
+ try {
320
+ stat = lstatSync(fullPath);
321
+ } catch {
322
+ continue;
323
+ }
324
+ if (stat.isSymbolicLink()) {
325
+ if (entry === "SKILL.md" && isSkillMarkdownPath(root, fullPath)) {
326
+ found.push(fullPath);
327
+ }
328
+ continue;
329
+ }
330
+ if (stat.isDirectory()) {
331
+ walk(fullPath);
332
+ } else if (entry === "SKILL.md" && isSkillMarkdownPath(root, fullPath)) {
333
+ found.push(fullPath);
334
+ }
335
+ }
336
+ }
337
+ walk(root);
338
+ return found;
339
+ }
340
+ function loadSkill({
341
+ skillDir,
342
+ skillMd,
343
+ packageName,
344
+ packageVersion
345
+ }) {
346
+ const [metadata, warning] = parseSkillFrontmatter(skillMd);
347
+ if (warning) {
348
+ return [null, `${skillMd}: ${warning}`];
349
+ }
350
+ const name = metadata["name"] ?? "";
351
+ const description = metadata["description"] ?? "";
352
+ const validationError = validateSkillMetadata({
353
+ name,
354
+ description,
355
+ parentDirName: basename(skillDir)
356
+ });
357
+ if (validationError) {
358
+ return [null, `${skillMd}: ${validationError}`];
359
+ }
360
+ return [
361
+ {
362
+ name,
363
+ description,
364
+ path: skillMd,
365
+ packageName,
366
+ packageVersion,
367
+ skillDir
368
+ },
369
+ null
370
+ ];
371
+ }
372
+ function parseSkillFrontmatter(skillMd) {
373
+ let text;
374
+ try {
375
+ text = readFileSync(skillMd, "utf8");
376
+ } catch (error) {
377
+ return [{}, `could not read SKILL.md (${String(error)})`];
378
+ }
379
+ if (!text.startsWith("---")) {
380
+ return [{}, "missing YAML frontmatter"];
381
+ }
382
+ const end = text.indexOf("\n---", 3);
383
+ if (end === -1) {
384
+ return [{}, "unterminated YAML frontmatter"];
385
+ }
386
+ const metadata = {};
387
+ for (const line of text.slice(3, end).split(/\r?\n/)) {
388
+ const stripped = line.trim();
389
+ if (stripped === "" || stripped.startsWith("#")) {
390
+ continue;
391
+ }
392
+ const separator = stripped.indexOf(":");
393
+ if (separator === -1) {
394
+ continue;
395
+ }
396
+ const key = stripped.slice(0, separator).trim();
397
+ if (key === "name" || key === "description") {
398
+ metadata[key] = stripped.slice(separator + 1).trim().replace(/^["']|["']$/g, "");
399
+ }
400
+ }
401
+ return [metadata, null];
402
+ }
403
+ function validateSkillMetadata({
404
+ name,
405
+ description,
406
+ parentDirName
407
+ }) {
408
+ if (!name) {
409
+ return "missing required 'name' field";
410
+ }
411
+ if (!SKILL_NAME_RE.test(name) || name.includes("--")) {
412
+ return "invalid 'name' field; use lowercase letters, numbers, and hyphens only";
413
+ }
414
+ if (name !== parentDirName) {
415
+ return `'name' field must match parent directory name (${parentDirName})`;
416
+ }
417
+ if (!description) {
418
+ return "missing required 'description' field";
419
+ }
420
+ if (description.length > 1024) {
421
+ return "'description' field must be at most 1024 characters";
422
+ }
423
+ return null;
424
+ }
425
+ function normalizePackageName(name) {
426
+ return name.replace(/[-_.]+/g, "-").toLowerCase();
427
+ }
428
+ function isDirectory(path) {
429
+ try {
430
+ return statSync(path).isDirectory();
431
+ } catch {
432
+ return false;
433
+ }
434
+ }
435
+ function isRelativeTo(path, parent) {
436
+ const childPath = resolve(path);
437
+ const parentPath = resolve(parent);
438
+ const childRelative = relative(parentPath, childPath);
439
+ return childRelative === "" || !childRelative.startsWith("..") && !isAbsolute(childRelative);
440
+ }
441
+ function isSkillMarkdownPath(root, skillMd) {
442
+ const relativePath = relative(root, skillMd);
443
+ if (relativePath === "" || relativePath.startsWith("..")) {
444
+ return false;
445
+ }
446
+ const parts = relativePath.split(/[\\/]+/);
447
+ return isSkillFileRecord(parts.join("/"));
448
+ }
449
+ function realpathOrResolve(path) {
450
+ try {
451
+ return realpathSync(path);
452
+ } catch {
453
+ return resolve(path);
454
+ }
455
+ }
456
+ function isRecord(value) {
457
+ return typeof value === "object" && value !== null && !Array.isArray(value);
458
+ }
459
+
460
+ // ts/src/deps.ts
461
+ function getPythonTopLevelDeps(projectRoot) {
462
+ const pyproject = join2(projectRoot, "pyproject.toml");
463
+ if (!existsSync2(pyproject)) {
464
+ return null;
465
+ }
466
+ let data;
467
+ try {
468
+ data = parse2(readFileSync2(pyproject, "utf8"));
469
+ } catch {
470
+ return null;
471
+ }
472
+ const deps = /* @__PURE__ */ new Set();
473
+ const project = data["project"];
474
+ if (!isRecord2(project)) {
475
+ return deps;
476
+ }
477
+ const dependencies = project["dependencies"];
478
+ if (Array.isArray(dependencies)) {
479
+ extractDepsFromSpecs(dependencies, deps);
480
+ }
481
+ const optionalDependencies = project["optional-dependencies"];
482
+ if (isRecord2(optionalDependencies)) {
483
+ for (const groupDependencies of Object.values(optionalDependencies)) {
484
+ if (Array.isArray(groupDependencies)) {
485
+ extractDepsFromSpecs(groupDependencies, deps);
486
+ }
487
+ }
488
+ }
489
+ return deps;
490
+ }
491
+ function getNodeTopLevelDeps(projectRoot) {
492
+ const packageJson = join2(projectRoot, "package.json");
493
+ if (!existsSync2(packageJson)) {
494
+ return null;
495
+ }
496
+ let data;
497
+ try {
498
+ data = JSON.parse(readFileSync2(packageJson, "utf8"));
499
+ } catch {
500
+ return null;
501
+ }
502
+ if (!isRecord2(data)) {
503
+ return /* @__PURE__ */ new Set();
504
+ }
505
+ const deps = /* @__PURE__ */ new Set();
506
+ for (const field of [
507
+ "dependencies",
508
+ "devDependencies",
509
+ "optionalDependencies",
510
+ "peerDependencies"
511
+ ]) {
512
+ const dependencies = data[field];
513
+ if (!isRecord2(dependencies)) {
514
+ continue;
515
+ }
516
+ for (const packageName of Object.keys(dependencies)) {
517
+ deps.add(normalizePackageName(packageName));
518
+ }
519
+ }
520
+ return deps;
521
+ }
522
+ function getTopLevelDeps(projectRoot) {
523
+ const dependencySets = [
524
+ getPythonTopLevelDeps(projectRoot),
525
+ getNodeTopLevelDeps(projectRoot)
526
+ ].filter((deps) => deps !== null);
527
+ if (dependencySets.length === 0) {
528
+ return null;
529
+ }
530
+ return new Set(dependencySets.flatMap((deps) => [...deps]));
531
+ }
532
+ function extractDepsFromSpecs(depSpecs, deps) {
533
+ for (const depSpec of depSpecs) {
534
+ if (typeof depSpec !== "string") {
535
+ continue;
536
+ }
537
+ const packageName = depSpec.split(/[>=<![\];,\s]/)[0]?.trim();
538
+ if (packageName && !packageName.startsWith("#")) {
539
+ deps.add(normalizePackageName(packageName));
540
+ }
541
+ }
542
+ }
543
+ function isRecord2(value) {
544
+ return typeof value === "object" && value !== null && !Array.isArray(value);
545
+ }
546
+
547
+ // ts/src/installer.ts
548
+ import {
549
+ cpSync,
550
+ existsSync as existsSync3,
551
+ lstatSync as lstatSync2,
552
+ mkdirSync,
553
+ readdirSync as readdirSync2,
554
+ readlinkSync,
555
+ statSync as statSync2,
556
+ symlinkSync,
557
+ unlinkSync
558
+ } from "fs";
559
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2 } from "path";
560
+ var UNIVERSAL_SKILLS_DIR = ".agents/skills";
561
+ var CLAUDE_SKILLS_DIR = ".claude/skills";
562
+ var InstallError = class extends Error {
563
+ constructor(message) {
564
+ super(message);
565
+ this.name = "InstallError";
566
+ }
567
+ };
568
+ function getTargetDirs(projectRoot, options = {}) {
569
+ const targets = [
570
+ { name: "universal", path: join3(projectRoot, UNIVERSAL_SKILLS_DIR) }
571
+ ];
572
+ if (options.includeClaude) {
573
+ targets.push({
574
+ name: "claude-compatible",
575
+ path: join3(projectRoot, CLAUDE_SKILLS_DIR)
576
+ });
577
+ }
578
+ return targets;
579
+ }
580
+ function installSkill(skill, targetDir, options = {}) {
581
+ const dest = join3(targetDir, skill.name);
582
+ const source = resolve2(skill.skillDir);
583
+ if (existsSync3(dest) || isSymlink(dest)) {
584
+ if (isSymlink(dest)) {
585
+ unlinkSync(dest);
586
+ } else if (lstatSync2(dest).isFile()) {
587
+ throw new InstallError(`Cannot overwrite non-symlink file: ${dest}`);
588
+ } else if (lstatSync2(dest).isDirectory()) {
589
+ throw new InstallError(`Cannot overwrite non-symlink directory: ${dest}`);
590
+ }
591
+ }
592
+ mkdirSync(dirname2(dest), { recursive: true });
593
+ if (options.copy) {
594
+ cpSync(source, dest, { recursive: true });
595
+ } else {
596
+ symlinkSync(getSymlinkTarget({ source, dest }), dest, "dir");
597
+ }
598
+ return dest;
599
+ }
600
+ function uninstallSkill(skillName, targetDir) {
601
+ const dest = join3(targetDir, skillName);
602
+ if (isSymlink(dest)) {
603
+ unlinkSync(dest);
604
+ return true;
605
+ }
606
+ return false;
607
+ }
608
+ function listInstalledSkills(targetDir) {
609
+ if (!isDirectory2(targetDir)) {
610
+ return [];
611
+ }
612
+ return readdirSync2(targetDir).sort().flatMap((name) => {
613
+ const path = join3(targetDir, name);
614
+ if (!isSymlink(path) && !isDirectory2(path)) {
615
+ return [];
616
+ }
617
+ const type = isSymlink(path) ? "symlink" : "directory";
618
+ return [
619
+ {
620
+ name,
621
+ type,
622
+ path,
623
+ target: type === "symlink" ? resolveSymlink(path) : null,
624
+ hasSkillMd: existsSync3(join3(path, "SKILL.md"))
625
+ }
626
+ ];
627
+ });
628
+ }
629
+ function getSymlinkTarget({ source, dest }) {
630
+ const relativeTarget = relative2(dirname2(resolve2(dest)), source);
631
+ return relativeTarget === "" ? source : relativeTarget;
632
+ }
633
+ function resolveSymlink(path) {
634
+ try {
635
+ return resolve2(dirname2(path), readlinkSync(path));
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+ function isSymlink(path) {
641
+ try {
642
+ return lstatSync2(path).isSymbolicLink();
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+ function isDirectory2(path) {
648
+ try {
649
+ return statSync2(path).isDirectory();
650
+ } catch {
651
+ return false;
652
+ }
653
+ }
654
+
655
+ // ts/src/python-env.ts
656
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "fs";
657
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3, sep } from "path";
658
+ function findProjectRoot(cwd) {
659
+ for (const directory of ancestors(cwd)) {
660
+ if (isFile(join4(directory, "pyproject.toml")) || isFile(join4(directory, "uv.lock")) || isFile(join4(directory, ".venv", "pyvenv.cfg")) || isFile(join4(directory, "package.json")) || isDirectory3(join4(directory, "node_modules"))) {
661
+ return directory;
662
+ }
663
+ }
664
+ return null;
665
+ }
666
+ function findVenv(cwd = process.cwd()) {
667
+ const projectRoot = findProjectRoot(cwd) ?? cwd;
668
+ const uvProjectEnvironment = process.env["UV_PROJECT_ENVIRONMENT"];
669
+ if (uvProjectEnvironment) {
670
+ const path = isAbsolute2(uvProjectEnvironment) ? uvProjectEnvironment : join4(projectRoot, uvProjectEnvironment);
671
+ if (isVenvDir(path)) {
672
+ return path;
673
+ }
674
+ }
675
+ for (const directory of ancestors(cwd)) {
676
+ const dotVenv = join4(directory, ".venv");
677
+ if (isVenvDir(dotVenv)) {
678
+ return dotVenv;
679
+ }
680
+ }
681
+ const virtualEnv = process.env["VIRTUAL_ENV"];
682
+ if (virtualEnv && isVenvDir(virtualEnv) && isRelativeTo2(virtualEnv, projectRoot)) {
683
+ return virtualEnv;
684
+ }
685
+ const condaPrefix = process.env["CONDA_PREFIX"];
686
+ if (condaPrefix && isDirectory3(condaPrefix)) {
687
+ return condaPrefix;
688
+ }
689
+ return null;
690
+ }
691
+ function getSitePackagesDir(venvPath) {
692
+ const windowsSitePackages = join4(venvPath, "Lib", "site-packages");
693
+ if (isDirectory3(windowsSitePackages)) {
694
+ return windowsSitePackages;
695
+ }
696
+ for (const libName of ["lib", "lib64"]) {
697
+ const libDir = join4(venvPath, libName);
698
+ if (!isDirectory3(libDir)) {
699
+ continue;
700
+ }
701
+ for (const child of readdirSync3(libDir).sort().reverse()) {
702
+ const sitePackages = join4(libDir, child, "site-packages");
703
+ if (child.startsWith("python") && isDirectory3(sitePackages)) {
704
+ return sitePackages;
705
+ }
706
+ }
707
+ }
708
+ return null;
709
+ }
710
+ function findNodeModules(cwd = process.cwd()) {
711
+ for (const directory of ancestors(cwd)) {
712
+ const nodeModules = join4(directory, "node_modules");
713
+ if (isDirectory3(nodeModules)) {
714
+ return nodeModules;
715
+ }
716
+ }
717
+ return null;
718
+ }
719
+ function isVenvDir(directory) {
720
+ return isFile(join4(directory, "pyvenv.cfg"));
721
+ }
722
+ function ancestors(start) {
723
+ const result = [];
724
+ let directory = resolve3(start);
725
+ while (true) {
726
+ result.push(directory);
727
+ const parent = dirname3(directory);
728
+ if (parent === directory) {
729
+ break;
730
+ }
731
+ directory = parent;
732
+ }
733
+ return result;
734
+ }
735
+ function isRelativeTo2(path, parent) {
736
+ const normalizedPath = resolve3(path);
737
+ const normalizedParent = resolve3(parent);
738
+ return normalizedPath === normalizedParent || normalizedPath.startsWith(`${normalizedParent}${sep}`);
739
+ }
740
+ function isFile(path) {
741
+ try {
742
+ return statSync3(path).isFile();
743
+ } catch {
744
+ return false;
745
+ }
746
+ }
747
+ function isDirectory3(path) {
748
+ try {
749
+ return statSync3(path).isDirectory();
750
+ } catch {
751
+ return false;
752
+ }
753
+ }
5
754
 
6
755
  // ts/src/cli.ts
7
- main();
756
+ function getProjectContext(cwd = process.cwd()) {
757
+ const projectRoot = findProjectRoot(cwd) ?? cwd;
758
+ const targetEnvironment = findVenv(cwd);
759
+ const sitePackagesDir = targetEnvironment === null ? null : getSitePackagesDir(targetEnvironment);
760
+ const nodeModulesDir = findNodeModules(cwd);
761
+ return { cwd, projectRoot, targetEnvironment, sitePackagesDir, nodeModulesDir };
762
+ }
763
+ function scanContext(context) {
764
+ const result = {
765
+ skills: [],
766
+ warnings: [],
767
+ environmentPath: context.targetEnvironment ?? void 0
768
+ };
769
+ if (context.sitePackagesDir !== null) {
770
+ const pythonResult = scanPythonDistributions(context.sitePackagesDir);
771
+ result.skills.push(...pythonResult.skills);
772
+ result.warnings.push(...pythonResult.warnings);
773
+ }
774
+ if (context.nodeModulesDir !== null) {
775
+ const nodeResult = scanNodePackages(context.nodeModulesDir);
776
+ result.skills.push(...nodeResult.skills);
777
+ result.warnings.push(...nodeResult.warnings);
778
+ }
779
+ if (context.sitePackagesDir === null && context.nodeModulesDir === null) {
780
+ result.warnings.push(
781
+ "No target Python environment with site-packages or node_modules was found. Run from a project root after installing dependencies, for example with 'uv sync' for Python or 'npm install' for Node.js."
782
+ );
783
+ }
784
+ return result;
785
+ }
786
+ function topLevelSkills({
787
+ context,
788
+ skills,
789
+ includeAll
790
+ }) {
791
+ if (includeAll) {
792
+ return skills;
793
+ }
794
+ const topLevelDeps = getTopLevelDeps(context.projectRoot);
795
+ if (topLevelDeps === null) {
796
+ return skills;
797
+ }
798
+ return skills.filter(
799
+ (skill) => topLevelDeps.has(normalizePackageName(skill.packageName))
800
+ );
801
+ }
802
+ function printWarnings(warnings) {
803
+ for (const warning of warnings) {
804
+ console.log(`Warning: ${warning}`);
805
+ }
806
+ }
807
+ function displayPath(path, projectRoot) {
808
+ if (!path) {
809
+ return "";
810
+ }
811
+ const relativePath = relative3(resolve4(projectRoot), resolve4(path));
812
+ if (relativePath === "") {
813
+ return ".";
814
+ }
815
+ if (!relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
816
+ return relativePath;
817
+ }
818
+ return path;
819
+ }
820
+ function printContext(context) {
821
+ console.log(`Project root: ${context.projectRoot}`);
822
+ console.log(
823
+ `Target Python environment: ${context.targetEnvironment ? displayPath(context.targetEnvironment, context.projectRoot) : "not found"}`
824
+ );
825
+ if (context.sitePackagesDir) {
826
+ console.log(
827
+ `Site-packages: ${displayPath(
828
+ context.sitePackagesDir,
829
+ context.projectRoot
830
+ )}`
831
+ );
832
+ }
833
+ if (context.nodeModulesDir) {
834
+ console.log(
835
+ `node_modules: ${displayPath(context.nodeModulesDir, context.projectRoot)}`
836
+ );
837
+ }
838
+ }
839
+ function printSkills(skills) {
840
+ if (skills.length === 0) {
841
+ console.log("No skills found in installed packages.");
842
+ return;
843
+ }
844
+ printTable(
845
+ ["Skill", "Package", "Version", "Description"],
846
+ skills.map((skill) => ({
847
+ Skill: skill.name,
848
+ Package: skill.packageName,
849
+ Version: skill.packageVersion,
850
+ Description: skill.description
851
+ }))
852
+ );
853
+ }
854
+ function scanJsonPayload({
855
+ context,
856
+ skills,
857
+ result
858
+ }) {
859
+ return {
860
+ project_root: context.projectRoot,
861
+ target_environment: context.targetEnvironment ?? "",
862
+ node_modules: context.nodeModulesDir ?? "",
863
+ skills: skills.map((skill) => ({
864
+ name: skill.name,
865
+ description: skill.description,
866
+ package: skill.packageName,
867
+ version: skill.packageVersion,
868
+ path: skill.skillDir
869
+ })),
870
+ warnings: result.warnings
871
+ };
872
+ }
873
+ function printTable(columns, rows) {
874
+ const widths = columns.map(
875
+ (column) => Math.max(column.length, ...rows.map((row) => row[column].length))
876
+ );
877
+ const formatRow = (row) => columns.map((column, index) => row[column].padEnd(widths[index])).join(" ");
878
+ console.log(formatRow(Object.fromEntries(columns.map((column) => [column, column]))));
879
+ console.log(widths.map((width) => "-".repeat(width)).join(" "));
880
+ for (const row of rows) {
881
+ console.log(formatRow(row));
882
+ }
883
+ }
884
+ function findCollisions(skills) {
885
+ const counts = /* @__PURE__ */ new Map();
886
+ for (const skill of skills) {
887
+ counts.set(skill.name, (counts.get(skill.name) ?? 0) + 1);
888
+ }
889
+ return new Set(
890
+ [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name)
891
+ );
892
+ }
893
+ function filterInstallableSkills({
894
+ skills,
895
+ selectedNames,
896
+ includeAll
897
+ }) {
898
+ const collisions = findCollisions(skills);
899
+ if (collisions.size > 0) {
900
+ console.log(
901
+ `Skipping colliding skill names: ${[...collisions].sort().join(", ")}`
902
+ );
903
+ }
904
+ const installable = skills.filter((skill) => !collisions.has(skill.name));
905
+ if (selectedNames.length > 0) {
906
+ const selected = new Set(selectedNames);
907
+ return installable.filter((skill) => selected.has(skill.name));
908
+ }
909
+ return includeAll ? installable : [];
910
+ }
911
+ function installedStatuses({
912
+ targets,
913
+ skills
914
+ }) {
915
+ const skillsByDir = new Map(
916
+ skills.map((skill) => [resolve4(skill.skillDir), skill])
917
+ );
918
+ const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
919
+ const statuses = [];
920
+ for (const target of targets) {
921
+ for (const installed of listInstalledSkills(target.path)) {
922
+ const skill = installed.target ? skillsByDir.get(resolve4(installed.target)) : null;
923
+ let matchedSkill = skill ?? null;
924
+ let status = "hand-authored";
925
+ if (installed.type === "symlink") {
926
+ if (installed.target === null || !exists(installed.target)) {
927
+ status = "broken";
928
+ matchedSkill = skillsByName.get(installed.name) ?? null;
929
+ } else if (skill) {
930
+ status = installed.name === skill.name ? "up to date" : "name mismatch";
931
+ } else if (skillsByName.has(installed.name)) {
932
+ matchedSkill = skillsByName.get(installed.name) ?? null;
933
+ status = "outdated";
934
+ } else {
935
+ status = "orphaned";
936
+ }
937
+ }
938
+ statuses.push({
939
+ target,
940
+ name: installed.name,
941
+ type: installed.type,
942
+ path: installed.path,
943
+ targetPath: installed.target,
944
+ status,
945
+ skill: matchedSkill
946
+ });
947
+ }
948
+ }
949
+ const installedNames = new Set(
950
+ statuses.filter(
951
+ (status) => ["up to date", "outdated", "broken", "name mismatch"].includes(
952
+ status.status
953
+ )
954
+ ).map((status) => `${status.target.name}\0${status.name}`)
955
+ );
956
+ for (const target of targets) {
957
+ for (const skill of skills) {
958
+ if (!installedNames.has(`${target.name}\0${skill.name}`)) {
959
+ statuses.push({
960
+ target,
961
+ name: skill.name,
962
+ type: "missing",
963
+ path: `${target.path}/${skill.name}`,
964
+ targetPath: skill.skillDir,
965
+ status: "new",
966
+ skill
967
+ });
968
+ }
969
+ }
970
+ }
971
+ return statuses;
972
+ }
973
+ function printStatusTable(statuses, projectRoot) {
974
+ printTable(
975
+ ["Target", "Skill", "Status", "Path", "Source"],
976
+ statuses.map((status) => ({
977
+ Target: status.target.name,
978
+ Skill: status.name,
979
+ Status: status.status,
980
+ Path: displayPath(status.path, projectRoot),
981
+ Source: displayPath(status.targetPath, projectRoot)
982
+ }))
983
+ );
984
+ }
985
+ function printInstalledSkillsTable(statuses, projectRoot) {
986
+ const installed = statuses.filter((status) => status.type !== "missing");
987
+ if (installed.length === 0) {
988
+ console.log("No skills installed.");
989
+ return;
990
+ }
991
+ printStatusTable(installed, projectRoot);
992
+ }
993
+ async function selectSkillsInteractive(skills) {
994
+ return checkbox({
995
+ message: "Select skills to install (press Space to select, Enter to confirm):",
996
+ choices: skills.map((skill) => ({
997
+ name: `${skill.name} (${skill.packageName})`,
998
+ value: skill
999
+ }))
1000
+ });
1001
+ }
1002
+ async function selectInstalledSkillsInteractive(statuses) {
1003
+ const removable = statuses.filter((status) => status.type === "symlink");
1004
+ if (removable.length === 0) {
1005
+ return [];
1006
+ }
1007
+ return checkbox({
1008
+ message: "Select skills to remove (press Space to select, Enter to confirm):",
1009
+ choices: removable.map((status) => ({
1010
+ name: `${status.name} [${status.target.name}]`,
1011
+ value: status
1012
+ }))
1013
+ });
1014
+ }
1015
+ function installSelected({
1016
+ skills,
1017
+ targets,
1018
+ projectRoot,
1019
+ copy = false
1020
+ }) {
1021
+ let installedCount = 0;
1022
+ for (const target of targets) {
1023
+ for (const skill of skills) {
1024
+ try {
1025
+ const dest = installSkill(skill, target.path, { copy });
1026
+ const method = copy ? "Copied" : "Symlinked";
1027
+ console.log(
1028
+ `${method}: ${skill.name} (${skill.packageName}) -> ${displayPath(
1029
+ dest,
1030
+ projectRoot
1031
+ )}`
1032
+ );
1033
+ installedCount++;
1034
+ } catch (error) {
1035
+ if (error instanceof InstallError) {
1036
+ console.log(`Skipped ${skill.name}: ${error.message}`);
1037
+ } else {
1038
+ throw error;
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ return installedCount;
1044
+ }
1045
+ async function sync(options) {
1046
+ const context = getProjectContext();
1047
+ const result = scanContext(context);
1048
+ const targets = getTargetDirs(context.projectRoot, {
1049
+ includeClaude: options.claude
1050
+ });
1051
+ printContext(context);
1052
+ console.log();
1053
+ printWarnings(result.warnings);
1054
+ if (result.warnings.length > 0) {
1055
+ console.log();
1056
+ }
1057
+ const selectedNames = options.skill ?? [];
1058
+ const collisions = findCollisions(result.skills);
1059
+ const candidateSkills = topLevelSkills({
1060
+ context,
1061
+ skills: result.skills,
1062
+ includeAll: Boolean(options.all) || selectedNames.length > 0
1063
+ });
1064
+ const candidateSkillNames = new Set(
1065
+ candidateSkills.map((skill) => skill.name)
1066
+ );
1067
+ const statuses = installedStatuses({
1068
+ targets,
1069
+ skills: result.skills.filter((skill) => !collisions.has(skill.name))
1070
+ });
1071
+ const visibleStatuses = statuses.filter(
1072
+ (status) => status.status !== "new" || candidateSkillNames.has(status.name)
1073
+ );
1074
+ if (visibleStatuses.length > 0) {
1075
+ printStatusTable(visibleStatuses, context.projectRoot);
1076
+ } else {
1077
+ console.log("No installed or discovered skills found.");
1078
+ }
1079
+ const drift = statuses.filter(
1080
+ (status) => ["broken", "outdated", "name mismatch", "orphaned"].includes(status.status)
1081
+ );
1082
+ if (options.check) {
1083
+ if (drift.length > 0 || collisions.size > 0) {
1084
+ process.exitCode = 1;
1085
+ }
1086
+ return;
1087
+ }
1088
+ for (const status of drift) {
1089
+ if (options.yes && (status.status === "broken" || status.status === "outdated") && status.skill) {
1090
+ installSkill(status.skill, status.target.path);
1091
+ }
1092
+ }
1093
+ let selected = filterInstallableSkills({
1094
+ skills: candidateSkills,
1095
+ selectedNames,
1096
+ includeAll: Boolean(options.all)
1097
+ });
1098
+ if (selected.length === 0 && !options.yes && selectedNames.length === 0 && !options.all) {
1099
+ const newSkills = visibleStatuses.filter((status) => status.status === "new" && status.skill !== null).map((status) => status.skill);
1100
+ if (newSkills.length > 0) {
1101
+ selected = await selectSkillsInteractive(newSkills);
1102
+ }
1103
+ }
1104
+ if (selected.length > 0) {
1105
+ console.log();
1106
+ const installedCount = installSelected({
1107
+ skills: selected,
1108
+ targets,
1109
+ projectRoot: context.projectRoot
1110
+ });
1111
+ console.log();
1112
+ console.log(`Installed ${installedCount} skill target(s).`);
1113
+ }
1114
+ }
1115
+ function scanCommand(options) {
1116
+ const context = getProjectContext();
1117
+ const result = scanContext(context);
1118
+ const skills = topLevelSkills({
1119
+ context,
1120
+ skills: result.skills,
1121
+ includeAll: Boolean(options.all)
1122
+ });
1123
+ if (options.json) {
1124
+ console.log(
1125
+ JSON.stringify(scanJsonPayload({ context, skills, result }), null, 2)
1126
+ );
1127
+ return;
1128
+ }
1129
+ printContext(context);
1130
+ console.log();
1131
+ printWarnings(result.warnings);
1132
+ if (result.warnings.length > 0) {
1133
+ console.log();
1134
+ }
1135
+ printSkills(skills);
1136
+ }
1137
+ function listCommand(options) {
1138
+ const context = getProjectContext();
1139
+ const result = scanContext(context);
1140
+ const skills = topLevelSkills({
1141
+ context,
1142
+ skills: result.skills,
1143
+ includeAll: Boolean(options.all)
1144
+ });
1145
+ const targets = getTargetDirs(context.projectRoot, {
1146
+ includeClaude: options.claude
1147
+ });
1148
+ const statuses = installedStatuses({ targets, skills: result.skills });
1149
+ if (options.json) {
1150
+ const payload = scanJsonPayload({ context, skills, result });
1151
+ console.log(
1152
+ JSON.stringify(
1153
+ {
1154
+ ...payload,
1155
+ installed: statuses.filter((status) => status.type !== "missing").map((status) => ({
1156
+ target: status.target.name,
1157
+ name: status.name,
1158
+ status: status.status,
1159
+ path: status.path,
1160
+ source: status.targetPath ?? ""
1161
+ }))
1162
+ },
1163
+ null,
1164
+ 2
1165
+ )
1166
+ );
1167
+ return;
1168
+ }
1169
+ printWarnings(result.warnings);
1170
+ if (options.installed) {
1171
+ printInstalledSkillsTable(statuses, context.projectRoot);
1172
+ } else {
1173
+ printSkills(skills);
1174
+ }
1175
+ }
1176
+ async function installCommand(options) {
1177
+ const context = getProjectContext();
1178
+ const result = scanContext(context);
1179
+ const selectedNames = options.skill ?? [];
1180
+ const skills = topLevelSkills({
1181
+ context,
1182
+ skills: result.skills,
1183
+ includeAll: Boolean(options.all) || selectedNames.length > 0
1184
+ });
1185
+ const targets = getTargetDirs(context.projectRoot, {
1186
+ includeClaude: options.claude
1187
+ });
1188
+ printWarnings(result.warnings);
1189
+ let selected = filterInstallableSkills({
1190
+ skills,
1191
+ selectedNames,
1192
+ includeAll: Boolean(options.all)
1193
+ });
1194
+ if (selected.length === 0 && !options.yes) {
1195
+ selected = await selectSkillsInteractive(skills);
1196
+ }
1197
+ if (selected.length === 0) {
1198
+ console.log("No skills selected.");
1199
+ return;
1200
+ }
1201
+ const installedCount = installSelected({
1202
+ skills: selected,
1203
+ targets,
1204
+ projectRoot: context.projectRoot,
1205
+ copy: options.copy
1206
+ });
1207
+ console.log();
1208
+ console.log(`Installed ${installedCount} skill target(s).`);
1209
+ }
1210
+ async function removeCommand(skillNames, options) {
1211
+ const context = getProjectContext();
1212
+ const result = scanContext(context);
1213
+ const targets = getTargetDirs(context.projectRoot, {
1214
+ includeClaude: options.claude
1215
+ });
1216
+ const statuses = installedStatuses({ targets, skills: result.skills });
1217
+ let selectedStatuses = [];
1218
+ if (skillNames.length > 0) {
1219
+ const selected = new Set(skillNames);
1220
+ selectedStatuses = statuses.filter(
1221
+ (status) => selected.has(status.name) && status.type === "symlink"
1222
+ );
1223
+ } else if (!options.yes) {
1224
+ selectedStatuses = await selectInstalledSkillsInteractive(statuses);
1225
+ }
1226
+ if (selectedStatuses.length === 0) {
1227
+ console.log("No skills selected.");
1228
+ return;
1229
+ }
1230
+ for (const status of selectedStatuses) {
1231
+ if (uninstallSkill(status.name, status.target.path)) {
1232
+ console.log(`Removed: ${status.name} (${status.target.name})`);
1233
+ } else {
1234
+ console.log(`Not found: ${status.name} (${status.target.name})`);
1235
+ }
1236
+ }
1237
+ }
1238
+ function createProgram() {
1239
+ const program = new Command().enablePositionalOptions();
1240
+ program.name("library-skills").description(
1241
+ "Discover and install agent skills from installed library packages."
1242
+ ).option("--claude", "Also manage .claude/skills alongside .agents/skills").option("-y, --yes", "Skip confirmation prompts").option("--check", "Validate only; exit 1 if installs drift").option("--all", "Install all newly discovered unmanaged skills").option(
1243
+ "-s, --skill <name>",
1244
+ "Install a specific discovered skill by name",
1245
+ collect,
1246
+ []
1247
+ ).action(async (options) => {
1248
+ await sync(options);
1249
+ });
1250
+ program.command("scan").description("Discover skills in installed packages.").option("--json", "Output as JSON").option("--all", "Include skills from transitive dependencies").action((options) => {
1251
+ scanCommand(options);
1252
+ });
1253
+ program.command("list").description("List discovered or currently installed skills.").option("--installed", "Only show installed skills").option("--json", "Output as JSON").option("--claude", "Also include .claude/skills alongside .agents/skills").option("--all", "Include skills from transitive dependencies").action((options) => {
1254
+ listCommand(options);
1255
+ });
1256
+ program.command("install").description("Install skills from installed packages.").option(
1257
+ "--claude",
1258
+ "Also install in .claude/skills alongside .agents/skills"
1259
+ ).option("-y, --yes", "Skip interactive selection").option("--all", "Install all newly discovered unmanaged skills").option(
1260
+ "-s, --skill <name>",
1261
+ "Install a specific discovered skill by name",
1262
+ collect,
1263
+ []
1264
+ ).option("--copy", "Copy files instead of creating symlinks").action(async (options) => {
1265
+ await installCommand(options);
1266
+ });
1267
+ program.command("remove").description("Remove installed symlinked skills.").argument("[skills...]", "Names of skills to remove").option(
1268
+ "--claude",
1269
+ "Also remove from .claude/skills alongside .agents/skills"
1270
+ ).option("-y, --yes", "Skip interactive selection").action(
1271
+ async (skillNames, options) => {
1272
+ await removeCommand(skillNames, options);
1273
+ }
1274
+ );
1275
+ return program;
1276
+ }
1277
+ async function main(argv = process.argv) {
1278
+ await createProgram().parseAsync(argv);
1279
+ }
1280
+ function collect(value, previous) {
1281
+ previous.push(value);
1282
+ return previous;
1283
+ }
1284
+ function exists(path) {
1285
+ try {
1286
+ statSync4(path);
1287
+ return true;
1288
+ } catch {
1289
+ return false;
1290
+ }
1291
+ }
1292
+ if (process.argv[1] && fileURLToPath2(import.meta.url) === resolve4(process.argv[1])) {
1293
+ main().catch((error) => {
1294
+ console.error(error);
1295
+ process.exit(1);
1296
+ });
1297
+ }
1298
+ var testing = {
1299
+ filterInstallableSkills,
1300
+ findCollisions,
1301
+ getProjectContext,
1302
+ installSelected,
1303
+ installedStatuses,
1304
+ listCommand,
1305
+ scanCommand,
1306
+ sync,
1307
+ topLevelSkills,
1308
+ displayPath,
1309
+ printTable
1310
+ };
1311
+ export {
1312
+ createProgram,
1313
+ main,
1314
+ testing
1315
+ };