@quadient/skillshare 1.1.0 → 1.2.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 +126 -101
  2. package/dist/index.js +535 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Development CLI for the Skillshare registry.
4
4
 
5
- Examples:
5
+ ## Examples
6
6
 
7
7
  ```bash
8
8
  bun run index.ts
@@ -11,126 +11,151 @@ bun run index.ts login local
11
11
  bun run index.ts list
12
12
  bun run index.ts install
13
13
  bun run index.ts install "Prompt Lint"
14
+ bun run index.ts update
15
+ bun run index.ts update "Prompt Lint" --dry-run
16
+ bun run index.ts update 12 --target ./installed
14
17
  bun run index.ts upload ./examples/prompt-lint --version 0.1.0
15
18
  bun run index.ts install 12 0.1.0 --target ./installed
16
19
  bun run index.ts install --skill-version-id 42 --target ./installed
17
20
  ```
18
21
 
19
- Interactive behavior:
22
+ ## Behavior
20
23
 
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.
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.
24
- - When skillshare.metadata.json already exists, uploads publish a new version of that skill instead of creating a duplicate entry.
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
24
  - Upload and publish commands show progress while scanning and reading local files, then keep a timed upload status visible until the registry call finishes.
25
+ - `update` reads the centralized install registry and refreshes recorded installs to the latest published registry versions.
26
+ - `update` can be narrowed to one skill or one target directory, and `--dry-run` prints planned changes without downloading or overwriting files.
27
+ - Interactive mode now checks for recorded installs with newer published versions and offers updating them before showing the main action menu.
27
28
 
28
29
  ## Current TUI Flow
29
30
 
30
31
  ```mermaid
31
32
  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
33
+ A[Start: skillshare] --> B{Command provided?}
34
+
35
+ B -- No --> C[runInteractive]
36
+ B -- Yes --> D{Which command?}
37
+
38
+ D -->|login local| E[login LOCAL_URL]
39
+ D -->|login| F[login PROD_URL]
40
+ D -->|logout| G[logoutSession]
41
+ D -->|whoami list show upload install update| H[createClient using active baseUrl]
42
+
43
+ C --> I[resolveActiveBaseUrl]
44
+ I --> J[ensureInteractiveLogin]
45
+ J --> K{Stored session exists?}
46
+ K -- No --> F
47
+ K -- Yes --> L{Refresh token still valid?}
48
+ L -- No --> F
49
+ L -- Yes --> M[createClient and GET /api/auth/me]
50
+ M --> N{Session still accepted?}
51
+ N -- No --> F
52
+ N -- Yes --> O[Interactive menu ready]
53
+
54
+ E --> P[Start PKCE and callback server]
55
+ F --> P
56
+ P --> Q[Open browser to /api/cli/login]
57
+ Q --> R[Wait for local callback with code and state]
58
+ R --> S[POST /api/cli/token]
59
+ S --> T[Store session.json]
60
+ T --> O
61
+
62
+ O --> U{Current directory contains SKILL.md?}
63
+ U -- Yes --> V{Upload current directory now?}
64
+ V -- Yes --> W[runInteractiveUpload]
65
+ V -- No --> X[Choose action]
66
+ U -- No --> X
67
+
68
+ X --> Y{Install, Update or Upload?}
69
+ Y -->|Install| Z[runInteractiveInstall]
70
+ Y -->|Update| U0[updateInstalledSkills]
71
+ Y -->|Upload| W
72
+
73
+ Z --> Z1[GET /api/skills]
74
+ Z1 --> Z2[Choose skill or resolve skillRef]
75
+ Z2 --> Z3[GET skill detail]
76
+ Z3 --> Z4[Choose version or use provided version]
77
+ Z4 --> Z5[Choose project/global scope]
78
+ Z5 --> Z6[Choose agents]
79
+ Z6 --> Z7[GET version detail]
80
+ Z7 --> Z8[POST download audit]
81
+ Z8 --> Z9[Write files to install targets]
82
+ Z9 --> Z10[Update installed.json]
83
+ Z10 --> END[Done]
84
+
85
+ U0 --> U1[Read installed.json for active baseUrl]
86
+ U1 --> U2[GET skill detail for each installed target]
87
+ U2 --> U3{Latest differs from installed version?}
88
+ U3 -- No --> U4[Skip target]
89
+ U3 -- Yes --> U5[GET latest version detail]
90
+ U5 --> U6[POST download audit]
91
+ U6 --> U7[Overwrite target files]
92
+ U7 --> U8[Update installed.json]
93
+ U4 --> END
94
+ U8 --> END
95
+
96
+ W --> W1[Load local skill context]
97
+ W1 --> W2{uploaded.json has skillId for this directory?}
98
+
99
+ W2 -- No --> W3[Prompt initial version]
100
+ W3 --> W4[createSkillFromDirectory]
101
+ W4 --> W5[POST create skill]
102
+ W5 --> W6[GET created skill detail]
103
+ W6 --> W7[Update uploaded.json]
104
+ W7 --> END
105
+
106
+ W2 -- Yes --> W8[GET existing skill detail]
107
+ W8 --> W9{Local version differs from latest?}
108
+ W9 -- Yes --> W10{Download latest first?}
109
+ W10 -- Yes --> W11[GET latest version detail]
110
+ W11 --> W12[POST download audit]
111
+ W12 --> W13[Overwrite local files and update installed.json]
112
+ W13 --> END
113
+
114
+ W10 -- No --> W14[Prompt new version]
115
+ W9 -- No --> W14
116
+ W14 --> W15[publishSkillFromDirectory]
117
+ W15 --> W16[POST publish version]
118
+ W16 --> W17[GET updated skill detail]
119
+ W17 --> W18[Update uploaded.json]
120
+ W18 --> END
123
121
  ```
124
122
 
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.
126
-
127
- Without an existing session, the CLI now defaults to the production registry. Use `login local` only when you want the local backend.
123
+ ## State Storage
128
124
 
129
125
  Session storage paths:
130
126
 
131
127
  - macOS/Linux: `~/.config/skillshare/session.json`
132
128
  - Windows: `%APPDATA%\\skillshare\\session.json`
133
129
 
130
+ Centralized TUI state:
131
+
132
+ - `uploaded.json` sits next to `session.json` and stores uploaded skill directories plus the registry skill/version they map to.
133
+ - `installed.json` sits next to `session.json` and stores installed target directories plus the installed skill/version for each target.
134
+ - New installs and uploads no longer need to create `skillshare.metadata.json` in the target directories.
135
+
136
+ ## Authentication
137
+
138
+ Bearer token precedence:
139
+
140
+ - `--token <token>` has the highest priority.
141
+ - `SKILLSHARE_TOKEN` is used when `--token` is not provided.
142
+ - Stored interactive session is used only when neither explicit token source is present.
143
+
144
+ Example non-interactive usage with a personal access token:
145
+
146
+ ```bash
147
+ export SKILLSHARE_TOKEN="<personal-access-token>"
148
+ npx @quadient/skillshare whoami
149
+ ```
150
+
151
+ Update recorded installs to the latest versions:
152
+
153
+ ```bash
154
+ npx @quadient/skillshare update
155
+ npx @quadient/skillshare update "Prompt Lint" --dry-run
156
+ npx @quadient/skillshare update 12 --target ./installed
157
+ ```
158
+
134
159
  ## Package Distribution
135
160
 
136
161
  The CLI is prepared for publishing as `@quadient/skillshare`.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { existsSync } from 'node:fs';
5
- import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
5
+ import { mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
6
6
  import { basename, dirname, join, relative, resolve } from 'node:path';
7
7
  import { homedir } from 'node:os';
8
8
  import { cancel, confirm, intro, isCancel, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
@@ -10,6 +10,8 @@ const PROD_URL = 'https://skillshare.inspireclouduat.net';
10
10
  const LOCAL_URL = 'http://localhost:5050';
11
11
  const APP_DIR = getAppDir();
12
12
  const SESSION_PATH = join(APP_DIR, 'session.json');
13
+ const UPLOADED_STATE_PATH = join(APP_DIR, 'uploaded.json');
14
+ const INSTALLED_STATE_PATH = join(APP_DIR, 'installed.json');
13
15
  const SKILL_FILE_NAME = 'SKILL.md';
14
16
  const SKILL_METADATA_FILE_NAME = 'skillshare.metadata.json';
15
17
  const UNIVERSAL_PROJECT_SKILLS_PATH = '.agents/skills';
@@ -75,9 +77,10 @@ Commands:
75
77
  install <skillId|skillName> [version]
76
78
  install <skillId|skillName> [version] --target <directory>
77
79
  install --skill-version-id <id> --target <directory>
80
+ update [skillId|skillName] [--target <directory>] [--dry-run]
78
81
 
79
82
  Options:
80
- --token <token> Bearer token for API requests
83
+ --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
81
84
  `;
82
85
  async function main() {
83
86
  const args = process.argv.slice(2);
@@ -104,8 +107,10 @@ async function main() {
104
107
  case 'list':
105
108
  case 'show':
106
109
  case 'upload':
107
- case 'install': {
108
- const client = await createClient(await resolveActiveBaseUrl(), options.token);
110
+ case 'install':
111
+ case 'update': {
112
+ const activeBaseUrl = await resolveActiveBaseUrl();
113
+ const client = await createClient(activeBaseUrl, options.token);
109
114
  switch (command) {
110
115
  case 'whoami': {
111
116
  const user = await client.get('/api/auth/me');
@@ -175,14 +180,27 @@ async function main() {
175
180
  const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
176
181
  const skill = await client.get(`/api/skills/${detail.skillId}`);
177
182
  await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
178
- await writeSkillVersionToDirectory(client, {
179
- skillId: detail.skillId,
183
+ const installProgress = createInstallProgressReporter({
180
184
  skillName: skill.name,
181
- skillDescription: skill.description,
182
185
  version: detail.version,
183
- files: detail.files,
184
- targetRoot: target,
186
+ totalTargets: 1,
187
+ ...calculateInstallTotals(detail.files, 1),
185
188
  });
189
+ try {
190
+ await writeSkillVersionToDirectory(client, {
191
+ skillId: detail.skillId,
192
+ skillName: skill.name,
193
+ skillDescription: skill.description,
194
+ version: detail.version,
195
+ files: detail.files,
196
+ targetRoot: target,
197
+ }, installProgress);
198
+ installProgress.stop();
199
+ }
200
+ catch (error) {
201
+ installProgress.clear();
202
+ throw error;
203
+ }
186
204
  console.log(`Installed skillVersion ${parsedSkillVersionId} (${detail.skillId}@${detail.version}) to ${target}`);
187
205
  return;
188
206
  }
@@ -204,17 +222,38 @@ async function main() {
204
222
  }
205
223
  const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
206
224
  await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
207
- await writeSkillVersionToDirectory(client, {
208
- skillId: skill.id,
225
+ const installProgress = createInstallProgressReporter({
209
226
  skillName: skill.name,
210
- skillDescription: skill.description,
211
227
  version: effectiveVersion,
212
- files: detail.files,
213
- targetRoot: target,
228
+ totalTargets: 1,
229
+ ...calculateInstallTotals(detail.files, 1),
214
230
  });
231
+ try {
232
+ await writeSkillVersionToDirectory(client, {
233
+ skillId: skill.id,
234
+ skillName: skill.name,
235
+ skillDescription: skill.description,
236
+ version: effectiveVersion,
237
+ files: detail.files,
238
+ targetRoot: target,
239
+ }, installProgress);
240
+ installProgress.stop();
241
+ }
242
+ catch (error) {
243
+ installProgress.clear();
244
+ throw error;
245
+ }
215
246
  console.log(`Installed ${skill.name}@${effectiveVersion} to ${target}`);
216
247
  return;
217
248
  }
249
+ case 'update': {
250
+ await updateInstalledSkills(client, {
251
+ skillRef: options.positionals[0],
252
+ target: options.flags.target,
253
+ dryRun: isFlagEnabled(options.flags, 'dry-run'),
254
+ });
255
+ return;
256
+ }
218
257
  }
219
258
  return;
220
259
  }
@@ -227,8 +266,19 @@ async function runInteractive() {
227
266
  const baseUrl = await resolveActiveBaseUrl();
228
267
  await ensureInteractiveLogin(baseUrl);
229
268
  const client = await createClient(baseUrl);
269
+ const pendingInstalledUpdates = await collectInstalledSkillUpdateCandidates(client);
270
+ if (pendingInstalledUpdates.length > 0) {
271
+ const shouldUpdateInstalled = await promptConfirm(pendingInstalledUpdates.length === 1
272
+ ? `There is 1 installed skill with a newer version available. Update it now?`
273
+ : `There are ${pendingInstalledUpdates.length} installed skills with newer versions available. Update all of them now?`, true);
274
+ if (shouldUpdateInstalled) {
275
+ await updateInstalledSkills(client, { candidates: pendingInstalledUpdates });
276
+ outro('Done.');
277
+ return;
278
+ }
279
+ }
230
280
  if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
231
- const localMetadata = await readLocalSkillMetadata(process.cwd());
281
+ const localMetadata = await readLocalSkillMetadata(process.cwd(), client.baseUrl);
232
282
  const shouldUploadCurrent = await promptConfirm(localMetadata
233
283
  ? `Current directory contains ${SKILL_FILE_NAME}. Upload a new version of ${localMetadata.skillName || `skill ${localMetadata.skillId}`} now?`
234
284
  : `Current directory contains ${SKILL_FILE_NAME}. Upload it now?`, true);
@@ -242,6 +292,7 @@ async function runInteractive() {
242
292
  message: 'What do you want to do?',
243
293
  options: [
244
294
  { value: 'install', label: 'Install a skill', hint: 'Browse registry and install to agents' },
295
+ { value: 'update', label: 'Update installed skills', hint: 'Refresh recorded installs to the latest registry versions' },
245
296
  { value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
246
297
  ],
247
298
  });
@@ -252,6 +303,9 @@ async function runInteractive() {
252
303
  if (action === 'install') {
253
304
  await runInteractiveInstall(client);
254
305
  }
306
+ else if (action === 'update') {
307
+ await updateInstalledSkills(client);
308
+ }
255
309
  else {
256
310
  await runInteractiveUpload(client);
257
311
  }
@@ -296,22 +350,35 @@ async function runInteractiveInstall(client, options) {
296
350
  await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
297
351
  const installName = createInstalledSkillDirectoryName(skill);
298
352
  const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
299
- for (const installTarget of installTargets) {
300
- await writeSkillVersionToDirectory(client, {
301
- skillId: skill.id,
302
- skillName: skill.name,
303
- skillDescription: skill.description,
304
- version: selectedVersion,
305
- files: versionDetail.files,
306
- targetRoot: installTarget.targetRoot,
307
- });
353
+ const installProgress = createInstallProgressReporter({
354
+ skillName: skill.name,
355
+ version: selectedVersion,
356
+ totalTargets: installTargets.length,
357
+ ...calculateInstallTotals(versionDetail.files, installTargets.length),
358
+ });
359
+ try {
360
+ for (const installTarget of installTargets) {
361
+ await writeSkillVersionToDirectory(client, {
362
+ skillId: skill.id,
363
+ skillName: skill.name,
364
+ skillDescription: skill.description,
365
+ version: selectedVersion,
366
+ files: versionDetail.files,
367
+ targetRoot: installTarget.targetRoot,
368
+ }, installProgress);
369
+ }
370
+ installProgress.stop();
371
+ }
372
+ catch (error) {
373
+ installProgress.clear();
374
+ throw error;
308
375
  }
309
376
  console.log(`Installed ${skill.name}@${selectedVersion} to ${installTargets.length} location(s) for ${selectedAgents.length} agent(s) in ${scope} scope.`);
310
377
  }
311
378
  async function runInteractiveUpload(client, initialDirectory) {
312
379
  const directory = initialDirectory
313
380
  ?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
314
- const localSkill = await loadLocalSkillContext(directory);
381
+ const localSkill = await loadLocalSkillContext(directory, client.baseUrl);
315
382
  const existingSkill = await resolveExistingUploadSkill(client, localSkill);
316
383
  if (!existingSkill) {
317
384
  const version = await promptText('Initial version', '0.1.0');
@@ -327,14 +394,27 @@ async function runInteractiveUpload(client, initialDirectory) {
327
394
  if (shouldDownloadLatest) {
328
395
  const latestDetail = await client.get(`/api/skills/${skill.id}/versions/${latestVersion}`);
329
396
  await client.post(`/api/skills/${skill.id}/versions/${latestVersion}/downloads`);
330
- await writeSkillVersionToDirectory(client, {
331
- skillId: skill.id,
397
+ const installProgress = createInstallProgressReporter({
332
398
  skillName: skill.name,
333
- skillDescription: skill.description,
334
399
  version: latestVersion,
335
- files: latestDetail.files,
336
- targetRoot: localSkill.directory,
400
+ totalTargets: 1,
401
+ ...calculateInstallTotals(latestDetail.files, 1),
337
402
  });
403
+ try {
404
+ await writeSkillVersionToDirectory(client, {
405
+ skillId: skill.id,
406
+ skillName: skill.name,
407
+ skillDescription: skill.description,
408
+ version: latestVersion,
409
+ files: latestDetail.files,
410
+ targetRoot: localSkill.directory,
411
+ }, installProgress);
412
+ installProgress.stop();
413
+ }
414
+ catch (error) {
415
+ installProgress.clear();
416
+ throw error;
417
+ }
338
418
  console.log(`Downloaded ${skill.name}@${latestVersion} to ${localSkill.directory}`);
339
419
  return;
340
420
  }
@@ -346,6 +426,129 @@ async function runInteractiveUpload(client, initialDirectory) {
346
426
  });
347
427
  console.log(`Published ${published.detail.name}@${published.response.version}`);
348
428
  }
429
+ async function updateInstalledSkills(client, options) {
430
+ const skillRef = normalizeText(options?.skillRef);
431
+ const targetFilter = normalizeText(options?.target);
432
+ const normalizedTargetFilter = targetFilter ? resolve(targetFilter) : null;
433
+ const dryRun = options?.dryRun ?? false;
434
+ let installedRecords = (await readInstalledSkillRecords())
435
+ .filter((record) => record.baseUrl === client.baseUrl);
436
+ if (skillRef) {
437
+ const normalizedSkillRef = skillRef.toLowerCase();
438
+ installedRecords = installedRecords.filter((record) => ((record.skillId != null && String(record.skillId) === normalizedSkillRef) ||
439
+ normalizeText(record.skillName)?.toLowerCase() === normalizedSkillRef ||
440
+ createNormalizedSkillDirectoryName(record.skillName) === normalizedSkillRef));
441
+ }
442
+ if (normalizedTargetFilter) {
443
+ installedRecords = installedRecords.filter((record) => resolve(record.targetRoot) === normalizedTargetFilter);
444
+ }
445
+ if (installedRecords.length === 0) {
446
+ const selectionDescription = formatUpdateSelectionDescription({
447
+ skillRef: skillRef ?? undefined,
448
+ target: targetFilter ?? undefined,
449
+ dryRun,
450
+ });
451
+ console.log(`No installed skills recorded for ${client.baseUrl}${selectionDescription}.`);
452
+ return;
453
+ }
454
+ const candidates = options?.candidates
455
+ ?? await collectInstalledSkillUpdateCandidates(client, {
456
+ records: installedRecords,
457
+ onSkipped(message) {
458
+ console.log(message);
459
+ },
460
+ });
461
+ const skippedCount = installedRecords.length - candidates.length;
462
+ if (candidates.length === 0) {
463
+ if (dryRun) {
464
+ console.log(`Dry run finished. Would update 0, skipped ${skippedCount}.`);
465
+ }
466
+ else {
467
+ console.log(`Update finished. Updated 0, skipped ${skippedCount}.`);
468
+ }
469
+ return;
470
+ }
471
+ let updatedCount = 0;
472
+ let plannedCount = 0;
473
+ for (const candidate of candidates) {
474
+ const { record, detail, latestVersion } = candidate;
475
+ if (dryRun) {
476
+ plannedCount += 1;
477
+ console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
478
+ continue;
479
+ }
480
+ const versionDetail = await client.get(`/api/skills/${record.skillId}/versions/${latestVersion}`);
481
+ await client.post(`/api/skills/${record.skillId}/versions/${latestVersion}/downloads`);
482
+ const installProgress = createInstallProgressReporter({
483
+ skillName: detail.name,
484
+ version: latestVersion,
485
+ totalTargets: 1,
486
+ ...calculateInstallTotals(versionDetail.files, 1),
487
+ });
488
+ try {
489
+ await writeSkillVersionToDirectory(client, {
490
+ skillId: detail.id,
491
+ skillName: detail.name,
492
+ skillDescription: detail.description,
493
+ version: latestVersion,
494
+ files: versionDetail.files,
495
+ targetRoot: record.targetRoot,
496
+ }, installProgress);
497
+ installProgress.stop();
498
+ }
499
+ catch (error) {
500
+ installProgress.clear();
501
+ throw error;
502
+ }
503
+ updatedCount += 1;
504
+ console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
505
+ }
506
+ if (dryRun) {
507
+ console.log(`Dry run finished. Would update ${plannedCount}, skipped ${skippedCount}.`);
508
+ return;
509
+ }
510
+ console.log(`Update finished. Updated ${updatedCount}, skipped ${skippedCount}.`);
511
+ }
512
+ async function collectInstalledSkillUpdateCandidates(client, options) {
513
+ const records = options?.records
514
+ ?? (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
515
+ const candidates = [];
516
+ for (const record of records) {
517
+ if (record.skillId == null) {
518
+ options?.onSkipped?.(`Skipping ${record.skillName}: installed record in ${record.targetRoot} does not contain a skill id.`);
519
+ continue;
520
+ }
521
+ const detail = await client.get(`/api/skills/${record.skillId}`);
522
+ const latestVersion = normalizeText(detail.latestVersion);
523
+ if (!latestVersion) {
524
+ options?.onSkipped?.(`Skipping ${record.skillName}: no latest version is published.`);
525
+ continue;
526
+ }
527
+ if (latestVersion === normalizeText(record.skillVersion)) {
528
+ options?.onSkipped?.(`Skipping ${record.skillName}: ${latestVersion} is already installed in ${record.targetRoot}.`);
529
+ continue;
530
+ }
531
+ candidates.push({
532
+ record,
533
+ detail,
534
+ latestVersion,
535
+ });
536
+ }
537
+ return candidates;
538
+ }
539
+ function formatUpdateSelectionDescription(options) {
540
+ const parts = [];
541
+ if (options.skillRef) {
542
+ parts.push(`skill '${options.skillRef}'`);
543
+ }
544
+ if (options.target) {
545
+ parts.push(`target '${options.target}'`);
546
+ }
547
+ if (parts.length === 0) {
548
+ return '';
549
+ }
550
+ return ` matching ${parts.join(' and ')}`;
551
+ }
349
552
  async function promptForSkill(skills) {
350
553
  let query = '';
351
554
  while (true) {
@@ -493,7 +696,7 @@ async function promptConfirm(message, initialValue = false) {
493
696
  }
494
697
  return value;
495
698
  }
496
- async function loadLocalSkillContext(directory) {
699
+ async function loadLocalSkillContext(directory, baseUrl) {
497
700
  const resolvedDirectory = resolve(directory);
498
701
  const scanStatus = spinner({ indicator: 'timer' });
499
702
  scanStatus.start(`Scanning ${resolvedDirectory}`);
@@ -529,7 +732,7 @@ async function loadLocalSkillContext(directory) {
529
732
  directory: resolvedDirectory,
530
733
  files,
531
734
  totalBytes,
532
- metadata: await readLocalSkillMetadata(resolvedDirectory),
735
+ metadata: await readLocalSkillMetadata(resolvedDirectory, baseUrl),
533
736
  defaults: deriveSkillDefaults(files, resolvedDirectory),
534
737
  };
535
738
  }
@@ -547,7 +750,7 @@ async function createSkillFromDirectory(client, localSkill, input) {
547
750
  });
548
751
  uploadStatus.message(`Fetching saved metadata for skill ${response.id}`);
549
752
  const detail = await client.get(`/api/skills/${response.id}`);
550
- await writeLocalSkillMetadata(localSkill.directory, createLocalSkillMetadata(detail, detail.latestVersion ?? input.version));
753
+ await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, detail.latestVersion ?? input.version));
551
754
  uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
552
755
  return { response, detail };
553
756
  }
@@ -567,7 +770,7 @@ async function publishSkillFromDirectory(client, localSkill, input) {
567
770
  });
568
771
  uploadStatus.message(`Fetching saved metadata for skill ${input.skillId}`);
569
772
  const detail = await client.get(`/api/skills/${input.skillId}`);
570
- await writeLocalSkillMetadata(localSkill.directory, createLocalSkillMetadata(detail, response.version));
773
+ await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, response.version));
571
774
  uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
572
775
  return { response, detail };
573
776
  }
@@ -576,19 +779,178 @@ async function publishSkillFromDirectory(client, localSkill, input) {
576
779
  throw error;
577
780
  }
578
781
  }
579
- async function writeSkillVersionToDirectory(client, input) {
782
+ async function writeSkillVersionToDirectory(client, input, installProgress) {
783
+ installProgress?.onTargetStart(input.targetRoot);
580
784
  for (const file of input.files) {
785
+ installProgress?.onFileStart(input.targetRoot, file.nameWithPath, file.length);
581
786
  const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${file.nameWithPath}`);
582
787
  const destination = join(input.targetRoot, file.nameWithPath);
583
788
  await mkdir(dirname(destination), { recursive: true });
584
- await writeFile(destination, Buffer.from(await response.arrayBuffer()));
789
+ await downloadResponseToFile(response, destination, (bytes) => {
790
+ installProgress?.onBytesTransferred(bytes);
791
+ });
792
+ installProgress?.onFileComplete();
585
793
  }
