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.
- package/README.md +87 -0
- package/bin/quiver.js +2 -0
- package/package.json +45 -0
- package/src/cli.js +307 -0
- package/src/core/add.js +33 -0
- package/src/core/config.js +48 -0
- package/src/core/export.js +48 -0
- package/src/core/import.js +57 -0
- package/src/core/inventory.js +234 -0
- package/src/core/paths.js +17 -0
- package/src/core/registry.js +488 -0
- package/src/core/remove.js +24 -0
- package/src/core/sync/git.js +212 -0
- package/src/core/sync/index.js +1 -0
- package/src/core/sync/snapshot.js +148 -0
- package/src/routes.js +270 -0
- package/src/server.js +58 -0
- package/ui/app.js +922 -0
- package/ui/index.html +14 -0
- package/ui/styles.css +870 -0
- package/ui/vendor/htm.mjs +4 -0
- package/ui/vendor/preact-hooks.mjs +3 -0
- package/ui/vendor/preact.mjs +3 -0
|
@@ -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
|
+
}
|