@quadient/skillshare 1.0.0 → 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 +97 -0
- package/dist/index.js +66 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,103 @@ Interactive behavior:
|
|
|
25
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.
|
|
26
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.
|
|
27
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
|
+
|
|
28
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.
|
|
29
126
|
|
|
30
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
|
@@ -139,10 +139,10 @@ async function main() {
|
|
|
139
139
|
throw new Error('upload requires <directory> --version <version>');
|
|
140
140
|
}
|
|
141
141
|
const localSkill = await loadLocalSkillContext(directory);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!
|
|
142
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill, {
|
|
143
|
+
explicitSkillId: options.flags['skill-id'],
|
|
144
|
+
});
|
|
145
|
+
if (!existingSkill) {
|
|
146
146
|
const created = await createSkillFromDirectory(client, localSkill, {
|
|
147
147
|
name: options.flags.name,
|
|
148
148
|
description: options.flags.description,
|
|
@@ -152,9 +152,8 @@ async function main() {
|
|
|
152
152
|
}
|
|
153
153
|
else {
|
|
154
154
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
155
|
-
skillId,
|
|
155
|
+
skillId: existingSkill.id,
|
|
156
156
|
version,
|
|
157
|
-
failIfMissing: false,
|
|
158
157
|
});
|
|
159
158
|
console.log(JSON.stringify(published.response, null, 2));
|
|
160
159
|
}
|
|
@@ -313,14 +312,15 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
313
312
|
const directory = initialDirectory
|
|
314
313
|
?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
|
|
315
314
|
const localSkill = await loadLocalSkillContext(directory);
|
|
316
|
-
|
|
315
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill);
|
|
316
|
+
if (!existingSkill) {
|
|
317
317
|
const version = await promptText('Initial version', '0.1.0');
|
|
318
318
|
const created = await createSkillFromDirectory(client, localSkill, { version });
|
|
319
319
|
console.log(`Created skill ${created.detail.id}: ${created.detail.name}@${version}`);
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
|
-
const skill = await client.get(`/api/skills/${
|
|
323
|
-
const localVersion = normalizeText(localSkill.metadata
|
|
322
|
+
const skill = await client.get(`/api/skills/${existingSkill.id}`);
|
|
323
|
+
const localVersion = normalizeText(localSkill.metadata?.skillVersion);
|
|
324
324
|
const latestVersion = normalizeText(skill.latestVersion);
|
|
325
325
|
if (latestVersion && localVersion && latestVersion !== localVersion) {
|
|
326
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);
|
|
@@ -343,7 +343,6 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
343
343
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
344
344
|
skillId: skill.id,
|
|
345
345
|
version,
|
|
346
|
-
failIfMissing: true,
|
|
347
346
|
});
|
|
348
347
|
console.log(`Published ${published.detail.name}@${published.response.version}`);
|
|
349
348
|
}
|
|
@@ -558,9 +557,6 @@ async function createSkillFromDirectory(client, localSkill, input) {
|
|
|
558
557
|
}
|
|
559
558
|
}
|
|
560
559
|
async function publishSkillFromDirectory(client, localSkill, input) {
|
|
561
|
-
if (input.failIfMissing && !localSkill.metadata?.skillId) {
|
|
562
|
-
throw new Error(`Missing ${SKILL_METADATA_FILE_NAME}. Create the skill first or restore the metadata file.`);
|
|
563
|
-
}
|
|
564
560
|
const uploadStatus = spinner({ indicator: 'timer' });
|
|
565
561
|
uploadStatus.start(`Uploading ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
566
562
|
try {
|
|
@@ -737,6 +733,61 @@ function normalizeText(value) {
|
|
|
737
733
|
const trimmed = value?.trim();
|
|
738
734
|
return trimmed ? trimmed : null;
|
|
739
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
|
+
}
|
|
740
791
|
function resolveSkillReference(skills, reference) {
|
|
741
792
|
const normalizedReference = normalizeText(reference)?.toLowerCase();
|
|
742
793
|
if (!normalizedReference) {
|
|
@@ -1163,10 +1214,8 @@ async function readLocalSkillMetadata(directory) {
|
|
|
1163
1214
|
const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
|
|
1164
1215
|
try {
|
|
1165
1216
|
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
throw new Error(`Invalid skillId in ${SKILL_METADATA_FILE_NAME}.`);
|
|
1169
|
-
}
|
|
1217
|
+
const parsedSkillId = Number(parsed.skillId);
|
|
1218
|
+
const skillId = Number.isInteger(parsedSkillId) && parsedSkillId > 0 ? parsedSkillId : undefined;
|
|
1170
1219
|
return {
|
|
1171
1220
|
skillId,
|
|
1172
1221
|
skillName: typeof parsed.skillName === 'string' ? parsed.skillName : '',
|