586
- await writeLocalSkillMetadata(input.targetRoot, {
794
+ await upsertInstalledSkillRecord(client.baseUrl, input.targetRoot, {
587
795
  skillId: input.skillId,
588
796
  skillName: input.skillName,
589
797
  skillDescription: input.skillDescription,
590
798
  skillVersion: input.version,
591
799
  });
800
+ installProgress?.onTargetComplete();
801
+ }
802
+ async function downloadResponseToFile(response, destination, onBytesTransferred) {
803
+ const body = response.body;
804
+ if (!body) {
805
+ const buffer = Buffer.from(await response.arrayBuffer());
806
+ await writeFile(destination, buffer);
807
+ onBytesTransferred?.(buffer.length);
808
+ return;
809
+ }
810
+ const fileHandle = await open(destination, 'w');
811
+ let failed = true;
812
+ try {
813
+ const reader = body.getReader();
814
+ while (true) {
815
+ const { done, value } = await reader.read();
816
+ if (done) {
817
+ failed = false;
818
+ break;
819
+ }
820
+ if (!value || value.length === 0) {
821
+ continue;
822
+ }
823
+ await fileHandle.writeFile(value);
824
+ onBytesTransferred?.(value.length);
825
+ }
826
+ }
827
+ finally {
828
+ await fileHandle.close();
829
+ if (failed) {
830
+ await rm(destination, { force: true });
831
+ }
832
+ }
833
+ }
834
+ function createInstallProgressReporter(input) {
835
+ const installStatus = spinner({ indicator: 'timer' });
836
+ const state = {
837
+ completedFiles: 0,
838
+ completedTargets: 0,
839
+ currentFileBytes: 0,
840
+ currentFileExpectedBytes: 0,
841
+ currentFilePath: null,
842
+ currentTargetRoot: null,
843
+ transferredBytes: 0,
844
+ };
845
+ let lastRenderAt = 0;
846
+ let lastRenderedBytes = 0;
847
+ installStatus.start(formatInstallProgressMessage(input, state));
848
+ function render(force = false) {
849
+ const now = Date.now();
850
+ const bytesDelta = state.transferredBytes - lastRenderedBytes;
851
+ if (!force && now - lastRenderAt < 100 && bytesDelta < 64 * 1024) {
852
+ return;
853
+ }
854
+ installStatus.message(formatInstallProgressMessage(input, state));
855
+ lastRenderAt = now;
856
+ lastRenderedBytes = state.transferredBytes;
857
+ }
858
+ return {
859
+ onTargetStart(targetRoot) {
860
+ state.currentTargetRoot = targetRoot;
861
+ render(true);
862
+ },
863
+ onFileStart(targetRoot, relativePath, expectedBytes) {
864
+ state.currentTargetRoot = targetRoot;
865
+ state.currentFilePath = relativePath;
866
+ state.currentFileExpectedBytes = Math.max(expectedBytes, 0);
867
+ state.currentFileBytes = 0;
868
+ render(true);
869
+ },
870
+ onBytesTransferred(bytes) {
871
+ state.transferredBytes += bytes;
872
+ state.currentFileBytes += bytes;
873
+ render();
874
+ },
875
+ onFileComplete() {
876
+ state.completedFiles += 1;
877
+ if (state.currentFileExpectedBytes > 0) {
878
+ state.currentFileBytes = state.currentFileExpectedBytes;
879
+ }
880
+ render(true);
881
+ },
882
+ onTargetComplete() {
883
+ state.completedTargets += 1;
884
+ render(true);
885
+ },
886
+ stop() {
887
+ installStatus.stop(formatInstallCompletedMessage(input, state));
888
+ },
889
+ clear() {
890
+ installStatus.clear();
891
+ },
892
+ };
893
+ }
894
+ function calculateInstallTotals(files, targetCount) {
895
+ const totalFiles = files.length * targetCount;
896
+ const totalBytes = files.reduce((sum, file) => sum + Math.max(file.length, 0), 0) * targetCount;
897
+ return { totalFiles, totalBytes };
898
+ }
899
+ function formatInstallProgressMessage(input, state) {
900
+ const parts = [
901
+ `Installing ${input.skillName}@${input.version}`,
902
+ `files ${state.completedFiles}/${input.totalFiles}`,
903
+ input.totalBytes > 0
904
+ ? `bytes ${formatByteSize(Math.min(state.transferredBytes, input.totalBytes))}/${formatByteSize(input.totalBytes)}`
905
+ : `bytes ${formatByteSize(state.transferredBytes)}`,
906
+ ];
907
+ if (input.totalTargets > 1) {
908
+ parts.push(`targets ${state.completedTargets}/${input.totalTargets}`);
909
+ }
910
+ if (state.currentTargetRoot) {
911
+ parts.push(`target ${formatInstallTargetRoot(state.currentTargetRoot)}`);
912
+ }
913
+ if (state.currentFilePath) {
914
+ const fileProgress = state.currentFileExpectedBytes > 0
915
+ ? ` (${formatByteSize(Math.min(state.currentFileBytes, state.currentFileExpectedBytes))}/${formatByteSize(state.currentFileExpectedBytes)})`
916
+ : state.currentFileBytes > 0
917
+ ? ` (${formatByteSize(state.currentFileBytes)})`
918
+ : '';
919
+ parts.push(`${state.currentFilePath}${fileProgress}`);
920
+ }
921
+ return parts.join(' · ');
922
+ }
923
+ function formatInstallCompletedMessage(input, state) {
924
+ const parts = [
925
+ `Installed ${input.skillName}@${input.version}`,
926
+ `files ${state.completedFiles}/${input.totalFiles}`,
927
+ input.totalBytes > 0
928
+ ? `bytes ${formatByteSize(Math.min(state.transferredBytes, input.totalBytes))}/${formatByteSize(input.totalBytes)}`
929
+ : `bytes ${formatByteSize(state.transferredBytes)}`,
930
+ ];
931
+ if (input.totalTargets > 1) {
932
+ parts.push(`targets ${state.completedTargets}/${input.totalTargets}`);
933
+ }
934
+ return parts.join(' · ');
935
+ }
936
+ function formatInstallTargetRoot(targetRoot) {
937
+ const cwd = process.cwd();
938
+ if (targetRoot === cwd) {
939
+ return '.';
940
+ }
941
+ const relativeToCwd = relative(cwd, targetRoot);
942
+ if (relativeToCwd && relativeToCwd !== '' && !relativeToCwd.startsWith('..')) {
943
+ return relativeToCwd.startsWith('.') ? relativeToCwd : `./${relativeToCwd}`;
944
+ }
945
+ const home = homedir();
946
+ if (targetRoot === home) {
947
+ return '~';
948
+ }
949
+ const relativeToHome = relative(home, targetRoot);
950
+ if (relativeToHome && relativeToHome !== '' && !relativeToHome.startsWith('..')) {
951
+ return `~/${relativeToHome}`;
952
+ }
953
+ return targetRoot;
592
954
  }
593
955
  function deriveSkillDefaults(files, directory) {
594
956
  const skillFile = files.find((file) => file.relativePath === SKILL_FILE_NAME);
@@ -856,11 +1218,15 @@ function resolveInstallTargets(agents, scope, installName) {
856
1218
  }
857
1219
  return [...targets.values()];
858
1220
  }
859
- function createInstalledSkillDirectoryName(skill) {
860
- const normalized = skill.name
1221
+ function createNormalizedSkillDirectoryName(name) {
1222
+ const normalized = name
861
1223
  .toLowerCase()
862
1224
  .replace(/[^a-z0-9]+/g, '-')
863
1225
  .replace(/^-+|-+$/g, '');
1226
+ return normalized;
1227
+ }
1228
+ function createInstalledSkillDirectoryName(skill) {
1229
+ const normalized = createNormalizedSkillDirectoryName(skill.name);
864
1230
  return normalized.length > 0 ? normalized : `skill-${skill.id}`;
865
1231
  }
866
1232
  async function login(baseUrl) {
@@ -898,6 +1264,7 @@ async function login(baseUrl) {
898
1264
  console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
899
1265
  }
900
1266
  async function parseOptions(args) {
1267
+ const booleanFlags = new Set(['dry-run']);
901
1268
  const flags = {};
902
1269
  const positionals = [];
903
1270
  for (let index = 0; index < args.length; index += 1) {
@@ -909,6 +1276,10 @@ async function parseOptions(args) {
909
1276
  const key = current.slice(2);
910
1277
  const value = args[index + 1];
911
1278
  if (!value || value.startsWith('--')) {
1279
+ if (booleanFlags.has(key)) {
1280
+ flags[key] = 'true';
1281
+ continue;
1282
+ }
912
1283
  throw new Error(`Missing value for ${current}`);
913
1284
  }
914
1285
  flags[key] = value;
@@ -918,12 +1289,21 @@ async function parseOptions(args) {
918
1289
  positionals.push(current);
919
1290
  }
920
1291
  }
1292
+ const tokenFromFlag = normalizeText(flags.token);
1293
+ const tokenFromEnvironment = normalizeText(process.env.SKILLSHARE_TOKEN);
921
1294
  return {
922
- token: flags.token,
1295
+ token: tokenFromFlag ?? tokenFromEnvironment ?? undefined,
923
1296
  flags,
924
1297
  positionals,
925
1298
  };
926
1299
  }
1300
+ function isFlagEnabled(flags, key) {
1301
+ const value = normalizeText(flags[key]);
1302
+ if (!value) {
1303
+ return false;
1304
+ }
1305
+ return !['0', 'false', 'no', 'off'].includes(value.toLowerCase());
1306
+ }
927
1307
  async function resolveActiveBaseUrl() {
928
1308
  const session = await loadAnySession();
929
1309
  if (session?.baseUrl) {
@@ -996,6 +1376,7 @@ async function createClient(baseUrl, explicitToken) {
996
1376
  });
997
1377
  }
998
1378
  return {
1379
+ baseUrl: resolvedBaseUrl,
999
1380
  async get(path) {
1000
1381
  const response = await sendAuthorized(path);
1001
1382
  return parseJson(response);
@@ -1210,7 +1591,24 @@ async function waitForLoginCallback() {
1210
1591
  },
1211
1592
  };
1212
1593
  }
1213
- async function readLocalSkillMetadata(directory) {
1594
+ async function readLocalSkillMetadata(directory, baseUrl) {
1595
+ const resolvedDirectory = resolve(directory);
1596
+ const normalizedBaseUrl = normalizeText(baseUrl) ? normalizeBaseUrl(baseUrl) : null;
1597
+ const uploadedRecords = await readUploadedSkillRecords();
1598
+ const matchingRecord = uploadedRecords
1599
+ .filter((record) => record.directory === resolvedDirectory && (!normalizedBaseUrl || record.baseUrl === normalizedBaseUrl))
1600
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
1601
+ if (matchingRecord) {
1602
+ return {
1603
+ skillId: matchingRecord.skillId,
1604
+ skillName: matchingRecord.skillName,
1605
+ skillDescription: matchingRecord.skillDescription,
1606
+ skillVersion: matchingRecord.skillVersion,
1607
+ };
1608
+ }
1609
+ return readLegacyLocalSkillMetadata(resolvedDirectory);
1610
+ }
1611
+ async function readLegacyLocalSkillMetadata(directory) {
1214
1612
  const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
1215
1613
  try {
1216
1614
  const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
@@ -1230,9 +1628,102 @@ async function readLocalSkillMetadata(directory) {
1230
1628
  throw new Error(`Failed to read ${metadataPath}: ${error instanceof Error ? error.message : String(error)}`);
1231
1629
  }
1232
1630
  }
1233
- async function writeLocalSkillMetadata(directory, metadata) {
1234
- await mkdir(directory, { recursive: true });
1235
- await writeFile(join(directory, SKILL_METADATA_FILE_NAME), `${JSON.stringify(metadata, null, 2)}\n`);
1631
+ async function upsertUploadedSkillMetadata(baseUrl, directory, metadata) {
1632
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
1633
+ const resolvedDirectory = resolve(directory);
1634
+ const records = (await readUploadedSkillRecords())
1635
+ .filter((record) => !(record.directory === resolvedDirectory && record.baseUrl === normalizedBaseUrl));
1636
+ records.unshift({
1637
+ ...metadata,
1638
+ directory: resolvedDirectory,
1639
+ baseUrl: normalizedBaseUrl,
1640
+ updatedAt: new Date().toISOString(),
1641
+ });
1642
+ await writeStateFile(UPLOADED_STATE_PATH, records);
1643
+ await removeLegacyLocalSkillMetadata(resolvedDirectory);
1644
+ }
1645
+ async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
1646
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
1647
+ const resolvedTargetRoot = resolve(targetRoot);
1648
+ const records = (await readInstalledSkillRecords())
1649
+ .filter((record) => !(record.targetRoot === resolvedTargetRoot && record.baseUrl === normalizedBaseUrl));
1650
+ records.unshift({
1651
+ ...metadata,
1652
+ targetRoot: resolvedTargetRoot,
1653
+ baseUrl: normalizedBaseUrl,
1654
+ updatedAt: new Date().toISOString(),
1655
+ });
1656
+ await writeStateFile(INSTALLED_STATE_PATH, records);
1657
+ await removeLegacyLocalSkillMetadata(resolvedTargetRoot);
1658
+ }
1659
+ async function readUploadedSkillRecords() {
1660
+ const parsed = await readStateFile(UPLOADED_STATE_PATH);
1661
+ return parsed.flatMap((entry) => {
1662
+ if (!entry || typeof entry !== 'object') {
1663
+ return [];
1664
+ }
1665
+ const skillId = Number(entry.skillId);
1666
+ const normalizedSkillId = Number.isInteger(skillId) && skillId > 0 ? skillId : undefined;
1667
+ return [{
1668
+ directory: typeof entry.directory === 'string' ? resolve(entry.directory) : '',
1669
+ baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
1670
+ updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
1671
+ skillId: normalizedSkillId,
1672
+ skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
1673
+ skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
1674
+ skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
1675
+ }].filter((record) => record.directory.length > 0);
1676
+ });
1677
+ }
1678
+ async function readInstalledSkillRecords() {
1679
+ const parsed = await readStateFile(INSTALLED_STATE_PATH);
1680
+ return parsed.flatMap((entry) => {
1681
+ if (!entry || typeof entry !== 'object') {
1682
+ return [];
1683
+ }
1684
+ const skillId = Number(entry.skillId);
1685
+ if (!Number.isInteger(skillId) || skillId <= 0) {
1686
+ return [];
1687
+ }
1688
+ return [{
1689
+ targetRoot: typeof entry.targetRoot === 'string' ? resolve(entry.targetRoot) : '',
1690
+ baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
1691
+ updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
1692
+ skillId,
1693
+ skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
1694
+ skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
1695
+ skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
1696
+ }].filter((record) => record.targetRoot.length > 0);
1697
+ });
1698
+ }
1699
+ async function readStateFile(path) {
1700
+ try {
1701
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
1702
+ return Array.isArray(parsed) ? parsed : [];
1703
+ }
1704
+ catch (error) {
1705
+ if (error?.code === 'ENOENT') {
1706
+ return [];
1707
+ }
1708
+ throw new Error(`Failed to read ${path}: ${error instanceof Error ? error.message : String(error)}`);
1709
+ }
1710
+ }
1711
+ async function writeStateFile(path, value) {
1712
+ await mkdir(APP_DIR, { recursive: true });
1713
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
1714
+ mode: process.platform === 'win32' ? undefined : 0o600,
1715
+ });
1716
+ }
1717
+ async function removeLegacyLocalSkillMetadata(directory) {
1718
+ try {
1719
+ await rm(join(resolve(directory), SKILL_METADATA_FILE_NAME));
1720
+ }
1721
+ catch (error) {
1722
+ if (error?.code === 'ENOENT') {
1723
+ return;
1724
+ }
1725
+ throw error;
1726
+ }
1236
1727
  }
1237
1728
  async function collectFilePlans(root, current = root) {
1238
1729
  const entries = await readdir(current, { withFileTypes: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {