joyskills-cli 0.1.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/LICENSE +21 -0
- package/README.md +256 -0
- package/package.json +51 -0
- package/spec/cli-spec.md +167 -0
- package/spec/lockfile-spec.md +108 -0
- package/spec/registry-spec.md +117 -0
- package/src/commands/audit.js +78 -0
- package/src/commands/install.js +119 -0
- package/src/commands/list.js +120 -0
- package/src/commands/remove.js +42 -0
- package/src/commands/status.js +116 -0
- package/src/commands/team.js +92 -0
- package/src/index.js +27 -0
- package/src/local.js +93 -0
- package/src/lockfile.js +76 -0
- package/src/registry.js +273 -0
- package/src/types.js +34 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { listCommand } from './commands/list.js';
|
|
5
|
+
import { installCommand } from './commands/install.js';
|
|
6
|
+
import { removeCommand } from './commands/remove.js';
|
|
7
|
+
import { teamCommand } from './commands/team.js';
|
|
8
|
+
import { auditCommand } from './commands/audit.js';
|
|
9
|
+
import { statusCommand } from './commands/status.js';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('joySkills')
|
|
15
|
+
.description('Team-level skill governance compatible with open skill / Claude Skills')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
|
|
18
|
+
// Add commands
|
|
19
|
+
listCommand(program);
|
|
20
|
+
installCommand(program);
|
|
21
|
+
removeCommand(program);
|
|
22
|
+
teamCommand(program);
|
|
23
|
+
auditCommand(program);
|
|
24
|
+
statusCommand(program);
|
|
25
|
+
|
|
26
|
+
// Parse arguments
|
|
27
|
+
program.parse();
|
package/src/local.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { LocalSkill } from './types.js';
|
|
4
|
+
|
|
5
|
+
export class LocalManager {
|
|
6
|
+
constructor(projectRoot) {
|
|
7
|
+
this.projectRoot = projectRoot;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get local skills directory path
|
|
12
|
+
*/
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return path.join(this.projectRoot, 'skills');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List all local skills
|
|
19
|
+
*/
|
|
20
|
+
listSkills() {
|
|
21
|
+
const skillsDir = this.getSkillsDir();
|
|
22
|
+
if (!fs.existsSync(skillsDir)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
27
|
+
.filter(dirent => dirent.isDirectory())
|
|
28
|
+
.map(dirent => dirent.name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a skill exists locally
|
|
33
|
+
*/
|
|
34
|
+
hasSkill(skillName) {
|
|
35
|
+
const skillPath = path.join(this.getSkillsDir(), skillName);
|
|
36
|
+
return fs.existsSync(skillPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read SKILL.md content of a local skill
|
|
41
|
+
*/
|
|
42
|
+
readSkill(skillName) {
|
|
43
|
+
const skillPath = path.join(this.getSkillsDir(), skillName);
|
|
44
|
+
if (!this.hasSkill(skillName)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
49
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
54
|
+
return new LocalSkill(skillName, skillPath, content);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Install a skill (create directory and SKILL.md)
|
|
59
|
+
*/
|
|
60
|
+
installSkill(skillName, skillMdContent) {
|
|
61
|
+
const skillsDir = this.getSkillsDir();
|
|
62
|
+
|
|
63
|
+
// Create skills directory if not exists
|
|
64
|
+
if (!fs.existsSync(skillsDir)) {
|
|
65
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const skillPath = path.join(skillsDir, skillName);
|
|
69
|
+
|
|
70
|
+
// Create skill directory
|
|
71
|
+
if (!fs.existsSync(skillPath)) {
|
|
72
|
+
fs.mkdirSync(skillPath, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Write SKILL.md
|
|
76
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
77
|
+
fs.writeFileSync(skillMdPath, skillMdContent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove a local skill
|
|
82
|
+
*/
|
|
83
|
+
removeSkill(skillName) {
|
|
84
|
+
const skillPath = path.join(this.getSkillsDir(), skillName);
|
|
85
|
+
if (!fs.existsSync(skillPath)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Remove skill directory recursively
|
|
90
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/lockfile.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
export class LockfileManager {
|
|
6
|
+
constructor(projectRoot) {
|
|
7
|
+
this.lockfilePath = path.join(projectRoot, 'joySkills.lock');
|
|
8
|
+
this.lockData = {
|
|
9
|
+
lockVersion: 1,
|
|
10
|
+
skills: {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load existing lockfile or initialize new one
|
|
16
|
+
*/
|
|
17
|
+
async load() {
|
|
18
|
+
if (!fs.existsSync(this.lockfilePath)) {
|
|
19
|
+
// Initialize empty if not exists
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = await fs.promises.readFile(this.lockfilePath, 'utf-8');
|
|
24
|
+
try {
|
|
25
|
+
this.lockData = yaml.parse(content);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`Failed to parse joyskill.lock: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save lockfile to disk
|
|
33
|
+
*/
|
|
34
|
+
async save() {
|
|
35
|
+
const content = yaml.stringify(this.lockData);
|
|
36
|
+
await fs.promises.writeFile(this.lockfilePath, content, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update or add a skill entry
|
|
41
|
+
*/
|
|
42
|
+
updateSkill(skillId, entry) {
|
|
43
|
+
this.lockData.skills[skillId] = entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove a skill entry
|
|
48
|
+
*/
|
|
49
|
+
removeSkill(skillId) {
|
|
50
|
+
delete this.lockData.skills[skillId];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get skill entry
|
|
55
|
+
*/
|
|
56
|
+
getSkill(skillId) {
|
|
57
|
+
return this.lockData.skills[skillId];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bind a registry
|
|
62
|
+
*/
|
|
63
|
+
bindRegistry(registryId, alias) {
|
|
64
|
+
if (!this.lockData.registryBindings) {
|
|
65
|
+
this.lockData.registryBindings = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if already bound
|
|
69
|
+
const existing = this.lockData.registryBindings.find(b => b.registryId === registryId);
|
|
70
|
+
if (existing) {
|
|
71
|
+
existing.alias = alias;
|
|
72
|
+
} else {
|
|
73
|
+
this.lockData.registryBindings.push({ registryId, alias });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
export class RegistryManager {
|
|
6
|
+
constructor(registryPath) {
|
|
7
|
+
this.index = null;
|
|
8
|
+
this.registryPath = registryPath;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load and parse registry.yaml
|
|
13
|
+
*/
|
|
14
|
+
async load() {
|
|
15
|
+
const indexPath = path.join(this.registryPath, 'registry.yaml');
|
|
16
|
+
if (!fs.existsSync(indexPath)) {
|
|
17
|
+
throw new Error(`Registry index not found at ${indexPath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const content = await fs.promises.readFile(indexPath, 'utf-8');
|
|
21
|
+
try {
|
|
22
|
+
this.index = yaml.parse(content);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new Error(`Failed to parse registry.yaml: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all skills in the registry
|
|
30
|
+
*/
|
|
31
|
+
getAllSkills() {
|
|
32
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
33
|
+
return this.index.skills || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get metadata for a specific skill
|
|
38
|
+
*/
|
|
39
|
+
getSkill(skillId) {
|
|
40
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
41
|
+
return this.index.skills.find(s => s.id === skillId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the recommended version for a skill
|
|
46
|
+
*/
|
|
47
|
+
getRecommendedVersion(skillId) {
|
|
48
|
+
const skill = this.getSkill(skillId);
|
|
49
|
+
if (!skill) return undefined;
|
|
50
|
+
|
|
51
|
+
// First look for explicitly recommended version
|
|
52
|
+
const recommended = skill.versions.find(v => v.recommended && v.state === 'approved');
|
|
53
|
+
if (recommended) return recommended;
|
|
54
|
+
|
|
55
|
+
// Fallback: find latest approved version
|
|
56
|
+
// Assuming versions are semver, sorting might be needed.
|
|
57
|
+
// For simplicity now, just taking the first approved one,
|
|
58
|
+
// but in production this should use semver-sort.
|
|
59
|
+
return skill.versions.find(v => v.state === 'approved');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the latest version for a skill
|
|
64
|
+
*/
|
|
65
|
+
getLatestVersion(skillId) {
|
|
66
|
+
const skill = this.getSkill(skillId);
|
|
67
|
+
if (!skill) return undefined;
|
|
68
|
+
|
|
69
|
+
// Find the latest approved version
|
|
70
|
+
// For now, just return the first approved version
|
|
71
|
+
// In production, this should properly sort by version
|
|
72
|
+
return skill.versions.find(v => v.state === 'approved');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a version exists and is approved
|
|
77
|
+
*/
|
|
78
|
+
getVersion(skillId, version) {
|
|
79
|
+
const skill = this.getSkill(skillId);
|
|
80
|
+
if (!skill) return undefined;
|
|
81
|
+
return skill.versions.find(v => v.version === version);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Search skills by name or description
|
|
86
|
+
*/
|
|
87
|
+
searchSkills(query) {
|
|
88
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
89
|
+
|
|
90
|
+
const lowercaseQuery = query.toLowerCase();
|
|
91
|
+
return this.index.skills.filter(skill =>
|
|
92
|
+
skill.id.toLowerCase().includes(lowercaseQuery) ||
|
|
93
|
+
(skill.name && skill.name.toLowerCase().includes(lowercaseQuery)) ||
|
|
94
|
+
(skill.description && skill.description.toLowerCase().includes(lowercaseQuery))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get skills by category
|
|
100
|
+
*/
|
|
101
|
+
getSkillsByCategory(category) {
|
|
102
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
103
|
+
return this.index.skills.filter(skill => skill.category === category);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get all categories
|
|
108
|
+
*/
|
|
109
|
+
getCategories() {
|
|
110
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
111
|
+
const categories = new Set();
|
|
112
|
+
this.index.skills.forEach(skill => {
|
|
113
|
+
if (skill.category) {
|
|
114
|
+
categories.add(skill.category);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return Array.from(categories);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a skill exists in the registry
|
|
122
|
+
*/
|
|
123
|
+
hasSkill(skillId) {
|
|
124
|
+
return this.getSkill(skillId) !== undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate a skill version
|
|
129
|
+
*/
|
|
130
|
+
validateVersion(skillId, version) {
|
|
131
|
+
const skill = this.getSkill(skillId);
|
|
132
|
+
if (!skill) {
|
|
133
|
+
return { valid: false, error: 'Skill not found in registry' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const versionData = this.getVersion(skillId, version);
|
|
137
|
+
if (!versionData) {
|
|
138
|
+
return { valid: false, error: 'Version not found' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (versionData.state === 'draft') {
|
|
142
|
+
return { valid: false, error: 'Version is in draft state' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (versionData.state === 'pending_review') {
|
|
146
|
+
return { valid: false, error: 'Version is pending review' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (versionData.state === 'deprecated') {
|
|
150
|
+
return { valid: true, warning: 'Version is deprecated' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { valid: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all approved skills
|
|
158
|
+
*/
|
|
159
|
+
getApprovedSkills() {
|
|
160
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
161
|
+
return this.index.skills.filter(skill =>
|
|
162
|
+
skill.versions.some(version => version.state === 'approved')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get skills by visibility
|
|
168
|
+
*/
|
|
169
|
+
getSkillsByVisibility(visibility) {
|
|
170
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
171
|
+
return this.index.skills.filter(skill => skill.visibility === visibility);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get registry info
|
|
176
|
+
*/
|
|
177
|
+
getRegistryInfo() {
|
|
178
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
179
|
+
return {
|
|
180
|
+
registryVersion: this.index.registryVersion,
|
|
181
|
+
registryId: this.index.registryId
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get skills by author
|
|
187
|
+
*/
|
|
188
|
+
getSkillsByAuthor(author) {
|
|
189
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
190
|
+
return this.index.skills.filter(skill => skill.author === author);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get skills by tags
|
|
195
|
+
*/
|
|
196
|
+
getSkillsByTags(tags) {
|
|
197
|
+
if (!this.index) throw new Error('Registry not loaded');
|
|
198
|
+
const tagArray = Array.isArray(tags) ? tags : [tags];
|
|
199
|
+
return this.index.skills.filter(skill =>
|
|
200
|
+
skill.tags && tagArray.some(tag => skill.tags.includes(tag))
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get skill download URL
|
|
206
|
+
*/
|
|
207
|
+
getSkillDownloadUrl(skillId, version) {
|
|
208
|
+
const skill = this.getSkill(skillId);
|
|
209
|
+
if (!skill) return null;
|
|
210
|
+
|
|
211
|
+
const versionData = this.getVersion(skillId, version);
|
|
212
|
+
if (!versionData) return null;
|
|
213
|
+
|
|
214
|
+
return versionData.downloadUrl || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get skill checksum
|
|
219
|
+
*/
|
|
220
|
+
getSkillChecksum(skillId, version) {
|
|
221
|
+
const skill = this.getSkill(skillId);
|
|
222
|
+
if (!skill) return null;
|
|
223
|
+
|
|
224
|
+
const versionData = this.getVersion(skillId, version);
|
|
225
|
+
if (!versionData) return null;
|
|
226
|
+
|
|
227
|
+
return versionData.checksum || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all versions for a skill
|
|
232
|
+
*/
|
|
233
|
+
getAllVersions(skillId) {
|
|
234
|
+
const skill = this.getSkill(skillId);
|
|
235
|
+
if (!skill) return [];
|
|
236
|
+
return skill.versions || [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get approved versions for a skill
|
|
241
|
+
*/
|
|
242
|
+
getApprovedVersions(skillId) {
|
|
243
|
+
const skill = this.getSkill(skillId);
|
|
244
|
+
if (!skill) return [];
|
|
245
|
+
return (skill.versions || []).filter(v => v.state === 'approved');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get skill dependencies
|
|
250
|
+
*/
|
|
251
|
+
getSkillDependencies(skillId, version) {
|
|
252
|
+
const skill = this.getSkill(skillId);
|
|
253
|
+
if (!skill) return [];
|
|
254
|
+
|
|
255
|
+
const versionData = this.getVersion(skillId, version);
|
|
256
|
+
if (!versionData) return [];
|
|
257
|
+
|
|
258
|
+
return versionData.dependencies || [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if skill is deprecated
|
|
263
|
+
*/
|
|
264
|
+
isSkillDeprecated(skillId, version) {
|
|
265
|
+
const skill = this.getSkill(skillId);
|
|
266
|
+
if (!skill) return false;
|
|
267
|
+
|
|
268
|
+
const versionData = this.getVersion(skillId, version);
|
|
269
|
+
if (!versionData) return false;
|
|
270
|
+
|
|
271
|
+
return versionData.state === 'deprecated';
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JoySkill Core Type Definitions
|
|
3
|
+
* Based on spec/registry-spec.md and spec/lockfile-spec.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// === Registry Types ===
|
|
7
|
+
|
|
8
|
+
export const SkillVisibility = {
|
|
9
|
+
PUBLIC: 'public',
|
|
10
|
+
RESTRICTED: 'restricted',
|
|
11
|
+
INTERNAL: 'internal'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const SkillVersionState = {
|
|
15
|
+
DRAFT: 'draft',
|
|
16
|
+
PENDING_REVIEW: 'pending_review',
|
|
17
|
+
APPROVED: 'approved',
|
|
18
|
+
DEPRECATED: 'deprecated',
|
|
19
|
+
ARCHIVED: 'archived'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// === Lockfile Types ===
|
|
23
|
+
|
|
24
|
+
export const LockfileVersion = 1;
|
|
25
|
+
|
|
26
|
+
// === Local Skill Types ===
|
|
27
|
+
|
|
28
|
+
export class LocalSkill {
|
|
29
|
+
constructor(name, path, skillMdContent) {
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.path = path;
|
|
32
|
+
this.skillMdContent = skillMdContent;
|
|
33
|
+
}
|
|
34
|
+
}
|