@quadient/skillshare 1.1.0 → 1.3.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 +140 -101
- package/dist/index.js +655 -51
- package/package.json +5 -5
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,165 @@ 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.
|
|
27
28
|
|
|
28
29
|
## Current TUI Flow
|
|
29
30
|
|
|
30
31
|
```mermaid
|
|
31
32
|
flowchart TD
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
+
|
|
159
|
+
Publish an authenticated static page. The upload replaces the page's complete
|
|
160
|
+
content, and the directory must contain `index.html` at its root. By default the
|
|
161
|
+
directory name is used as the page slug:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx @quadient/skillshare pages upload ./release-notes
|
|
165
|
+
npx @quadient/skillshare pages upload ./site --slug release-notes
|
|
166
|
+
npx @quadient/skillshare pages list
|
|
167
|
+
npx @quadient/skillshare pages show release-notes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The page is then available at
|
|
171
|
+
`https://skillshare.inspireclouduat.net/p/release-notes/` after SSO sign-in.
|
|
172
|
+
|
|
134
173
|
## Package Distribution
|
|
135
174
|
|
|
136
175
|
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,13 @@ 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]
|
|
81
|
+
pages list
|
|
82
|
+
pages show <slug>
|
|
83
|
+
pages upload <directory> [--slug <slug>]
|
|
78
84
|
|
|
79
85
|
Options:
|
|
80
|
-
--token <token> Bearer token for API requests
|
|
86
|
+
--token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
|
|
81
87
|
`;
|
|
82
88
|
async function main() {
|
|
83
89
|
const args = process.argv.slice(2);
|
|
@@ -104,8 +110,12 @@ async function main() {
|
|
|
104
110
|
case 'list':
|
|
105
111
|
case 'show':
|
|
106
112
|
case 'upload':
|
|
107
|
-
case 'install':
|
|
108
|
-
|
|
113
|
+
case 'install':
|
|
114
|
+
case 'update':
|
|
115
|
+
case 'page':
|
|
116
|
+
case 'pages': {
|
|
117
|
+
const activeBaseUrl = await resolveActiveBaseUrl();
|
|
118
|
+
const client = await createClient(activeBaseUrl, options.token);
|
|
109
119
|
switch (command) {
|
|
110
120
|
case 'whoami': {
|
|
111
121
|
const user = await client.get('/api/auth/me');
|
|
@@ -175,14 +185,27 @@ async function main() {
|
|
|
175
185
|
const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
|
|
176
186
|
const skill = await client.get(`/api/skills/${detail.skillId}`);
|
|
177
187
|
await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
|
|
178
|
-
|
|
179
|
-
skillId: detail.skillId,
|
|
188
|
+
const installProgress = createInstallProgressReporter({
|
|
180
189
|
skillName: skill.name,
|
|
181
|
-
skillDescription: skill.description,
|
|
182
190
|
version: detail.version,
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
totalTargets: 1,
|
|
192
|
+
...calculateInstallTotals(detail.files, 1),
|
|
185
193
|
});
|
|
194
|
+
try {
|
|
195
|
+
await writeSkillVersionToDirectory(client, {
|
|
196
|
+
skillId: detail.skillId,
|
|
197
|
+
skillName: skill.name,
|
|
198
|
+
skillDescription: skill.description,
|
|
199
|
+
version: detail.version,
|
|
200
|
+
files: detail.files,
|
|
201
|
+
targetRoot: target,
|
|
202
|
+
}, installProgress);
|
|
203
|
+
installProgress.stop();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
installProgress.clear();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
186
209
|
console.log(`Installed skillVersion ${parsedSkillVersionId} (${detail.skillId}@${detail.version}) to ${target}`);
|
|
187
210
|
return;
|
|
188
211
|
}
|
|
@@ -204,17 +227,77 @@ async function main() {
|
|
|
204
227
|
}
|
|
205
228
|
const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
|
|
206
229
|
await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
|
|
207
|
-
|
|
208
|
-
skillId: skill.id,
|
|
230
|
+
const installProgress = createInstallProgressReporter({
|
|
209
231
|
skillName: skill.name,
|
|
210
|
-
skillDescription: skill.description,
|
|
211
232
|
version: effectiveVersion,
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
totalTargets: 1,
|
|
234
|
+
...calculateInstallTotals(detail.files, 1),
|
|
214
235
|
});
|
|
236
|
+
try {
|
|
237
|
+
await writeSkillVersionToDirectory(client, {
|
|
238
|
+
skillId: skill.id,
|
|
239
|
+
skillName: skill.name,
|
|
240
|
+
skillDescription: skill.description,
|
|
241
|
+
version: effectiveVersion,
|
|
242
|
+
files: detail.files,
|
|
243
|
+
targetRoot: target,
|
|
244
|
+
}, installProgress);
|
|
245
|
+
installProgress.stop();
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
installProgress.clear();
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
215
251
|
console.log(`Installed ${skill.name}@${effectiveVersion} to ${target}`);
|
|
216
252
|
return;
|
|
217
253
|
}
|
|
254
|
+
case 'update': {
|
|
255
|
+
await updateInstalledSkills(client, {
|
|
256
|
+
skillRef: options.positionals[0],
|
|
257
|
+
target: options.flags.target,
|
|
258
|
+
dryRun: isFlagEnabled(options.flags, 'dry-run'),
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
case 'page':
|
|
263
|
+
case 'pages': {
|
|
264
|
+
const action = options.positionals[0] ?? 'list';
|
|
265
|
+
if (action === 'list') {
|
|
266
|
+
const pages = await client.get('/api/pages');
|
|
267
|
+
for (const page of pages) {
|
|
268
|
+
console.log(`${page.slug}\t${page.fileCount}\t${page.ownerDisplayName}`);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (action === 'show') {
|
|
273
|
+
const slug = options.positionals[1];
|
|
274
|
+
if (!slug)
|
|
275
|
+
throw new Error('pages show requires <slug>');
|
|
276
|
+
console.log(JSON.stringify(await client.get(`/api/pages/${encodeURIComponent(slug)}`), null, 2));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (action === 'upload') {
|
|
280
|
+
const directory = options.positionals[1];
|
|
281
|
+
if (!directory)
|
|
282
|
+
throw new Error('pages upload requires <directory>');
|
|
283
|
+
const resolvedDirectory = resolve(directory);
|
|
284
|
+
const slug = normalizePageSlug(options.flags.slug ?? basename(resolvedDirectory));
|
|
285
|
+
const pageFiles = await loadLocalPageFiles(resolvedDirectory);
|
|
286
|
+
const uploadStatus = spinner({ indicator: 'timer' });
|
|
287
|
+
uploadStatus.start(`Uploading ${pageFiles.files.length} files (${formatByteSize(pageFiles.totalBytes)}) to /p/${slug}/`);
|
|
288
|
+
try {
|
|
289
|
+
const detail = await client.replacePageContent(slug, pageFiles.files);
|
|
290
|
+
uploadStatus.stop(`Published /p/${detail.slug}/ with ${detail.files.length} files`);
|
|
291
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
uploadStatus.clear();
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`Unknown pages command '${action}'. Use list, show, or upload.`);
|
|
300
|
+
}
|
|
218
301
|
}
|
|
219
302
|
return;
|
|
220
303
|
}
|
|
@@ -227,8 +310,19 @@ async function runInteractive() {
|
|
|
227
310
|
const baseUrl = await resolveActiveBaseUrl();
|
|
228
311
|
await ensureInteractiveLogin(baseUrl);
|
|
229
312
|
const client = await createClient(baseUrl);
|
|
313
|
+
const pendingInstalledUpdates = await collectInstalledSkillUpdateCandidates(client);
|
|
314
|
+
if (pendingInstalledUpdates.length > 0) {
|
|
315
|
+
const shouldUpdateInstalled = await promptConfirm(pendingInstalledUpdates.length === 1
|
|
316
|
+
? `There is 1 installed skill with a newer version available. Update it now?`
|
|
317
|
+
: `There are ${pendingInstalledUpdates.length} installed skills with newer versions available. Update all of them now?`, true);
|
|
318
|
+
if (shouldUpdateInstalled) {
|
|
319
|
+
await updateInstalledSkills(client, { candidates: pendingInstalledUpdates });
|
|
320
|
+
outro('Done.');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
230
324
|
if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
|
|
231
|
-
const localMetadata = await readLocalSkillMetadata(process.cwd());
|
|
325
|
+
const localMetadata = await readLocalSkillMetadata(process.cwd(), client.baseUrl);
|
|
232
326
|
const shouldUploadCurrent = await promptConfirm(localMetadata
|
|
233
327
|
? `Current directory contains ${SKILL_FILE_NAME}. Upload a new version of ${localMetadata.skillName || `skill ${localMetadata.skillId}`} now?`
|
|
234
328
|
: `Current directory contains ${SKILL_FILE_NAME}. Upload it now?`, true);
|
|
@@ -242,6 +336,7 @@ async function runInteractive() {
|
|
|
242
336
|
message: 'What do you want to do?',
|
|
243
337
|
options: [
|
|
244
338
|
{ value: 'install', label: 'Install a skill', hint: 'Browse registry and install to agents' },
|
|
339
|
+
{ value: 'update', label: 'Update installed skills', hint: 'Refresh recorded installs to the latest registry versions' },
|
|
245
340
|
{ value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
|
|
246
341
|
],
|
|
247
342
|
});
|
|
@@ -252,6 +347,9 @@ async function runInteractive() {
|
|
|
252
347
|
if (action === 'install') {
|
|
253
348
|
await runInteractiveInstall(client);
|
|
254
349
|
}
|
|
350
|
+
else if (action === 'update') {
|
|
351
|
+
await updateInstalledSkills(client);
|
|
352
|
+
}
|
|
255
353
|
else {
|
|
256
354
|
await runInteractiveUpload(client);
|
|
257
355
|
}
|
|
@@ -296,22 +394,35 @@ async function runInteractiveInstall(client, options) {
|
|
|
296
394
|
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
297
395
|
const installName = createInstalledSkillDirectoryName(skill);
|
|
298
396
|
const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
397
|
+
const installProgress = createInstallProgressReporter({
|
|
398
|
+
skillName: skill.name,
|
|
399
|
+
version: selectedVersion,
|
|
400
|
+
totalTargets: installTargets.length,
|
|
401
|
+
...calculateInstallTotals(versionDetail.files, installTargets.length),
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
for (const installTarget of installTargets) {
|
|
405
|
+
await writeSkillVersionToDirectory(client, {
|
|
406
|
+
skillId: skill.id,
|
|
407
|
+
skillName: skill.name,
|
|
408
|
+
skillDescription: skill.description,
|
|
409
|
+
version: selectedVersion,
|
|
410
|
+
files: versionDetail.files,
|
|
411
|
+
targetRoot: installTarget.targetRoot,
|
|
412
|
+
}, installProgress);
|
|
413
|
+
}
|
|
414
|
+
installProgress.stop();
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
installProgress.clear();
|
|
418
|
+
throw error;
|
|
308
419
|
}
|
|
309
420
|
console.log(`Installed ${skill.name}@${selectedVersion} to ${installTargets.length} location(s) for ${selectedAgents.length} agent(s) in ${scope} scope.`);
|
|
310
421
|
}
|
|
311
422
|
async function runInteractiveUpload(client, initialDirectory) {
|
|
312
423
|
const directory = initialDirectory
|
|
313
424
|
?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
|
|
314
|
-
const localSkill = await loadLocalSkillContext(directory);
|
|
425
|
+
const localSkill = await loadLocalSkillContext(directory, client.baseUrl);
|
|
315
426
|
const existingSkill = await resolveExistingUploadSkill(client, localSkill);
|
|
316
427
|
if (!existingSkill) {
|
|
317
428
|
const version = await promptText('Initial version', '0.1.0');
|
|
@@ -327,14 +438,27 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
327
438
|
if (shouldDownloadLatest) {
|
|
328
439
|
const latestDetail = await client.get(`/api/skills/${skill.id}/versions/${latestVersion}`);
|
|
329
440
|
await client.post(`/api/skills/${skill.id}/versions/${latestVersion}/downloads`);
|
|
330
|
-
|
|
331
|
-
skillId: skill.id,
|
|
441
|
+
const installProgress = createInstallProgressReporter({
|
|
332
442
|
skillName: skill.name,
|
|
333
|
-
skillDescription: skill.description,
|
|
334
443
|
version: latestVersion,
|
|
335
|
-
|
|
336
|
-
|
|
444
|
+
totalTargets: 1,
|
|
445
|
+
...calculateInstallTotals(latestDetail.files, 1),
|
|
337
446
|
});
|
|
447
|
+
try {
|
|
448
|
+
await writeSkillVersionToDirectory(client, {
|
|
449
|
+
skillId: skill.id,
|
|
450
|
+
skillName: skill.name,
|
|
451
|
+
skillDescription: skill.description,
|
|
452
|
+
version: latestVersion,
|
|
453
|
+
files: latestDetail.files,
|
|
454
|
+
targetRoot: localSkill.directory,
|
|
455
|
+
}, installProgress);
|
|
456
|
+
installProgress.stop();
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
installProgress.clear();
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
338
462
|
console.log(`Downloaded ${skill.name}@${latestVersion} to ${localSkill.directory}`);
|
|
339
463
|
return;
|
|
340
464
|
}
|
|
@@ -346,6 +470,129 @@ async function runInteractiveUpload(client, initialDirectory) {
|
|
|
346
470
|
});
|
|
347
471
|
console.log(`Published ${published.detail.name}@${published.response.version}`);
|
|
348
472
|
}
|
|
473
|
+
async function updateInstalledSkills(client, options) {
|
|
474
|
+
const skillRef = normalizeText(options?.skillRef);
|
|
475
|
+
const targetFilter = normalizeText(options?.target);
|
|
476
|
+
const normalizedTargetFilter = targetFilter ? resolve(targetFilter) : null;
|
|
477
|
+
const dryRun = options?.dryRun ?? false;
|
|
478
|
+
let installedRecords = (await readInstalledSkillRecords())
|
|
479
|
+
.filter((record) => record.baseUrl === client.baseUrl);
|
|
480
|
+
if (skillRef) {
|
|
481
|
+
const normalizedSkillRef = skillRef.toLowerCase();
|
|
482
|
+
installedRecords = installedRecords.filter((record) => ((record.skillId != null && String(record.skillId) === normalizedSkillRef) ||
|
|
483
|
+
normalizeText(record.skillName)?.toLowerCase() === normalizedSkillRef ||
|
|
484
|
+
createNormalizedSkillDirectoryName(record.skillName) === normalizedSkillRef));
|
|
485
|
+
}
|
|
486
|
+
if (normalizedTargetFilter) {
|
|
487
|
+
installedRecords = installedRecords.filter((record) => resolve(record.targetRoot) === normalizedTargetFilter);
|
|
488
|
+
}
|
|
489
|
+
if (installedRecords.length === 0) {
|
|
490
|
+
const selectionDescription = formatUpdateSelectionDescription({
|
|
491
|
+
skillRef: skillRef ?? undefined,
|
|
492
|
+
target: targetFilter ?? undefined,
|
|
493
|
+
dryRun,
|
|
494
|
+
});
|
|
495
|
+
console.log(`No installed skills recorded for ${client.baseUrl}${selectionDescription}.`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const candidates = options?.candidates
|
|
499
|
+
?? await collectInstalledSkillUpdateCandidates(client, {
|
|
500
|
+
records: installedRecords,
|
|
501
|
+
onSkipped(message) {
|
|
502
|
+
console.log(message);
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
const skippedCount = installedRecords.length - candidates.length;
|
|
506
|
+
if (candidates.length === 0) {
|
|
507
|
+
if (dryRun) {
|
|
508
|
+
console.log(`Dry run finished. Would update 0, skipped ${skippedCount}.`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
console.log(`Update finished. Updated 0, skipped ${skippedCount}.`);
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
let updatedCount = 0;
|
|
516
|
+
let plannedCount = 0;
|
|
517
|
+
for (const candidate of candidates) {
|
|
518
|
+
const { record, detail, latestVersion } = candidate;
|
|
519
|
+
if (dryRun) {
|
|
520
|
+
plannedCount += 1;
|
|
521
|
+
console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const versionDetail = await client.get(`/api/skills/${record.skillId}/versions/${latestVersion}`);
|
|
525
|
+
await client.post(`/api/skills/${record.skillId}/versions/${latestVersion}/downloads`);
|
|
526
|
+
const installProgress = createInstallProgressReporter({
|
|
527
|
+
skillName: detail.name,
|
|
528
|
+
version: latestVersion,
|
|
529
|
+
totalTargets: 1,
|
|
530
|
+
...calculateInstallTotals(versionDetail.files, 1),
|
|
531
|
+
});
|
|
532
|
+
try {
|
|
533
|
+
await writeSkillVersionToDirectory(client, {
|
|
534
|
+
skillId: detail.id,
|
|
535
|
+
skillName: detail.name,
|
|
536
|
+
skillDescription: detail.description,
|
|
537
|
+
version: latestVersion,
|
|
538
|
+
files: versionDetail.files,
|
|
539
|
+
targetRoot: record.targetRoot,
|
|
540
|
+
}, installProgress);
|
|
541
|
+
installProgress.stop();
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
installProgress.clear();
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
updatedCount += 1;
|
|
548
|
+
console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
549
|
+
}
|
|
550
|
+
if (dryRun) {
|
|
551
|
+
console.log(`Dry run finished. Would update ${plannedCount}, skipped ${skippedCount}.`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
console.log(`Update finished. Updated ${updatedCount}, skipped ${skippedCount}.`);
|
|
555
|
+
}
|
|
556
|
+
async function collectInstalledSkillUpdateCandidates(client, options) {
|
|
557
|
+
const records = options?.records
|
|
558
|
+
?? (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
|
|
559
|
+
const candidates = [];
|
|
560
|
+
for (const record of records) {
|
|
561
|
+
if (record.skillId == null) {
|
|
562
|
+
options?.onSkipped?.(`Skipping ${record.skillName}: installed record in ${record.targetRoot} does not contain a skill id.`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const detail = await client.get(`/api/skills/${record.skillId}`);
|
|
566
|
+
const latestVersion = normalizeText(detail.latestVersion);
|
|
567
|
+
if (!latestVersion) {
|
|
568
|
+
options?.onSkipped?.(`Skipping ${record.skillName}: no latest version is published.`);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (latestVersion === normalizeText(record.skillVersion)) {
|
|
572
|
+
options?.onSkipped?.(`Skipping ${record.skillName}: ${latestVersion} is already installed in ${record.targetRoot}.`);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
candidates.push({
|
|
576
|
+
record,
|
|
577
|
+
detail,
|
|
578
|
+
latestVersion,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return candidates;
|
|
582
|
+
}
|
|
583
|
+
function formatUpdateSelectionDescription(options) {
|
|
584
|
+
const parts = [];
|
|
585
|
+
if (options.skillRef) {
|
|
586
|
+
parts.push(`skill '${options.skillRef}'`);
|
|
587
|
+
}
|
|
588
|
+
if (options.target) {
|
|
589
|
+
parts.push(`target '${options.target}'`);
|
|
590
|
+
}
|
|
591
|
+
if (parts.length === 0) {
|
|
592
|
+
return '';
|
|
593
|
+
}
|
|
594
|
+
return ` matching ${parts.join(' and ')}`;
|
|
595
|
+
}
|
|
349
596
|
async function promptForSkill(skills) {
|
|
350
597
|
let query = '';
|
|
351
598
|
while (true) {
|
|
@@ -493,7 +740,7 @@ async function promptConfirm(message, initialValue = false) {
|
|
|
493
740
|
}
|
|
494
741
|
return value;
|
|
495
742
|
}
|
|
496
|
-
async function loadLocalSkillContext(directory) {
|
|
743
|
+
async function loadLocalSkillContext(directory, baseUrl) {
|
|
497
744
|
const resolvedDirectory = resolve(directory);
|
|
498
745
|
const scanStatus = spinner({ indicator: 'timer' });
|
|
499
746
|
scanStatus.start(`Scanning ${resolvedDirectory}`);
|
|
@@ -529,10 +776,24 @@ async function loadLocalSkillContext(directory) {
|
|
|
529
776
|
directory: resolvedDirectory,
|
|
530
777
|
files,
|
|
531
778
|
totalBytes,
|
|
532
|
-
metadata: await readLocalSkillMetadata(resolvedDirectory),
|
|
779
|
+
metadata: await readLocalSkillMetadata(resolvedDirectory, baseUrl),
|
|
533
780
|
defaults: deriveSkillDefaults(files, resolvedDirectory),
|
|
534
781
|
};
|
|
535
782
|
}
|
|
783
|
+
async function loadLocalPageFiles(directory) {
|
|
784
|
+
const filePlans = await collectFilePlans(directory, directory, { includeSkillMetadata: true });
|
|
785
|
+
if (filePlans.length === 0) {
|
|
786
|
+
throw new Error('The page directory is empty.');
|
|
787
|
+
}
|
|
788
|
+
if (!filePlans.some((file) => file.relativePath === 'index.html')) {
|
|
789
|
+
throw new Error('The page directory must contain index.html at its root.');
|
|
790
|
+
}
|
|
791
|
+
const totalBytes = filePlans.reduce((sum, file) => sum + file.size, 0);
|
|
792
|
+
return {
|
|
793
|
+
files: await collectFiles(filePlans),
|
|
794
|
+
totalBytes,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
536
797
|
async function createSkillFromDirectory(client, localSkill, input) {
|
|
537
798
|
const name = normalizeText(input.name) ?? localSkill.defaults.name;
|
|
538
799
|
const description = normalizeText(input.description) ?? localSkill.defaults.description;
|
|
@@ -547,7 +808,7 @@ async function createSkillFromDirectory(client, localSkill, input) {
|
|
|
547
808
|
});
|
|
548
809
|
uploadStatus.message(`Fetching saved metadata for skill ${response.id}`);
|
|
549
810
|
const detail = await client.get(`/api/skills/${response.id}`);
|
|
550
|
-
await
|
|
811
|
+
await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, detail.latestVersion ?? input.version));
|
|
551
812
|
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
552
813
|
return { response, detail };
|
|
553
814
|
}
|
|
@@ -567,7 +828,7 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
567
828
|
});
|
|
568
829
|
uploadStatus.message(`Fetching saved metadata for skill ${input.skillId}`);
|
|
569
830
|
const detail = await client.get(`/api/skills/${input.skillId}`);
|
|
570
|
-
await
|
|
831
|
+
await upsertUploadedSkillMetadata(client.baseUrl, localSkill.directory, createLocalSkillMetadata(detail, response.version));
|
|
571
832
|
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
572
833
|
return { response, detail };
|
|
573
834
|
}
|
|
@@ -576,19 +837,178 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
576
837
|
throw error;
|
|
577
838
|
}
|
|
578
839
|
}
|
|
579
|
-
async function writeSkillVersionToDirectory(client, input) {
|
|
840
|
+
async function writeSkillVersionToDirectory(client, input, installProgress) {
|
|
841
|
+
installProgress?.onTargetStart(input.targetRoot);
|
|
580
842
|
for (const file of input.files) {
|
|
843
|
+
installProgress?.onFileStart(input.targetRoot, file.nameWithPath, file.length);
|
|
581
844
|
const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${file.nameWithPath}`);
|
|
582
845
|
const destination = join(input.targetRoot, file.nameWithPath);
|
|
583
846
|
await mkdir(dirname(destination), { recursive: true });
|
|
584
|
-
await
|
|
847
|
+
await downloadResponseToFile(response, destination, (bytes) => {
|
|
848
|
+
installProgress?.onBytesTransferred(bytes);
|
|
849
|
+
});
|
|
850
|
+
installProgress?.onFileComplete();
|
|
585
851
|
}
|
|
586
|
-
await
|
|
852
|
+
await upsertInstalledSkillRecord(client.baseUrl, input.targetRoot, {
|
|
587
853
|
skillId: input.skillId,
|
|
588
854
|
skillName: input.skillName,
|
|
589
855
|
skillDescription: input.skillDescription,
|
|
590
856
|
skillVersion: input.version,
|
|
591
857
|
});
|
|
858
|
+
installProgress?.onTargetComplete();
|
|
859
|
+
}
|
|
860
|
+
async function downloadResponseToFile(response, destination, onBytesTransferred) {
|
|
861
|
+
const body = response.body;
|
|
862
|
+
if (!body) {
|
|
863
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
864
|
+
await writeFile(destination, buffer);
|
|
865
|
+
onBytesTransferred?.(buffer.length);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const fileHandle = await open(destination, 'w');
|
|
869
|
+
let failed = true;
|
|
870
|
+
try {
|
|
871
|
+
const reader = body.getReader();
|
|
872
|
+
while (true) {
|
|
873
|
+
const { done, value } = await reader.read();
|
|
874
|
+
if (done) {
|
|
875
|
+
failed = false;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
if (!value || value.length === 0) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
await fileHandle.writeFile(value);
|
|
882
|
+
onBytesTransferred?.(value.length);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
await fileHandle.close();
|
|
887
|
+
if (failed) {
|
|
888
|
+
await rm(destination, { force: true });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function createInstallProgressReporter(input) {
|
|
893
|
+
const installStatus = spinner({ indicator: 'timer' });
|
|
894
|
+
const state = {
|
|
895
|
+
completedFiles: 0,
|
|
896
|
+
completedTargets: 0,
|
|
897
|
+
currentFileBytes: 0,
|
|
898
|
+
currentFileExpectedBytes: 0,
|
|
899
|
+
currentFilePath: null,
|
|
900
|
+
currentTargetRoot: null,
|
|
901
|
+
transferredBytes: 0,
|
|
902
|
+
};
|
|
903
|
+
let lastRenderAt = 0;
|
|
904
|
+
let lastRenderedBytes = 0;
|
|
905
|
+
installStatus.start(formatInstallProgressMessage(input, state));
|
|
906
|
+
function render(force = false) {
|
|
907
|
+
const now = Date.now();
|
|
908
|
+
const bytesDelta = state.transferredBytes - lastRenderedBytes;
|
|
909
|
+
if (!force && now - lastRenderAt < 100 && bytesDelta < 64 * 1024) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
installStatus.message(formatInstallProgressMessage(input, state));
|
|
913
|
+
lastRenderAt = now;
|
|
914
|
+
lastRenderedBytes = state.transferredBytes;
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
onTargetStart(targetRoot) {
|
|
918
|
+
state.currentTargetRoot = targetRoot;
|
|
919
|
+
render(true);
|
|
920
|
+
},
|
|
921
|
+
onFileStart(targetRoot, relativePath, expectedBytes) {
|
|
922
|
+
state.currentTargetRoot = targetRoot;
|
|
923
|
+
state.currentFilePath = relativePath;
|
|
924
|
+
state.currentFileExpectedBytes = Math.max(expectedBytes, 0);
|
|
925
|
+
state.currentFileBytes = 0;
|
|
926
|
+
render(true);
|
|
927
|
+
},
|
|
928
|
+
onBytesTransferred(bytes) {
|
|
929
|
+
state.transferredBytes += bytes;
|
|
930
|
+
state.currentFileBytes += bytes;
|
|
931
|
+
render();
|
|
932
|
+
},
|
|
933
|
+
onFileComplete() {
|
|
934
|
+
state.completedFiles += 1;
|
|
935
|
+
if (state.currentFileExpectedBytes > 0) {
|
|
936
|
+
state.currentFileBytes = state.currentFileExpectedBytes;
|
|
937
|
+
}
|
|
938
|
+
render(true);
|
|
939
|
+
},
|
|
940
|
+
onTargetComplete() {
|
|
941
|
+
state.completedTargets += 1;
|
|
942
|
+
render(true);
|
|
943
|
+
},
|
|
944
|
+
stop() {
|
|
945
|
+
installStatus.stop(formatInstallCompletedMessage(input, state));
|
|
946
|
+
},
|
|
947
|
+
clear() {
|
|
948
|
+
installStatus.clear();
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function calculateInstallTotals(files, targetCount) {
|
|
953
|
+
const totalFiles = files.length * targetCount;
|
|
954
|
+
const totalBytes = files.reduce((sum, file) => sum + Math.max(file.length, 0), 0) * targetCount;
|
|
955
|
+
return { totalFiles, totalBytes };
|
|
956
|
+
}
|
|
957
|
+
function formatInstallProgressMessage(input, state) {
|
|
958
|
+
const parts = [
|
|
959
|
+
`Installing ${input.skillName}@${input.version}`,
|
|
960
|
+
`files ${state.completedFiles}/${input.totalFiles}`,
|
|
961
|
+
input.totalBytes > 0
|
|
962
|
+
? `bytes ${formatByteSize(Math.min(state.transferredBytes, input.totalBytes))}/${formatByteSize(input.totalBytes)}`
|
|
963
|
+
: `bytes ${formatByteSize(state.transferredBytes)}`,
|
|
964
|
+
];
|
|
965
|
+
if (input.totalTargets > 1) {
|
|
966
|
+
parts.push(`targets ${state.completedTargets}/${input.totalTargets}`);
|
|
967
|
+
}
|
|
968
|
+
if (state.currentTargetRoot) {
|
|
969
|
+
parts.push(`target ${formatInstallTargetRoot(state.currentTargetRoot)}`);
|
|
970
|
+
}
|
|
971
|
+
if (state.currentFilePath) {
|
|
972
|
+
const fileProgress = state.currentFileExpectedBytes > 0
|
|
973
|
+
? ` (${formatByteSize(Math.min(state.currentFileBytes, state.currentFileExpectedBytes))}/${formatByteSize(state.currentFileExpectedBytes)})`
|
|
974
|
+
: state.currentFileBytes > 0
|
|
975
|
+
? ` (${formatByteSize(state.currentFileBytes)})`
|
|
976
|
+
: '';
|
|
977
|
+
parts.push(`${state.currentFilePath}${fileProgress}`);
|
|
978
|
+
}
|
|
979
|
+
return parts.join(' · ');
|
|
980
|
+
}
|
|
981
|
+
function formatInstallCompletedMessage(input, state) {
|
|
982
|
+
const parts = [
|
|
983
|
+
`Installed ${input.skillName}@${input.version}`,
|
|
984
|
+
`files ${state.completedFiles}/${input.totalFiles}`,
|
|
985
|
+
input.totalBytes > 0
|
|
986
|
+
? `bytes ${formatByteSize(Math.min(state.transferredBytes, input.totalBytes))}/${formatByteSize(input.totalBytes)}`
|
|
987
|
+
: `bytes ${formatByteSize(state.transferredBytes)}`,
|
|
988
|
+
];
|
|
989
|
+
if (input.totalTargets > 1) {
|
|
990
|
+
parts.push(`targets ${state.completedTargets}/${input.totalTargets}`);
|
|
991
|
+
}
|
|
992
|
+
return parts.join(' · ');
|
|
993
|
+
}
|
|
994
|
+
function formatInstallTargetRoot(targetRoot) {
|
|
995
|
+
const cwd = process.cwd();
|
|
996
|
+
if (targetRoot === cwd) {
|
|
997
|
+
return '.';
|
|
998
|
+
}
|
|
999
|
+
const relativeToCwd = relative(cwd, targetRoot);
|
|
1000
|
+
if (relativeToCwd && relativeToCwd !== '' && !relativeToCwd.startsWith('..')) {
|
|
1001
|
+
return relativeToCwd.startsWith('.') ? relativeToCwd : `./${relativeToCwd}`;
|
|
1002
|
+
}
|
|
1003
|
+
const home = homedir();
|
|
1004
|
+
if (targetRoot === home) {
|
|
1005
|
+
return '~';
|
|
1006
|
+
}
|
|
1007
|
+
const relativeToHome = relative(home, targetRoot);
|
|
1008
|
+
if (relativeToHome && relativeToHome !== '' && !relativeToHome.startsWith('..')) {
|
|
1009
|
+
return `~/${relativeToHome}`;
|
|
1010
|
+
}
|
|
1011
|
+
return targetRoot;
|
|
592
1012
|
}
|
|
593
1013
|
function deriveSkillDefaults(files, directory) {
|
|
594
1014
|
const skillFile = files.find((file) => file.relativePath === SKILL_FILE_NAME);
|
|
@@ -733,6 +1153,13 @@ function normalizeText(value) {
|
|
|
733
1153
|
const trimmed = value?.trim();
|
|
734
1154
|
return trimmed ? trimmed : null;
|
|
735
1155
|
}
|
|
1156
|
+
function normalizePageSlug(value) {
|
|
1157
|
+
const normalized = value.trim().toLowerCase();
|
|
1158
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(normalized)) {
|
|
1159
|
+
throw new Error('Page slug must be 1-64 lowercase letters, numbers, or hyphens, and cannot start or end with a hyphen.');
|
|
1160
|
+
}
|
|
1161
|
+
return normalized;
|
|
1162
|
+
}
|
|
736
1163
|
async function resolveExistingUploadSkill(client, localSkill, options) {
|
|
737
1164
|
const explicitSkillId = normalizeText(options?.explicitSkillId);
|
|
738
1165
|
if (explicitSkillId) {
|
|
@@ -856,11 +1283,15 @@ function resolveInstallTargets(agents, scope, installName) {
|
|
|
856
1283
|
}
|
|
857
1284
|
return [...targets.values()];
|
|
858
1285
|
}
|
|
859
|
-
function
|
|
860
|
-
const normalized =
|
|
1286
|
+
function createNormalizedSkillDirectoryName(name) {
|
|
1287
|
+
const normalized = name
|
|
861
1288
|
.toLowerCase()
|
|
862
1289
|
.replace(/[^a-z0-9]+/g, '-')
|
|
863
1290
|
.replace(/^-+|-+$/g, '');
|
|
1291
|
+
return normalized;
|
|
1292
|
+
}
|
|
1293
|
+
function createInstalledSkillDirectoryName(skill) {
|
|
1294
|
+
const normalized = createNormalizedSkillDirectoryName(skill.name);
|
|
864
1295
|
return normalized.length > 0 ? normalized : `skill-${skill.id}`;
|
|
865
1296
|
}
|
|
866
1297
|
async function login(baseUrl) {
|
|
@@ -898,6 +1329,7 @@ async function login(baseUrl) {
|
|
|
898
1329
|
console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
|
|
899
1330
|
}
|
|
900
1331
|
async function parseOptions(args) {
|
|
1332
|
+
const booleanFlags = new Set(['dry-run']);
|
|
901
1333
|
const flags = {};
|
|
902
1334
|
const positionals = [];
|
|
903
1335
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -909,6 +1341,10 @@ async function parseOptions(args) {
|
|
|
909
1341
|
const key = current.slice(2);
|
|
910
1342
|
const value = args[index + 1];
|
|
911
1343
|
if (!value || value.startsWith('--')) {
|
|
1344
|
+
if (booleanFlags.has(key)) {
|
|
1345
|
+
flags[key] = 'true';
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
912
1348
|
throw new Error(`Missing value for ${current}`);
|
|
913
1349
|
}
|
|
914
1350
|
flags[key] = value;
|
|
@@ -918,12 +1354,21 @@ async function parseOptions(args) {
|
|
|
918
1354
|
positionals.push(current);
|
|
919
1355
|
}
|
|
920
1356
|
}
|
|
1357
|
+
const tokenFromFlag = normalizeText(flags.token);
|
|
1358
|
+
const tokenFromEnvironment = normalizeText(process.env.SKILLSHARE_TOKEN);
|
|
921
1359
|
return {
|
|
922
|
-
token:
|
|
1360
|
+
token: tokenFromFlag ?? tokenFromEnvironment ?? undefined,
|
|
923
1361
|
flags,
|
|
924
1362
|
positionals,
|
|
925
1363
|
};
|
|
926
1364
|
}
|
|
1365
|
+
function isFlagEnabled(flags, key) {
|
|
1366
|
+
const value = normalizeText(flags[key]);
|
|
1367
|
+
if (!value) {
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
return !['0', 'false', 'no', 'off'].includes(value.toLowerCase());
|
|
1371
|
+
}
|
|
927
1372
|
async function resolveActiveBaseUrl() {
|
|
928
1373
|
const session = await loadAnySession();
|
|
929
1374
|
if (session?.baseUrl) {
|
|
@@ -996,6 +1441,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
996
1441
|
});
|
|
997
1442
|
}
|
|
998
1443
|
return {
|
|
1444
|
+
baseUrl: resolvedBaseUrl,
|
|
999
1445
|
async get(path) {
|
|
1000
1446
|
const response = await sendAuthorized(path);
|
|
1001
1447
|
return parseJson(response);
|
|
@@ -1043,6 +1489,17 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1043
1489
|
});
|
|
1044
1490
|
return parseJson(response);
|
|
1045
1491
|
},
|
|
1492
|
+
async replacePageContent(slug, files) {
|
|
1493
|
+
const formData = new FormData();
|
|
1494
|
+
for (const file of files) {
|
|
1495
|
+
formData.append('files', new Blob([toBlobPart(file.content)], { type: file.contentType }), file.relativePath);
|
|
1496
|
+
}
|
|
1497
|
+
const response = await sendAuthorized(`/api/pages/${encodeURIComponent(slug)}/content`, {
|
|
1498
|
+
method: 'PUT',
|
|
1499
|
+
body: formData,
|
|
1500
|
+
});
|
|
1501
|
+
return parseJson(response);
|
|
1502
|
+
},
|
|
1046
1503
|
};
|
|
1047
1504
|
}
|
|
1048
1505
|
async function buildHeaders(getAccessToken, initialHeaders) {
|
|
@@ -1210,7 +1667,24 @@ async function waitForLoginCallback() {
|
|
|
1210
1667
|
},
|
|
1211
1668
|
};
|
|
1212
1669
|
}
|
|
1213
|
-
async function readLocalSkillMetadata(directory) {
|
|
1670
|
+
async function readLocalSkillMetadata(directory, baseUrl) {
|
|
1671
|
+
const resolvedDirectory = resolve(directory);
|
|
1672
|
+
const normalizedBaseUrl = normalizeText(baseUrl) ? normalizeBaseUrl(baseUrl) : null;
|
|
1673
|
+
const uploadedRecords = await readUploadedSkillRecords();
|
|
1674
|
+
const matchingRecord = uploadedRecords
|
|
1675
|
+
.filter((record) => record.directory === resolvedDirectory && (!normalizedBaseUrl || record.baseUrl === normalizedBaseUrl))
|
|
1676
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
1677
|
+
if (matchingRecord) {
|
|
1678
|
+
return {
|
|
1679
|
+
skillId: matchingRecord.skillId,
|
|
1680
|
+
skillName: matchingRecord.skillName,
|
|
1681
|
+
skillDescription: matchingRecord.skillDescription,
|
|
1682
|
+
skillVersion: matchingRecord.skillVersion,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
return readLegacyLocalSkillMetadata(resolvedDirectory);
|
|
1686
|
+
}
|
|
1687
|
+
async function readLegacyLocalSkillMetadata(directory) {
|
|
1214
1688
|
const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
|
|
1215
1689
|
try {
|
|
1216
1690
|
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
@@ -1230,17 +1704,110 @@ async function readLocalSkillMetadata(directory) {
|
|
|
1230
1704
|
throw new Error(`Failed to read ${metadataPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1231
1705
|
}
|
|
1232
1706
|
}
|
|
1233
|
-
async function
|
|
1234
|
-
|
|
1235
|
-
|
|
1707
|
+
async function upsertUploadedSkillMetadata(baseUrl, directory, metadata) {
|
|
1708
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
1709
|
+
const resolvedDirectory = resolve(directory);
|
|
1710
|
+
const records = (await readUploadedSkillRecords())
|
|
1711
|
+
.filter((record) => !(record.directory === resolvedDirectory && record.baseUrl === normalizedBaseUrl));
|
|
1712
|
+
records.unshift({
|
|
1713
|
+
...metadata,
|
|
1714
|
+
directory: resolvedDirectory,
|
|
1715
|
+
baseUrl: normalizedBaseUrl,
|
|
1716
|
+
updatedAt: new Date().toISOString(),
|
|
1717
|
+
});
|
|
1718
|
+
await writeStateFile(UPLOADED_STATE_PATH, records);
|
|
1719
|
+
await removeLegacyLocalSkillMetadata(resolvedDirectory);
|
|
1720
|
+
}
|
|
1721
|
+
async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
|
|
1722
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
1723
|
+
const resolvedTargetRoot = resolve(targetRoot);
|
|
1724
|
+
const records = (await readInstalledSkillRecords())
|
|
1725
|
+
.filter((record) => !(record.targetRoot === resolvedTargetRoot && record.baseUrl === normalizedBaseUrl));
|
|
1726
|
+
records.unshift({
|
|
1727
|
+
...metadata,
|
|
1728
|
+
targetRoot: resolvedTargetRoot,
|
|
1729
|
+
baseUrl: normalizedBaseUrl,
|
|
1730
|
+
updatedAt: new Date().toISOString(),
|
|
1731
|
+
});
|
|
1732
|
+
await writeStateFile(INSTALLED_STATE_PATH, records);
|
|
1733
|
+
await removeLegacyLocalSkillMetadata(resolvedTargetRoot);
|
|
1734
|
+
}
|
|
1735
|
+
async function readUploadedSkillRecords() {
|
|
1736
|
+
const parsed = await readStateFile(UPLOADED_STATE_PATH);
|
|
1737
|
+
return parsed.flatMap((entry) => {
|
|
1738
|
+
if (!entry || typeof entry !== 'object') {
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
const skillId = Number(entry.skillId);
|
|
1742
|
+
const normalizedSkillId = Number.isInteger(skillId) && skillId > 0 ? skillId : undefined;
|
|
1743
|
+
return [{
|
|
1744
|
+
directory: typeof entry.directory === 'string' ? resolve(entry.directory) : '',
|
|
1745
|
+
baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
|
|
1746
|
+
updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
|
|
1747
|
+
skillId: normalizedSkillId,
|
|
1748
|
+
skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
|
|
1749
|
+
skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
|
|
1750
|
+
skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
|
|
1751
|
+
}].filter((record) => record.directory.length > 0);
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
async function readInstalledSkillRecords() {
|
|
1755
|
+
const parsed = await readStateFile(INSTALLED_STATE_PATH);
|
|
1756
|
+
return parsed.flatMap((entry) => {
|
|
1757
|
+
if (!entry || typeof entry !== 'object') {
|
|
1758
|
+
return [];
|
|
1759
|
+
}
|
|
1760
|
+
const skillId = Number(entry.skillId);
|
|
1761
|
+
if (!Number.isInteger(skillId) || skillId <= 0) {
|
|
1762
|
+
return [];
|
|
1763
|
+
}
|
|
1764
|
+
return [{
|
|
1765
|
+
targetRoot: typeof entry.targetRoot === 'string' ? resolve(entry.targetRoot) : '',
|
|
1766
|
+
baseUrl: typeof entry.baseUrl === 'string' ? normalizeBaseUrl(entry.baseUrl) : PROD_URL,
|
|
1767
|
+
updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
|
|
1768
|
+
skillId,
|
|
1769
|
+
skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
|
|
1770
|
+
skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
|
|
1771
|
+
skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
|
|
1772
|
+
}].filter((record) => record.targetRoot.length > 0);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
async function readStateFile(path) {
|
|
1776
|
+
try {
|
|
1777
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
1778
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
if (error?.code === 'ENOENT') {
|
|
1782
|
+
return [];
|
|
1783
|
+
}
|
|
1784
|
+
throw new Error(`Failed to read ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function writeStateFile(path, value) {
|
|
1788
|
+
await mkdir(APP_DIR, { recursive: true });
|
|
1789
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
|
|
1790
|
+
mode: process.platform === 'win32' ? undefined : 0o600,
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
async function removeLegacyLocalSkillMetadata(directory) {
|
|
1794
|
+
try {
|
|
1795
|
+
await rm(join(resolve(directory), SKILL_METADATA_FILE_NAME));
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
if (error?.code === 'ENOENT') {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
throw error;
|
|
1802
|
+
}
|
|
1236
1803
|
}
|
|
1237
|
-
async function collectFilePlans(root, current = root) {
|
|
1804
|
+
async function collectFilePlans(root, current = root, options = {}) {
|
|
1238
1805
|
const entries = await readdir(current, { withFileTypes: true });
|
|
1239
1806
|
const files = [];
|
|
1240
1807
|
for (const entry of entries) {
|
|
1241
1808
|
const entryPath = join(current, entry.name);
|
|
1242
1809
|
if (entry.isDirectory()) {
|
|
1243
|
-
files.push(...(await collectFilePlans(root, entryPath)));
|
|
1810
|
+
files.push(...(await collectFilePlans(root, entryPath, options)));
|
|
1244
1811
|
continue;
|
|
1245
1812
|
}
|
|
1246
1813
|
const entryStat = await stat(entryPath);
|
|
@@ -1248,7 +1815,7 @@ async function collectFilePlans(root, current = root) {
|
|
|
1248
1815
|
continue;
|
|
1249
1816
|
}
|
|
1250
1817
|
const relativePath = relative(root, entryPath).replaceAll('\\', '/');
|
|
1251
|
-
if (relativePath === SKILL_METADATA_FILE_NAME) {
|
|
1818
|
+
if (!options.includeSkillMetadata && relativePath === SKILL_METADATA_FILE_NAME) {
|
|
1252
1819
|
continue;
|
|
1253
1820
|
}
|
|
1254
1821
|
files.push({
|
|
@@ -1284,14 +1851,51 @@ function validateSkillDirectory(files) {
|
|
|
1284
1851
|
}
|
|
1285
1852
|
}
|
|
1286
1853
|
function guessContentType(path) {
|
|
1287
|
-
|
|
1854
|
+
const normalized = path.toLowerCase();
|
|
1855
|
+
if (normalized.endsWith('.html') || normalized.endsWith('.htm'))
|
|
1856
|
+
return 'text/html; charset=utf-8';
|
|
1857
|
+
if (normalized.endsWith('.css'))
|
|
1858
|
+
return 'text/css; charset=utf-8';
|
|
1859
|
+
if (normalized.endsWith('.js') || normalized.endsWith('.mjs'))
|
|
1860
|
+
return 'text/javascript; charset=utf-8';
|
|
1861
|
+
if (normalized.endsWith('.md'))
|
|
1288
1862
|
return 'text/markdown; charset=utf-8';
|
|
1289
|
-
if (
|
|
1863
|
+
if (normalized.endsWith('.json') || normalized.endsWith('.webmanifest'))
|
|
1290
1864
|
return 'application/json; charset=utf-8';
|
|
1291
|
-
if (
|
|
1865
|
+
if (normalized.endsWith('.yaml') || normalized.endsWith('.yml'))
|
|
1292
1866
|
return 'application/yaml; charset=utf-8';
|
|
1293
|
-
if (
|
|
1867
|
+
if (normalized.endsWith('.txt') || normalized.endsWith('.csv'))
|
|
1294
1868
|
return 'text/plain; charset=utf-8';
|
|
1869
|
+
if (normalized.endsWith('.svg'))
|
|
1870
|
+
return 'image/svg+xml';
|
|
1871
|
+
if (normalized.endsWith('.png'))
|
|
1872
|
+
return 'image/png';
|
|
1873
|
+
if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg'))
|
|
1874
|
+
return 'image/jpeg';
|
|
1875
|
+
if (normalized.endsWith('.webp'))
|
|
1876
|
+
return 'image/webp';
|
|
1877
|
+
if (normalized.endsWith('.gif'))
|
|
1878
|
+
return 'image/gif';
|
|
1879
|
+
if (normalized.endsWith('.avif'))
|
|
1880
|
+
return 'image/avif';
|
|
1881
|
+
if (normalized.endsWith('.ico'))
|
|
1882
|
+
return 'image/x-icon';
|
|
1883
|
+
if (normalized.endsWith('.mp4'))
|
|
1884
|
+
return 'video/mp4';
|
|
1885
|
+
if (normalized.endsWith('.webm'))
|
|
1886
|
+
return 'video/webm';
|
|
1887
|
+
if (normalized.endsWith('.mp3'))
|
|
1888
|
+
return 'audio/mpeg';
|
|
1889
|
+
if (normalized.endsWith('.wav'))
|
|
1890
|
+
return 'audio/wav';
|
|
1891
|
+
if (normalized.endsWith('.pdf'))
|
|
1892
|
+
return 'application/pdf';
|
|
1893
|
+
if (normalized.endsWith('.wasm'))
|
|
1894
|
+
return 'application/wasm';
|
|
1895
|
+
if (normalized.endsWith('.woff'))
|
|
1896
|
+
return 'font/woff';
|
|
1897
|
+
if (normalized.endsWith('.woff2'))
|
|
1898
|
+
return 'font/woff2';
|
|
1295
1899
|
return 'application/octet-stream';
|
|
1296
1900
|
}
|
|
1297
1901
|
function formatByteSize(bytes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quadient/skillshare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Skillshare CLI for installing and publishing Quadient skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^
|
|
25
|
-
"tsx": "^4.
|
|
26
|
-
"typescript": "^
|
|
24
|
+
"@types/node": "^26.1.1",
|
|
25
|
+
"tsx": "^4.23.1",
|
|
26
|
+
"typescript": "^7.0.2"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=20"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@clack/prompts": "^1.
|
|
32
|
+
"@clack/prompts": "^1.7.0"
|
|
33
33
|
}
|
|
34
34
|
}
|