@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.
Files changed (3) hide show
  1. package/README.md +97 -0
  2. package/dist/index.js +66 -17
  3. 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 skillId = options.flags['skill-id']
143
- ? parsePositiveInteger(options.flags['skill-id'], '--skill-id')
144
- : localSkill.metadata?.skillId;
145
- if (!skillId) {
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
- if (!localSkill.metadata?.skillId) {
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/${localSkill.metadata.skillId}`);
323
- const localVersion = normalizeText(localSkill.metadata.skillVersion);
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 skillId = Number(parsed.skillId);
1167
- if (!Number.isInteger(skillId) || skillId <= 0) {
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 : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {