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/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
+ }
@@ -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
+ }
@@ -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
+ }