@quadient/skillshare 1.0.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.
- package/README.md +132 -10
- package/dist/index.js +601 -61
- 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,29 +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
|
-
|
|
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.
|
|
28
|
+
|
|
29
|
+
## Current TUI Flow
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart TD
|
|
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
|
|
121
|
+
```
|
|
27
122
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
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
|
|
31
124
|
|
|
32
125
|
Session storage paths:
|
|
33
126
|
|
|
34
127
|
- macOS/Linux: `~/.config/skillshare/session.json`
|
|
35
128
|
- Windows: `%APPDATA%\\skillshare\\session.json`
|
|
36
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
|
+
|
|
37
159
|
## Package Distribution
|
|
38
160
|
|
|
39
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
|
-
|
|
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');
|
|
@@ -139,10 +144,10 @@ async function main() {
|
|
|
139
144
|
throw new Error('upload requires <directory> --version <version>');
|
|
140
145
|
}
|
|
141
146
|
const localSkill = await loadLocalSkillContext(directory);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!
|
|
147
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill, {
|
|
148
|
+
explicitSkillId: options.flags['skill-id'],
|
|
149
|
+
});
|
|
150
|
+
if (!existingSkill) {
|
|
146
151
|
const created = await createSkillFromDirectory(client, localSkill, {
|
|
147
152
|
name: options.flags.name,
|
|
148
153
|
description: options.flags.description,
|
|
@@ -152,9 +157,8 @@ async function main() {
|
|
|
152
157
|
}
|
|
153
158
|
else {
|
|
154
159
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
155
|
-
skillId,
|
|
160
|
+
skillId: existingSkill.id,
|
|
156
161
|
version,
|
|
157
|
-
failIfMissing: false,
|
|
158
162
|
});
|
|
159
163
|
console.log(JSON.stringify(published.response, null, 2));
|
|
160
164
|
}
|
|
@@ -176,14 +180,27 @@ async function main() {
|
|
|
176
180
|
const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
|
|
177
181
|
const skill = await client.get(`/api/skills/${detail.skillId}`);
|
|
178
182
|
await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
|
|
179
|
-
|
|
180
|
-
skillId: detail.skillId,
|
|
183
|
+
const installProgress = createInstallProgressReporter({
|
|
181
184
|
skillName: skill.name,
|
|
182
|
-
skillDescription: skill.description,
|
|
183
185
|
version: detail.version,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
totalTargets: 1,
|
|
187
|
+
...calculateInstallTotals(detail.files, 1),
|
|
186
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
|
+
}
|
|
187
204
|
console.log(`Installed skillVersion ${parsedSkillVersionId} (${detail.skillId}@${detail.version}) to ${target}`);
|
|
188
205
|
return;
|
|
189
206
|
}
|
|
@@ -205,17 +222,38 @@ async function main() {
|
|
|
205
222
|
}
|
|
206
223
|
const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
|
|
207
224
|
await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
|
|
208
|
-
|
|
209
|
-
skillId: skill.id,
|
|
225
|
+
const installProgress = createInstallProgressReporter({
|
|
210
226
|
skillName: skill.name,
|
|
211
|
-
skillDescription: skill.description,
|
|
212
227
|
version: effectiveVersion,
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
totalTargets: 1,
|
|
229
|
+
...calculateInstallTotals(detail.files, 1),
|
|
215
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
|
+
}
|
|
216
246
|
console.log(`Installed ${skill.name}@${effectiveVersion} to ${target}`);
|
|
217
247
|
return;
|
|
218
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
|
+
}
|
|
219
257
|
}
|
|
220
258
|
return;
|
|
221
259
|
}
|
|
@@ -228,8 +266,19 @@ async function runInteractive() {
|
|
|
228
266
|
const baseUrl = await resolveActiveBaseUrl();
|
|
229
267
|
await ensureInteractiveLogin(baseUrl);
|
|
230
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
|
+
}
|
|
231
280
|
if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
|
|
232
|
-
const localMetadata = await readLocalSkillMetadata(process.cwd());
|
|
281
|
+
const localMetadata = await readLocalSkillMetadata(process.cwd(), client.baseUrl);
|
|
233
282
|
const shouldUploadCurrent = await promptConfirm(localMetadata
|
|
234
283
|
? `Current directory contains ${SKILL_FILE_NAME}. Upload a new version of ${localMetadata.skillName || `skill ${localMetadata.skillId}`} now?`
|
|
235
284
|
: `Current directory contains ${SKILL_FILE_NAME}. Upload it now?`, true);
|
|
@@ -243,6 +292,7 @@ async function runInteractive() {
|
|
|
243
292
|
message: 'What do you want to do?',
|
|
244
293
|
options: [
|
|
245
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' },
|
|
246
296
|
{ value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
|
|
247
297
|
],
|
|
248
298
|
});
|
|
@@ -253,6 +303,9 @@ async function runInteractive() {
|
|
|
253
303
|
if (action === 'install') {
|
|
254
304
|
await runInteractiveInstall(client);
|
|
255
305
|
}
|
|
306
|
+
else if (action === 'update') {
|
|
307
|
+
await updateInstalledSkills(client);
|
|
308
|
+
}
|
|
256
309
|
else {
|
|
257
310
|
await runInteractiveUpload(client);
|
|
258
311
|
}
|
|
@@ -297,44 +350,71 @@ async function runInteractiveInstall(client, options) {
|
|
|
297
350
|
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
298
351
|
const installName = createInstalledSkillDirectoryName(skill);
|
|
299
352
|
const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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;
|
|
309
375
|
}
|
|
310
376
|
console.log(`Installed ${skill.name}@${selectedVersion} to ${installTargets.length} location(s) for ${selectedAgents.length} agent(s) in ${scope} scope.`);
|
|
311
377
|
}
|
|
312
378
|
async function runInteractiveUpload(client, initialDirectory) {
|
|
313
379
|
const directory = initialDirectory
|
|
314
380
|
?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
|
|
315
|
-
const localSkill = await loadLocalSkillContext(directory);
|
|
316
|
-
|
|
381
|
+
const localSkill = await loadLocalSkillContext(directory, client.baseUrl);
|
|
382
|
+
const existingSkill = await resolveExistingUploadSkill(client, localSkill);
|
|
383
|
+
if (!existingSkill) {
|
|
317
384
|
const version = await promptText('Initial version', '0.1.0');
|
|
318
385
|
const created = await createSkillFromDirectory(client, localSkill, { version });
|
|
319
386
|
console.log(`Created skill ${created.detail.id}: ${created.detail.name}@${version}`);
|
|
320
387
|
return;
|
|
321
388
|
}
|
|
322
|
-
const skill = await client.get(`/api/skills/${
|
|
323
|
-
const localVersion = normalizeText(localSkill.metadata
|
|
389
|
+
const skill = await client.get(`/api/skills/${existingSkill.id}`);
|
|
390
|
+
const localVersion = normalizeText(localSkill.metadata?.skillVersion);
|
|
324
391
|
const latestVersion = normalizeText(skill.latestVersion);
|
|
325
392
|
if (latestVersion && localVersion && latestVersion !== localVersion) {
|
|
326
393
|
const shouldDownloadLatest = await promptConfirm(`Local version is ${localVersion}, but the server latest is ${latestVersion}. Download it first? Local file changes will be overwritten.`, false);
|
|
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
|
-
|
|
331
|
-
skillId: skill.id,
|
|
397
|
+
const installProgress = createInstallProgressReporter({
|
|
332
398
|
skillName: skill.name,
|
|
333
|
-
skillDescription: skill.description,
|
|
334
399
|
version: latestVersion,
|
|
335
|
-
|
|
336
|
-
|
|
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
|
}
|
|
@@ -343,10 +423,132 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
343
423
|
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
344
424
|
skillId: skill.id,
|
|
345
425
|
version,
|
|
346
|
-
failIfMissing: true,
|
|
347
426
|
});
|
|
348
427
|
console.log(`Published ${published.detail.name}@${published.response.version}`);
|
|
349
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
|
+
}
|
|
350
552
|
async function promptForSkill(skills) {
|
|
351
553
|
let query = '';
|
|
352
554
|
while (true) {
|
|
@@ -494,7 +696,7 @@ async function promptConfirm(message, initialValue = false) {
|
|
|
494
696
|
}
|
|
495
697
|
return value;
|
|
496
698
|
}
|
|
497
|
-
async function loadLocalSkillContext(directory) {
|
|
699
|
+
async function loadLocalSkillContext(directory, baseUrl) {
|
|
498
700
|
const resolvedDirectory = resolve(directory);
|
|
499
701
|
const scanStatus = spinner({ indicator: 'timer' });
|
|
500
702
|
scanStatus.start(`Scanning ${resolvedDirectory}`);
|
|
@@ -530,7 +732,7 @@ async function loadLocalSkillContext(directory) {
|
|
|
530
732
|
directory: resolvedDirectory,
|
|
531
733
|
files,
|
|
532
734
|
totalBytes,
|
|
533
|
-
metadata: await readLocalSkillMetadata(resolvedDirectory),
|
|
735
|
+
metadata: await readLocalSkillMetadata(resolvedDirectory, baseUrl),
|
|
534
736
|
defaults: deriveSkillDefaults(files, resolvedDirectory),
|
|
535
737
|
};
|
|
536
738
|
}
|
|
@@ -548,7 +750,7 @@ async function createSkillFromDirectory(client, localSkill, input) {
|
|
|
548
750
|
});
|
|
549
751
|
uploadStatus.message(`Fetching saved metadata for skill ${response.id}`);
|
|
550
752
|
const detail = await client.get(`/api/skills/${response.id}`);
|
|
551
|
-
await
|
|
753
|
+
await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, detail.latestVersion ?? input.version));
|
|
552
754
|
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
553
755
|
return { response, detail };
|
|
554
756
|
}
|
|
@@ -558,9 +760,6 @@ async function createSkillFromDirectory(client, localSkill, input) {
|
|
|
558
760
|
}
|
|
559
761
|
}
|
|
560
762
|
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
763
|
const uploadStatus = spinner({ indicator: 'timer' });
|
|
565
764
|
uploadStatus.start(`Uploading ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
566
765
|
try {
|
|
@@ -571,7 +770,7 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
571
770
|
});
|
|
572
771
|
uploadStatus.message(`Fetching saved metadata for skill ${input.skillId}`);
|
|
573
772
|
const detail = await client.get(`/api/skills/${input.skillId}`);
|
|
574
|
-
await
|
|
773
|
+
await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, response.version));
|
|
575
774
|
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
576
775
|
return { response, detail };
|
|
577
776
|
}
|
|
@@ -580,19 +779,178 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
580
779
|
throw error;
|
|
581
780
|
}
|
|
582
781
|
}
|
|
583
|
-
async function writeSkillVersionToDirectory(client, input) {
|
|
782
|
+
async function writeSkillVersionToDirectory(client, input, installProgress) {
|
|
783
|
+
installProgress?.onTargetStart(input.targetRoot);
|
|
584
784
|
for (const file of input.files) {
|
|
785
|
+
installProgress?.onFileStart(input.targetRoot, file.nameWithPath, file.length);
|
|
585
786
|
const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${file.nameWithPath}`);
|
|
586
787
|
const destination = join(input.targetRoot, file.nameWithPath);
|
|
587
788
|
await mkdir(dirname(destination), { recursive: true });
|
|
588
|
-
await
|
|
789
|
+
await downloadResponseToFile(response, destination, (bytes) => {
|
|
790
|
+
installProgress?.onBytesTransferred(bytes);
|
|
791
|
+
});
|
|
792
|
+
installProgress?.onFileComplete();
|
|
589
793
|
}
|
|
590
|
-
await
|
|
794
|
+
await upsertInstalledSkillRecord(client.baseUrl, input.targetRoot, {
|
|
591
795
|
skillId: input.skillId,
|
|
592
796
|
skillName: input.skillName,
|
|
593
797
|
skillDescription: input.skillDescription,
|
|
594
798
|
skillVersion: input.version,
|
|
595
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;
|
|
596
954
|
}
|
|
597
955
|
function deriveSkillDefaults(files, directory) {
|
|
598
956
|
const skillFile = files.find((file) => file.relativePath === SKILL_FILE_NAME);
|
|
@@ -737,6 +1095,61 @@ function normalizeText(value) {
|
|
|
737
1095
|
const trimmed = value?.trim();
|
|
738
1096
|
return trimmed ? trimmed : null;
|
|
739
1097
|
}
|
|
1098
|
+
async function resolveExistingUploadSkill(client, localSkill, options) {
|
|
1099
|
+
const explicitSkillId = normalizeText(options?.explicitSkillId);
|
|
1100
|
+
if (explicitSkillId) {
|
|
1101
|
+
return {
|
|
1102
|
+
id: parsePositiveInteger(explicitSkillId, '--skill-id'),
|
|
1103
|
+
name: '',
|
|
1104
|
+
description: '',
|
|
1105
|
+
ownerUserId: 0,
|
|
1106
|
+
created: '',
|
|
1107
|
+
updated: '',
|
|
1108
|
+
deprecated: false,
|
|
1109
|
+
latestVersion: null,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
if (localSkill.metadata?.skillId) {
|
|
1113
|
+
return {
|
|
1114
|
+
id: localSkill.metadata.skillId,
|
|
1115
|
+
name: localSkill.metadata.skillName,
|
|
1116
|
+
description: localSkill.metadata.skillDescription,
|
|
1117
|
+
ownerUserId: 0,
|
|
1118
|
+
created: '',
|
|
1119
|
+
updated: '',
|
|
1120
|
+
deprecated: false,
|
|
1121
|
+
latestVersion: localSkill.metadata.skillVersion,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const lookupName = normalizeText(localSkill.metadata?.skillName) ?? localSkill.defaults.name;
|
|
1125
|
+
return resolveUploadSkillByName(client, lookupName);
|
|
1126
|
+
}
|
|
1127
|
+
async function resolveUploadSkillByName(client, skillName) {
|
|
1128
|
+
const normalizedSkillName = normalizeText(skillName)?.toLowerCase();
|
|
1129
|
+
if (!normalizedSkillName) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
const skills = await client.get('/api/skills');
|
|
1133
|
+
const exactNameMatches = skills.filter((skill) => normalizeText(skill.name)?.toLowerCase() === normalizedSkillName);
|
|
1134
|
+
if (exactNameMatches.length === 0) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
const currentUser = await client.get('/api/auth/me');
|
|
1138
|
+
const authorizedMatches = currentUser.isAdmin
|
|
1139
|
+
? exactNameMatches
|
|
1140
|
+
: exactNameMatches.filter((skill) => skill.ownerUserId === currentUser.storedUserId);
|
|
1141
|
+
const authorizedMatch = authorizedMatches[0];
|
|
1142
|
+
if (authorizedMatches.length === 1 && authorizedMatch) {
|
|
1143
|
+
return authorizedMatch;
|
|
1144
|
+
}
|
|
1145
|
+
if (authorizedMatches.length > 1) {
|
|
1146
|
+
throw new Error(`Skill name '${skillName}' matches multiple skills you can update: ${formatSkillMatches(authorizedMatches)}. Use --skill-id instead.`);
|
|
1147
|
+
}
|
|
1148
|
+
if (exactNameMatches.length === 1) {
|
|
1149
|
+
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.`);
|
|
1150
|
+
}
|
|
1151
|
+
throw new Error(`Skill name '${skillName}' matches multiple existing skills: ${formatSkillMatches(exactNameMatches)}. Use --skill-id instead.`);
|
|
1152
|
+
}
|
|
740
1153
|
function resolveSkillReference(skills, reference) {
|
|
741
1154
|
const normalizedReference = normalizeText(reference)?.toLowerCase();
|
|
742
1155
|
if (!normalizedReference) {
|
|
@@ -805,11 +1218,15 @@ function resolveInstallTargets(agents, scope, installName) {
|
|
|
805
1218
|
}
|
|
806
1219
|
return [...targets.values()];
|
|
807
1220
|
}
|
|
808
|
-
function
|
|
809
|
-
const normalized =
|
|
1221
|
+
function createNormalizedSkillDirectoryName(name) {
|
|
1222
|
+
const normalized = name
|
|
810
1223
|
.toLowerCase()
|
|
811
1224
|
.replace(/[^a-z0-9]+/g, '-')
|
|
812
1225
|
.replace(/^-+|-+$/g, '');
|
|
1226
|
+
return normalized;
|
|
1227
|
+
}
|
|
1228
|
+
function createInstalledSkillDirectoryName(skill) {
|
|
1229
|
+
const normalized = createNormalizedSkillDirectoryName(skill.name);
|
|
813
1230
|
return normalized.length > 0 ? normalized : `skill-${skill.id}`;
|
|
814
1231
|
}
|
|
815
1232
|
async function login(baseUrl) {
|
|
@@ -847,6 +1264,7 @@ async function login(baseUrl) {
|
|
|
847
1264
|
console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
|
|
848
1265
|
}
|
|
849
1266
|
async function parseOptions(args) {
|
|
1267
|
+
const booleanFlags = new Set(['dry-run']);
|
|
850
1268
|
const flags = {};
|
|
851
1269
|
const positionals = [];
|
|
852
1270
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -858,6 +1276,10 @@ async function parseOptions(args) {
|
|
|
858
1276
|
const key = current.slice(2);
|
|
859
1277
|
const value = args[index + 1];
|
|
860
1278
|
if (!value || value.startsWith('--')) {
|
|
1279
|
+
if (booleanFlags.has(key)) {
|
|
1280
|
+
flags[key] = 'true';
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
861
1283
|
throw new Error(`Missing value for ${current}`);
|
|
862
1284
|
}
|
|
863
1285
|
flags[key] = value;
|
|
@@ -867,12 +1289,21 @@ async function parseOptions(args) {
|
|
|
867
1289
|
positionals.push(current);
|
|
868
1290
|
}
|
|
869
1291
|
}
|
|
1292
|
+
const tokenFromFlag = normalizeText(flags.token);
|
|
1293
|
+
const tokenFromEnvironment = normalizeText(process.env.SKILLSHARE_TOKEN);
|
|
870
1294
|
return {
|
|
871
|
-
token:
|
|
1295
|
+
token: tokenFromFlag ?? tokenFromEnvironment ?? undefined,
|
|
872
1296
|
flags,
|
|
873
1297
|
positionals,
|
|
874
1298
|
};
|
|
875
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
|
+
}
|
|
876
1307
|
async function resolveActiveBaseUrl() {
|
|
877
1308
|
const session = await loadAnySession();
|
|
878
1309
|
if (session?.baseUrl) {
|
|
@@ -945,6 +1376,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
945
1376
|
});
|
|
946
1377
|
}
|
|
947
1378
|
return {
|
|
1379
|
+
baseUrl: resolvedBaseUrl,
|
|
948
1380
|
async get(path) {
|
|
949
1381
|
const response = await sendAuthorized(path);
|
|
950
1382
|
return parseJson(response);
|
|
@@ -1159,14 +1591,29 @@ async function waitForLoginCallback() {
|
|
|
1159
1591
|
},
|
|
1160
1592
|
};
|
|
1161
1593
|
}
|
|
1162
|
-
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) {
|
|
1163
1612
|
const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
|
|
1164
1613
|
try {
|
|
1165
1614
|
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
throw new Error(`Invalid skillId in ${SKILL_METADATA_FILE_NAME}.`);
|
|
1169
|
-
}
|
|
1615
|
+
const parsedSkillId = Number(parsed.skillId);
|
|
1616
|
+
const skillId = Number.isInteger(parsedSkillId) && parsedSkillId > 0 ? parsedSkillId : undefined;
|
|
1170
1617
|
return {
|
|
1171
1618
|
skillId,
|
|
1172
1619
|
skillName: typeof parsed.skillName === 'string' ? parsed.skillName : '',
|
|
@@ -1181,9 +1628,102 @@ async function readLocalSkillMetadata(directory) {
|
|
|
1181
1628
|
throw new Error(`Failed to read ${metadataPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1182
1629
|
}
|
|
1183
1630
|
}
|
|
1184
|
-
async function
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
+
}
|
|
1187
1727
|
}
|
|
1188
1728
|
async function collectFilePlans(root, current = root) {
|
|
1189
1729
|
const entries = await readdir(current, { withFileTypes: true });
|