quiver-skill-manager 0.1.0

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.
@@ -0,0 +1,234 @@
1
+ import { readdirSync, readFileSync, writeFileSync, statSync, lstatSync, existsSync, realpathSync } from 'fs';
2
+ import { join } from 'path';
3
+ import matter from 'gray-matter';
4
+ import { SKILLS_DIR, PLUGINS_DIR, ensureDirs } from './paths.js';
5
+
6
+ export function listSkills() {
7
+ ensureDirs();
8
+ const entries = readdirSync(SKILLS_DIR, { withFileTypes: true });
9
+ const skills = [];
10
+
11
+ for (const entry of entries) {
12
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
13
+ if (entry.name.startsWith('.')) continue;
14
+
15
+ const skillPath = join(SKILLS_DIR, entry.name);
16
+ const skillFile = join(skillPath, 'SKILL.md');
17
+
18
+ let frontmatter = {};
19
+ let content = '';
20
+ try {
21
+ const raw = readFileSync(skillFile, 'utf-8');
22
+ const parsed = matter(raw);
23
+ frontmatter = parsed.data;
24
+ content = parsed.content;
25
+ } catch {
26
+ // No SKILL.md or can't parse — still list the directory
27
+ }
28
+
29
+ const lstat = lstatSync(skillPath);
30
+ const isSymlink = lstat.isSymbolicLink();
31
+
32
+ let files = [];
33
+ try {
34
+ files = collectFiles(skillPath);
35
+ } catch {}
36
+
37
+ skills.push({
38
+ name: frontmatter.name || entry.name,
39
+ dirName: entry.name,
40
+ description: frontmatter.description || '',
41
+ tags: frontmatter.tags || [],
42
+ version: frontmatter.version || null,
43
+ author: frontmatter.author || null,
44
+ path: skillPath,
45
+ isSymlink,
46
+ files,
47
+ fileCount: files.length,
48
+ modified: (() => { try { return statSync(skillPath).mtime.toISOString(); } catch { return new Date().toISOString(); } })(),
49
+ hasSkillFile: content !== '' || Object.keys(frontmatter).length > 0,
50
+ source: 'local',
51
+ pluginName: null
52
+ });
53
+ }
54
+
55
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
56
+ }
57
+
58
+ export function listPluginSkills() {
59
+ if (!existsSync(PLUGINS_DIR)) return [];
60
+
61
+ const skills = [];
62
+ let marketplaces;
63
+ try {
64
+ marketplaces = readdirSync(PLUGINS_DIR, { withFileTypes: true });
65
+ } catch { return []; }
66
+
67
+ for (const marketplace of marketplaces) {
68
+ if (!marketplace.isDirectory() || marketplace.name.startsWith('.')) continue;
69
+
70
+ const pluginsPath = join(PLUGINS_DIR, marketplace.name, 'plugins');
71
+ if (!existsSync(pluginsPath)) continue;
72
+
73
+ let plugins;
74
+ try {
75
+ plugins = readdirSync(pluginsPath, { withFileTypes: true });
76
+ } catch { continue; }
77
+
78
+ for (const plugin of plugins) {
79
+ if (!plugin.isDirectory() || plugin.name.startsWith('.')) continue;
80
+
81
+ const pluginPath = join(pluginsPath, plugin.name);
82
+
83
+ // Read plugin metadata
84
+ let pluginMeta = { name: plugin.name, description: '', version: null, author: null };
85
+ try {
86
+ const metaFile = join(pluginPath, '.claude-plugin', 'plugin.json');
87
+ const raw = readFileSync(metaFile, 'utf-8');
88
+ const meta = JSON.parse(raw);
89
+ pluginMeta = {
90
+ name: meta.name || plugin.name,
91
+ description: meta.description || '',
92
+ version: meta.version || null,
93
+ author: typeof meta.author === 'object' ? meta.author.name : meta.author || null
94
+ };
95
+ } catch {}
96
+
97
+ // Scan skills/ subdirectory
98
+ const skillsPath = join(pluginPath, 'skills');
99
+ if (existsSync(skillsPath)) {
100
+ try {
101
+ const skillDirs = readdirSync(skillsPath, { withFileTypes: true });
102
+ for (const skillDir of skillDirs) {
103
+ if (!skillDir.isDirectory() || skillDir.name.startsWith('.')) continue;
104
+
105
+ const skillPath = join(skillsPath, skillDir.name);
106
+ const skillFile = join(skillPath, 'SKILL.md');
107
+
108
+ let frontmatter = {};
109
+ let content = '';
110
+ try {
111
+ const raw = readFileSync(skillFile, 'utf-8');
112
+ const parsed = matter(raw);
113
+ frontmatter = parsed.data;
114
+ content = parsed.content;
115
+ } catch { continue; }
116
+
117
+ let files = [];
118
+ try { files = collectFiles(skillPath); } catch {}
119
+
120
+ skills.push({
121
+ name: frontmatter.name || skillDir.name,
122
+ dirName: skillDir.name,
123
+ description: frontmatter.description || '',
124
+ tags: frontmatter.tags || [],
125
+ version: frontmatter.version || pluginMeta.version,
126
+ author: frontmatter.author || pluginMeta.author,
127
+ path: skillPath,
128
+ isSymlink: false,
129
+ files,
130
+ fileCount: files.length,
131
+ modified: (() => { try { return statSync(skillPath).mtime.toISOString(); } catch { return new Date().toISOString(); } })(),
132
+ hasSkillFile: true,
133
+ source: 'plugin',
134
+ pluginName: pluginMeta.name
135
+ });
136
+ }
137
+ } catch {}
138
+ }
139
+
140
+ // Scan commands/ subdirectory (legacy format)
141
+ const commandsPath = join(pluginPath, 'commands');
142
+ if (existsSync(commandsPath)) {
143
+ try {
144
+ const cmdFiles = readdirSync(commandsPath).filter(f => f.endsWith('.md'));
145
+ for (const cmdFile of cmdFiles) {
146
+ const cmdPath = join(commandsPath, cmdFile);
147
+ const cmdName = cmdFile.replace('.md', '');
148
+
149
+ let frontmatter = {};
150
+ try {
151
+ const raw = readFileSync(cmdPath, 'utf-8');
152
+ const parsed = matter(raw);
153
+ frontmatter = parsed.data;
154
+ } catch { continue; }
155
+
156
+ skills.push({
157
+ name: cmdName,
158
+ dirName: cmdName,
159
+ description: frontmatter.description || '',
160
+ tags: frontmatter.tags || [],
161
+ version: pluginMeta.version,
162
+ author: pluginMeta.author,
163
+ path: cmdPath,
164
+ isSymlink: false,
165
+ files: [cmdFile],
166
+ fileCount: 1,
167
+ modified: (() => { try { return statSync(cmdPath).mtime.toISOString(); } catch { return new Date().toISOString(); } })(),
168
+ hasSkillFile: true,
169
+ source: 'plugin',
170
+ pluginName: pluginMeta.name
171
+ });
172
+ }
173
+ } catch {}
174
+ }
175
+ }
176
+ }
177
+
178
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
179
+ }
180
+
181
+ export function listAll() {
182
+ return [...listSkills(), ...listPluginSkills()]
183
+ .sort((a, b) => a.name.localeCompare(b.name));
184
+ }
185
+
186
+ export function getSkill(name) {
187
+ const skills = listAll();
188
+ return skills.find(s => s.name === name || s.dirName === name) || null;
189
+ }
190
+
191
+ export function getSkillContent(name) {
192
+ const skill = getSkill(name);
193
+ if (!skill) return null;
194
+
195
+ // For plugin commands (single .md files), the path IS the file
196
+ const skillFile = skill.path.endsWith('.md') ? skill.path : join(skill.path, 'SKILL.md');
197
+ try {
198
+ const raw = readFileSync(skillFile, 'utf-8');
199
+ const parsed = matter(raw);
200
+ return { ...skill, content: parsed.content, frontmatter: parsed.data };
201
+ } catch {
202
+ return { ...skill, content: '', frontmatter: {} };
203
+ }
204
+ }
205
+
206
+ export function saveSkillContent(name, raw) {
207
+ const skill = getSkill(name);
208
+ if (!skill) throw new Error(`Skill not found: ${name}`);
209
+ if (skill.source !== 'local') throw new Error('Only local skills can be edited.');
210
+
211
+ const skillFile = join(skill.path, 'SKILL.md');
212
+ writeFileSync(skillFile, raw);
213
+ return { name: skill.name, path: skillFile };
214
+ }
215
+
216
+ function collectFiles(dir, base = '', depth = 0, visited = new Set()) {
217
+ if (depth > 10) return [];
218
+ let realDir;
219
+ try { realDir = realpathSync(dir); } catch { return []; }
220
+ if (visited.has(realDir)) return [];
221
+ visited.add(realDir);
222
+
223
+ const results = [];
224
+ const entries = readdirSync(dir, { withFileTypes: true });
225
+ for (const entry of entries) {
226
+ const rel = base ? `${base}/${entry.name}` : entry.name;
227
+ if (entry.isDirectory()) {
228
+ results.push(...collectFiles(join(dir, entry.name), rel, depth + 1, visited));
229
+ } else {
230
+ results.push(rel);
231
+ }
232
+ }
233
+ return results;
234
+ }
@@ -0,0 +1,17 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { mkdirSync } from 'fs';
4
+
5
+ const home = homedir();
6
+
7
+ export const SKILLS_DIR = join(home, '.claude', 'skills');
8
+ export const PLUGINS_DIR = join(home, '.claude', 'plugins', 'marketplaces');
9
+ export const CONFIG_DIR = join(home, '.quiver');
10
+ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
11
+ export const SYNC_DIR = join(CONFIG_DIR, 'sync');
12
+
13
+ export function ensureDirs() {
14
+ mkdirSync(SKILLS_DIR, { recursive: true });
15
+ mkdirSync(CONFIG_DIR, { recursive: true });
16
+ mkdirSync(SYNC_DIR, { recursive: true });
17
+ }