This repository has no description
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
5import { homedir } from 'node:os';
6import { dirname, join } from 'node:path';
7import { fileURLToPath } from 'node:url';
8
9const DEFAULT_SKILL_NAME = 'using-vit';
10const SOURCE_MISSING_ERROR = 'source skill files missing or empty';
11
12function defaultSourceDir() {
13 return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills', 'vit');
14}
15
16function errorMessage(err) {
17 if (err instanceof Error) return err.message || String(err);
18 return String(err);
19}
20
21function readSkillName(sourceDir) {
22 try {
23 const content = readFileSync(join(sourceDir, 'SKILL.md'), 'utf-8');
24 const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
25 if (!match) return DEFAULT_SKILL_NAME;
26 const nameMatch = match[1].match(/^name:\s*(.+)$/m);
27 return nameMatch ? nameMatch[1].trim() : DEFAULT_SKILL_NAME;
28 } catch {
29 return DEFAULT_SKILL_NAME;
30 }
31}
32
33function sourceExists(sourceDir) {
34 try {
35 return existsSync(sourceDir) && readdirSync(sourceDir).length > 0;
36 } catch {
37 return false;
38 }
39}
40
41export function skillInstallReason(skillResult) {
42 if (skillResult.results.some(result => result.status === 'source-missing')) {
43 return 'source skill files missing';
44 }
45
46 const failed = skillResult.results.filter(result => result.status === 'failed');
47 if (failed.length > 0) {
48 return failed
49 .map(result => `${result.label} failed: ${result.error || 'unknown error'}`)
50 .join('; ');
51 }
52
53 return 'skill install failed';
54}
55
56// sourceDir exists only as a test seam; production callers pass nothing.
57export function ensureSkill({ sourceDir = defaultSourceDir() } = {}) {
58 const name = readSkillName(sourceDir);
59 const home = homedir();
60 const targets = [
61 { label: 'claude', path: join(home, '.claude', 'skills', name) },
62 { label: 'agents', path: join(home, '.agents', 'skills', name) },
63 ];
64
65 if (!sourceExists(sourceDir)) {
66 return {
67 name,
68 source: null,
69 ok: false,
70 results: targets.map(target => ({
71 ...target,
72 status: 'source-missing',
73 error: SOURCE_MISSING_ERROR,
74 })),
75 };
76 }
77
78 const results = targets.map(target => {
79 try {
80 const existed = existsSync(join(target.path, 'SKILL.md'));
81 mkdirSync(target.path, { recursive: true });
82 cpSync(sourceDir, target.path, { recursive: true, force: true });
83 return {
84 ...target,
85 status: existed ? 'already-present' : 'created',
86 error: null,
87 };
88 } catch (err) {
89 return {
90 ...target,
91 status: 'failed',
92 error: errorMessage(err),
93 };
94 }
95 });
96
97 return {
98 name,
99 source: sourceDir,
100 ok: results.every(result => result.status === 'created' || result.status === 'already-present'),
101 results,
102 };
103}