@quadient/skillshare 0.1.1 → 1.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 +100 -0
- package/dist/index.js +133 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ bun run index.ts
|
|
|
9
9
|
bun run index.ts login
|
|
10
10
|
bun run index.ts login local
|
|
11
11
|
bun run index.ts list
|
|
12
|
+
bun run index.ts install
|
|
13
|
+
bun run index.ts install "Prompt Lint"
|
|
12
14
|
bun run index.ts upload ./examples/prompt-lint --version 0.1.0
|
|
13
15
|
bun run index.ts install 12 0.1.0 --target ./installed
|
|
14
16
|
bun run index.ts install --skill-version-id 42 --target ./installed
|
|
@@ -17,11 +19,109 @@ bun run index.ts install --skill-version-id 42 --target ./installed
|
|
|
17
19
|
Interactive behavior:
|
|
18
20
|
|
|
19
21
|
- If the current directory already contains SKILL.md, the TUI offers to upload it before showing the main action picker.
|
|
22
|
+
- `install` opens the guided installer. When you pass `<skillId|skillName>`, the installer opens with that skill already selected; adding `[version]` skips the version picker too.
|
|
20
23
|
- After install or upload, the CLI writes skillshare.metadata.json into the skill directory with the skill id, name, description, and installed or published version.
|
|
21
24
|
- When skillshare.metadata.json already exists, uploads publish a new version of that skill instead of creating a duplicate entry.
|
|
22
25
|
- New skill creation derives name and description from SKILL.md frontmatter when available, so the user usually only needs to enter the initial version.
|
|
23
26
|
- Upload and publish commands show progress while scanning and reading local files, then keep a timed upload status visible until the registry call finishes.
|
|
24
27
|
|
|
28
|
+
## Current TUI Flow
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart TD
|
|
32
|
+
A[Start: skillshare] --> B{Command provided?}
|
|
33
|
+
|
|
34
|
+
B -- No --> C[runInteractive]
|
|
35
|
+
B -- Yes --> D{Which command?}
|
|
36
|
+
|
|
37
|
+
D -->|login local| E[login LOCAL_URL]
|
|
38
|
+
D -->|login| F[login PROD_URL]
|
|
39
|
+
D -->|logout| G[logoutSession]
|
|
40
|
+
D -->|whoami list show upload install| H[createClient using active baseUrl]
|
|
41
|
+
|
|
42
|
+
C --> I[resolveActiveBaseUrl]
|
|
43
|
+
I --> J[ensureInteractiveLogin]
|
|
44
|
+
J --> K{Stored session exists?}
|
|
45
|
+
K -- No --> F
|
|
46
|
+
K -- Yes --> L{Refresh token still valid?}
|
|
47
|
+
L -- No --> F
|
|
48
|
+
L -- Yes --> M[createClient and GET /api/auth/me]
|
|
49
|
+
M --> N{Session still accepted?}
|
|
50
|
+
N -- No --> F
|
|
51
|
+
N -- Yes --> O[Interactive menu ready]
|
|
52
|
+
|
|
53
|
+
E --> P[Start PKCE and callback server]
|
|
54
|
+
F --> P
|
|
55
|
+
P --> Q[Open browser to /api/cli/login]
|
|
56
|
+
Q --> R[Wait for local callback with code and state]
|
|
57
|
+
R --> S[POST /api/cli/token]
|
|
58
|
+
S --> T[Store session.json]
|
|
59
|
+
T --> O
|
|
60
|
+
|
|
61
|
+
O --> U{Current directory contains SKILL.md?}
|
|
62
|
+
U -- Yes --> V{Upload current directory now?}
|
|
63
|
+
V -- Yes --> W[runInteractiveUpload]
|
|
64
|
+
V -- No --> X[Choose action]
|
|
65
|
+
U -- No --> X
|
|
66
|
+
|
|
67
|
+
X --> Y{Install or Upload?}
|
|
68
|
+
Y -->|Install| Z[runInteractiveInstall]
|
|
69
|
+
Y -->|Upload| W
|
|
70
|
+
|
|
71
|
+
Z --> Z1[GET /api/skills]
|
|
72
|
+
Z1 --> Z2[Choose skill or resolve skillRef]
|
|
73
|
+
Z2 --> Z3[GET skill detail]
|
|
74
|
+
Z3 --> Z4[Choose version or use provided version]
|
|
75
|
+
Z4 --> Z5[Choose project/global scope]
|
|
76
|
+
Z5 --> Z6[Choose agents]
|
|
77
|
+
Z6 --> Z7[GET version detail]
|
|
78
|
+
Z7 --> Z8[POST download audit]
|
|
79
|
+
Z8 --> Z9[Write files to install targets]
|
|
80
|
+
Z9 --> Z10[Write skillshare.metadata.json]
|
|
81
|
+
Z10 --> END[Done]
|
|
82
|
+
|
|
83
|
+
W --> W1[Load local skill context]
|
|
84
|
+
W1 --> W2{Local metadata has skillId?}
|
|
85
|
+
|
|
86
|
+
W2 -- No --> W3[Prompt initial version]
|
|
87
|
+
W3 --> W4[createSkillFromDirectory]
|
|
88
|
+
W4 --> W5[POST create skill]
|
|
89
|
+
W5 --> W6[GET created skill detail]
|
|
90
|
+
W6 --> W7[Write skillshare.metadata.json]
|
|
91
|
+
W7 --> END
|
|
92
|
+
|
|
93
|
+
W2 -- Yes --> W8[GET existing skill detail]
|
|
94
|
+
W8 --> W9{Local version differs from latest?}
|
|
95
|
+
W9 -- Yes --> W10{Download latest first?}
|
|
96
|
+
W10 -- Yes --> W11[GET latest version detail]
|
|
97
|
+
W11 --> W12[POST download audit]
|
|
98
|
+
W12 --> W13[Overwrite local files and metadata]
|
|
99
|
+
W13 --> END
|
|
100
|
+
|
|
101
|
+
W10 -- No --> W14[Prompt new version]
|
|
102
|
+
W9 -- No --> W14
|
|
103
|
+
W14 --> W15[publishSkillFromDirectory]
|
|
104
|
+
W15 --> W16[POST publish version]
|
|
105
|
+
W16 --> W17[GET updated skill detail]
|
|
106
|
+
W17 --> W18[Update skillshare.metadata.json]
|
|
107
|
+
W18 --> END
|
|
108
|
+
|
|
109
|
+
H --> H1{Non-interactive branch}
|
|
110
|
+
H1 -->|whoami| H2[GET /api/auth/me]
|
|
111
|
+
H1 -->|list| H3[GET /api/skills]
|
|
112
|
+
H1 -->|show| H4[GET skill or version detail]
|
|
113
|
+
H1 -->|upload| H5[Load local skill context]
|
|
114
|
+
H1 -->|install without --target| Z
|
|
115
|
+
H1 -->|install with --target| H6[Resolve skill and version]
|
|
116
|
+
H6 --> H7[GET version detail]
|
|
117
|
+
H7 --> H8[POST download audit]
|
|
118
|
+
H8 --> H9[Write files and metadata to target]
|
|
119
|
+
H9 --> END
|
|
120
|
+
H5 --> H10{Known skillId?}
|
|
121
|
+
H10 -- No --> W4
|
|
122
|
+
H10 -- Yes --> W15
|
|
123
|
+
```
|
|
124
|
+
|
|
25
125
|
Login opens a browser against the selected Skillshare host, exchanges the backend login code for CLI tokens, and stores the access token plus refresh token in the user config directory.
|
|
26
126
|
|
|
27
127
|
Without an existing session, the CLI now defaults to the production registry. Use `login local` only when you want the local backend.
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,9 @@ Commands:
|
|
|
71
71
|
list
|
|
72
72
|
show <skillId> [version]
|
|
73
73
|
upload <directory> --version <version> [--skill-id <id>] [--name <name> --description <desc>]
|
|
74
|
-
install
|
|
74
|
+
install
|
|
75
|
+
install <skillId|skillName> [version]
|
|
76
|
+
install <skillId|skillName> [version] --target <directory>
|
|
75
77
|
install --skill-version-id <id> --target <directory>
|
|
76
78
|
|
|
77
79
|
Options:
|
|
@@ -137,10 +139,10 @@ async function main() {
|
|
|
137
139
|
throw new Error('upload requires <directory> --version <version>');
|
|
138
140
|
}
|
|
139
141
|
const localSkill = await loadLocalSkillContext(directory);
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!
|
|
142
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill, {
|
|
143
|
+
explicitSkillId: options.flags['skill-id'],
|
|
144
|
+
});
|
|
145
|
+
if (!existingSkill) {
|
|
144
146
|
const created = await createSkillFromDirectory(client, localSkill, {
|
|
145
147
|
name: options.flags.name,
|
|
146
148
|
description: options.flags.description,
|
|
@@ -150,9 +152,8 @@ async function main() {
|
|
|
150
152
|
}
|
|
151
153
|
else {
|
|
152
154
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
153
|
-
skillId,
|
|
155
|
+
skillId: existingSkill.id,
|
|
154
156
|
version,
|
|
155
|
-
failIfMissing: false,
|
|
156
157
|
});
|
|
157
158
|
console.log(JSON.stringify(published.response, null, 2));
|
|
158
159
|
}
|
|
@@ -160,7 +161,7 @@ async function main() {
|
|
|
160
161
|
}
|
|
161
162
|
case 'install': {
|
|
162
163
|
const skillVersionId = options.flags['skill-version-id'];
|
|
163
|
-
const
|
|
164
|
+
const skillRef = options.positionals[0];
|
|
164
165
|
const version = options.positionals[1];
|
|
165
166
|
const target = options.flags.target;
|
|
166
167
|
if (skillVersionId) {
|
|
@@ -185,16 +186,24 @@ async function main() {
|
|
|
185
186
|
console.log(`Installed skillVersion ${parsedSkillVersionId} (${detail.skillId}@${detail.version}) to ${target}`);
|
|
186
187
|
return;
|
|
187
188
|
}
|
|
188
|
-
if (!
|
|
189
|
-
|
|
189
|
+
if (!target) {
|
|
190
|
+
await runInteractiveInstall(client, {
|
|
191
|
+
skillRef,
|
|
192
|
+
version,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!skillRef) {
|
|
197
|
+
throw new Error('install requires <skillId|skillName> [version] when --target <directory> is provided');
|
|
190
198
|
}
|
|
191
|
-
const
|
|
199
|
+
const skills = await client.get('/api/skills');
|
|
200
|
+
const skill = resolveSkillReference(skills, skillRef);
|
|
192
201
|
const effectiveVersion = version ?? skill.latestVersion;
|
|
193
202
|
if (!effectiveVersion) {
|
|
194
|
-
throw new Error(`Skill '${
|
|
203
|
+
throw new Error(`Skill '${skill.name}' does not have a published version.`);
|
|
195
204
|
}
|
|
196
|
-
const detail = await client.get(`/api/skills/${
|
|
197
|
-
await client.post(`/api/skills/${
|
|
205
|
+
const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
|
|
206
|
+
await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
|
|
198
207
|
await writeSkillVersionToDirectory(client, {
|
|
199
208
|
skillId: skill.id,
|
|
200
209
|
skillName: skill.name,
|
|
@@ -203,7 +212,7 @@ async function main() {
|
|
|
203
212
|
files: detail.files,
|
|
204
213
|
targetRoot: target,
|
|
205
214
|
});
|
|
206
|
-
console.log(`Installed ${
|
|
215
|
+
console.log(`Installed ${skill.name}@${effectiveVersion} to ${target}`);
|
|
207
216
|
return;
|
|
208
217
|
}
|
|
209
218
|
}
|
|
@@ -266,14 +275,18 @@ async function ensureInteractiveLogin(baseUrl) {
|
|
|
266
275
|
await login(baseUrl);
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
|
-
async function runInteractiveInstall(client) {
|
|
278
|
+
async function runInteractiveInstall(client, options) {
|
|
270
279
|
const skills = await client.get('/api/skills');
|
|
271
280
|
if (skills.length === 0) {
|
|
272
281
|
throw new Error('No skills available in the registry.');
|
|
273
282
|
}
|
|
274
|
-
const skill =
|
|
283
|
+
const skill = options?.skillRef
|
|
284
|
+
? resolveSkillReference(skills, options.skillRef)
|
|
285
|
+
: await promptForSkill(skills);
|
|
275
286
|
const detail = await client.get(`/api/skills/${skill.id}`);
|
|
276
|
-
const selectedVersion =
|
|
287
|
+
const selectedVersion = options?.version
|
|
288
|
+
? resolveSkillVersion(detail, options.version)
|
|
289
|
+
: await promptForVersion(detail);
|
|
277
290
|
const scope = await promptForInstallScope();
|
|
278
291
|
const selectedAgents = await promptForAgents(scope);
|
|
279
292
|
if (selectedAgents.length === 0) {
|
|
@@ -299,14 +312,15 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
299
312
|
const directory = initialDirectory
|
|
300
313
|
?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
|
|
301
314
|
const localSkill = await loadLocalSkillContext(directory);
|
|
302
|
-
|
|
315
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill);
|
|
316
|
+
if (!existingSkill) {
|
|
303
317
|
const version = await promptText('Initial version', '0.1.0');
|
|
304
318
|
const created = await createSkillFromDirectory(client, localSkill, { version });
|
|
305
319
|
console.log(`Created skill ${created.detail.id}: ${created.detail.name}@${version}`);
|
|
306
320
|
return;
|
|
307
321
|
}
|
|
308
|
-
const skill = await client.get(`/api/skills/${
|
|
309
|
-
const localVersion = normalizeText(localSkill.metadata
|
|
322
|
+
const skill = await client.get(`/api/skills/${existingSkill.id}`);
|
|
323
|
+
const localVersion = normalizeText(localSkill.metadata?.skillVersion);
|
|
310
324
|
const latestVersion = normalizeText(skill.latestVersion);
|
|
311
325
|
if (latestVersion && localVersion && latestVersion !== localVersion) {
|
|
312
326
|
const shouldDownloadLatest = await promptConfirm(`Local version is ${localVersion}, but the server latest is ${latestVersion}. Download it first? Local file changes will be overwritten.`, false);
|
|
@@ -329,7 +343,6 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
329
343
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
330
344
|
skillId: skill.id,
|
|
331
345
|
version,
|
|
332
|
-
failIfMissing: true,
|
|
333
346
|
});
|
|
334
347
|
console.log(`Published ${published.detail.name}@${published.response.version}`);
|
|
335
348
|
}
|
|
@@ -544,9 +557,6 @@ async function createSkillFromDirectory(client, localSkill, input) {
|
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
async function publishSkillFromDirectory(client, localSkill, input) {
|
|
547
|
-
if (input.failIfMissing && !localSkill.metadata?.skillId) {
|
|
548
|
-
throw new Error(`Missing ${SKILL_METADATA_FILE_NAME}. Create the skill first or restore the metadata file.`);
|
|
549
|
-
}
|
|
550
560
|
const uploadStatus = spinner({ indicator: 'timer' });
|
|
551
561
|
uploadStatus.start(`Uploading ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
552
562
|
try {
|
|
@@ -723,6 +733,102 @@ function normalizeText(value) {
|
|
|
723
733
|
const trimmed = value?.trim();
|
|
724
734
|
return trimmed ? trimmed : null;
|
|
725
735
|
}
|
|
736
|
+
async function resolveExistingUploadSkill(client, localSkill, options) {
|
|
737
|
+
const explicitSkillId = normalizeText(options?.explicitSkillId);
|
|
738
|
+
if (explicitSkillId) {
|
|
739
|
+
return {
|
|
740
|
+
id: parsePositiveInteger(explicitSkillId, '--skill-id'),
|
|
741
|
+
name: '',
|
|
742
|
+
description: '',
|
|
743
|
+
ownerUserId: 0,
|
|
744
|
+
created: '',
|
|
745
|
+
updated: '',
|
|
746
|
+
deprecated: false,
|
|
747
|
+
latestVersion: null,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (localSkill.metadata?.skillId) {
|
|
751
|
+
return {
|
|
752
|
+
id: localSkill.metadata.skillId,
|
|
753
|
+
name: localSkill.metadata.skillName,
|
|
754
|
+
description: localSkill.metadata.skillDescription,
|
|
755
|
+
ownerUserId: 0,
|
|
756
|
+
created: '',
|
|
757
|
+
updated: '',
|
|
758
|
+
deprecated: false,
|
|
759
|
+
latestVersion: localSkill.metadata.skillVersion,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const lookupName = normalizeText(localSkill.metadata?.skillName) ?? localSkill.defaults.name;
|
|
763
|
+
return resolveUploadSkillByName(client, lookupName);
|
|
764
|
+
}
|
|
765
|
+
async function resolveUploadSkillByName(client, skillName) {
|
|
766
|
+
const normalizedSkillName = normalizeText(skillName)?.toLowerCase();
|
|
767
|
+
if (!normalizedSkillName) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const skills = await client.get('/api/skills');
|
|
771
|
+
const exactNameMatches = skills.filter((skill) => normalizeText(skill.name)?.toLowerCase() === normalizedSkillName);
|
|
772
|
+
if (exactNameMatches.length === 0) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const currentUser = await client.get('/api/auth/me');
|
|
776
|
+
const authorizedMatches = currentUser.isAdmin
|
|
777
|
+
? exactNameMatches
|
|
778
|
+
: exactNameMatches.filter((skill) => skill.ownerUserId === currentUser.storedUserId);
|
|
779
|
+
const authorizedMatch = authorizedMatches[0];
|
|
780
|
+
if (authorizedMatches.length === 1 && authorizedMatch) {
|
|
781
|
+
return authorizedMatch;
|
|
782
|
+
}
|
|
783
|
+
if (authorizedMatches.length > 1) {
|
|
784
|
+
throw new Error(`Skill name '${skillName}' matches multiple skills you can update: ${formatSkillMatches(authorizedMatches)}. Use --skill-id instead.`);
|
|
785
|
+
}
|
|
786
|
+
if (exactNameMatches.length === 1) {
|
|
787
|
+
throw new Error(`Skill '${skillName}' already exists on the server, but the current user can publish new versions only as the owner or an admin.`);
|
|
788
|
+
}
|
|
789
|
+
throw new Error(`Skill name '${skillName}' matches multiple existing skills: ${formatSkillMatches(exactNameMatches)}. Use --skill-id instead.`);
|
|
790
|
+
}
|
|
791
|
+
function resolveSkillReference(skills, reference) {
|
|
792
|
+
const normalizedReference = normalizeText(reference)?.toLowerCase();
|
|
793
|
+
if (!normalizedReference) {
|
|
794
|
+
throw new Error('Skill reference is required.');
|
|
795
|
+
}
|
|
796
|
+
const idMatch = skills.find((skill) => String(skill.id) === normalizedReference);
|
|
797
|
+
if (idMatch) {
|
|
798
|
+
return idMatch;
|
|
799
|
+
}
|
|
800
|
+
const exactNameMatches = skills.filter((skill) => normalizeText(skill.name)?.toLowerCase() === normalizedReference);
|
|
801
|
+
const exactNameMatch = exactNameMatches[0];
|
|
802
|
+
if (exactNameMatches.length === 1 && exactNameMatch) {
|
|
803
|
+
return exactNameMatch;
|
|
804
|
+
}
|
|
805
|
+
if (exactNameMatches.length > 1) {
|
|
806
|
+
throw new Error(`Skill reference '${reference}' matches multiple skills: ${formatSkillMatches(exactNameMatches)}. Use the numeric skill id instead.`);
|
|
807
|
+
}
|
|
808
|
+
const slugMatches = skills.filter((skill) => createInstalledSkillDirectoryName(skill) === normalizedReference);
|
|
809
|
+
const slugMatch = slugMatches[0];
|
|
810
|
+
if (slugMatches.length === 1 && slugMatch) {
|
|
811
|
+
return slugMatch;
|
|
812
|
+
}
|
|
813
|
+
if (slugMatches.length > 1) {
|
|
814
|
+
throw new Error(`Skill reference '${reference}' matches multiple skills: ${formatSkillMatches(slugMatches)}. Use the numeric skill id instead.`);
|
|
815
|
+
}
|
|
816
|
+
throw new Error(`Skill '${reference}' was not found. Run "skillshare list" to inspect available skills.`);
|
|
817
|
+
}
|
|
818
|
+
function resolveSkillVersion(detail, version) {
|
|
819
|
+
const normalizedVersion = normalizeText(version);
|
|
820
|
+
if (!normalizedVersion) {
|
|
821
|
+
throw new Error('Version is required.');
|
|
822
|
+
}
|
|
823
|
+
const requestedVersion = detail.versions.find((item) => item.version === normalizedVersion);
|
|
824
|
+
if (!requestedVersion) {
|
|
825
|
+
throw new Error(`Skill '${detail.name}' does not have version '${normalizedVersion}'.`);
|
|
826
|
+
}
|
|
827
|
+
return requestedVersion.version;
|
|
828
|
+
}
|
|
829
|
+
function formatSkillMatches(skills) {
|
|
830
|
+
return skills.map((skill) => `${skill.id}:${skill.name}`).join(', ');
|
|
831
|
+
}
|
|
726
832
|
function filterSkills(skills, query) {
|
|
727
833
|
const normalized = query.trim().toLowerCase();
|
|
728
834
|
return skills.filter((skill) => skill.name.toLowerCase().includes(normalized) ||
|
|
@@ -1108,10 +1214,8 @@ async function readLocalSkillMetadata(directory) {
|
|
|
1108
1214
|
const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
|
|
1109
1215
|
try {
|
|
1110
1216
|
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
throw new Error(`Invalid skillId in ${SKILL_METADATA_FILE_NAME}.`);
|
|
1114
|
-
}
|
|
1217
|
+
const parsedSkillId = Number(parsed.skillId);
|
|
1218
|
+
const skillId = Number.isInteger(parsedSkillId) && parsedSkillId > 0 ? parsedSkillId : undefined;
|
|
1115
1219
|
return {
|
|
1116
1220
|
skillId,
|
|
1117
1221
|
skillName: typeof parsed.skillName === 'string' ? parsed.skillName : '',
|