@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.
Files changed (3) hide show
  1. package/README.md +100 -0
  2. package/dist/index.js +133 -29
  3. 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 <skillId> [version] --target <directory>
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 skillId = options.flags['skill-id']
141
- ? parsePositiveInteger(options.flags['skill-id'], '--skill-id')
142
- : localSkill.metadata?.skillId;
143
- if (!skillId) {
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 skillId = options.positionals[0];
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 (!skillId || !target) {
189
- throw new Error('install requires <skillId> [version] --target <directory> or --skill-version-id <id> --target <directory>');
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 skill = await client.get(`/api/skills/${skillId}`);
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 '${skillId}' does not have a published version.`);
203
+ throw new Error(`Skill '${skill.name}' does not have a published version.`);
195
204
  }
196
- const detail = await client.get(`/api/skills/${skillId}/versions/${effectiveVersion}`);
197
- await client.post(`/api/skills/${skillId}/versions/${effectiveVersion}/downloads`);
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 ${skillId}@${effectiveVersion} to ${target}`);
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 = await promptForSkill(skills);
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 = await promptForVersion(detail);
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
- if (!localSkill.metadata?.skillId) {
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/${localSkill.metadata.skillId}`);
309
- const localVersion = normalizeText(localSkill.metadata.skillVersion);
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 skillId = Number(parsed.skillId);
1112
- if (!Number.isInteger(skillId) || skillId <= 0) {
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 : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "0.1.1",
3
+ "version": "1.1.0",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {