@quadient/skillshare 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/bin/skillshare +2 -0
- package/dist/index.js +1252 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Skillshare TUI
|
|
2
|
+
|
|
3
|
+
Development CLI for the Skillshare registry.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run index.ts
|
|
9
|
+
bun run index.ts login
|
|
10
|
+
bun run index.ts login local
|
|
11
|
+
bun run index.ts list
|
|
12
|
+
bun run index.ts upload ./examples/prompt-lint --version 0.1.0
|
|
13
|
+
bun run index.ts install 12 0.1.0 --target ./installed
|
|
14
|
+
bun run index.ts install --skill-version-id 42 --target ./installed
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Interactive behavior:
|
|
18
|
+
|
|
19
|
+
- If the current directory already contains SKILL.md, the TUI offers to upload it before showing the main action picker.
|
|
20
|
+
- After install or upload, the CLI writes skillshare.metadata.json into the skill directory with the skill id, name, description, and installed or published version.
|
|
21
|
+
- When skillshare.metadata.json already exists, uploads publish a new version of that skill instead of creating a duplicate entry.
|
|
22
|
+
- New skill creation derives name and description from SKILL.md frontmatter when available, so the user usually only needs to enter the initial version.
|
|
23
|
+
- Upload and publish commands show progress while scanning and reading local files, then keep a timed upload status visible until the registry call finishes.
|
|
24
|
+
|
|
25
|
+
Login opens a browser against the selected Skillshare host, exchanges the backend login code for CLI tokens, and stores the access token plus refresh token in the user config directory.
|
|
26
|
+
|
|
27
|
+
Without an existing session, the CLI now defaults to the production registry. Use `login local` only when you want the local backend.
|
|
28
|
+
|
|
29
|
+
Session storage paths:
|
|
30
|
+
|
|
31
|
+
- macOS/Linux: `~/.config/skillshare/session.json`
|
|
32
|
+
- Windows: `%APPDATA%\\skillshare\\session.json`
|
|
33
|
+
|
|
34
|
+
## Package Distribution
|
|
35
|
+
|
|
36
|
+
The CLI is prepared for publishing as `@quadient/skillshare`.
|
|
37
|
+
|
|
38
|
+
Recommended invocation:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @quadient/skillshare
|
|
42
|
+
bunx @quadient/skillshare
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Packaging checks:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bunx tsc --noEmit
|
|
49
|
+
npm run build
|
|
50
|
+
npm pack --dry-run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Publishing from the repo root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bun run publish:tui:dry-run
|
|
57
|
+
bun run publish:tui
|
|
58
|
+
bun run publish:tui -- --otp 123456
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The publish script builds `tui/`, validates the packed files with `npm pack --dry-run`, and only then runs `npm publish`.
|
package/bin/skillshare
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { cancel, confirm, intro, isCancel, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
|
|
9
|
+
const PROD_URL = 'https://skillshare.inspireclouduat.net';
|
|
10
|
+
const LOCAL_URL = 'http://localhost:5050';
|
|
11
|
+
const APP_DIR = getAppDir();
|
|
12
|
+
const SESSION_PATH = join(APP_DIR, 'session.json');
|
|
13
|
+
const SKILL_FILE_NAME = 'SKILL.md';
|
|
14
|
+
const SKILL_METADATA_FILE_NAME = 'skillshare.metadata.json';
|
|
15
|
+
const UNIVERSAL_PROJECT_SKILLS_PATH = '.agents/skills';
|
|
16
|
+
const UNIVERSAL_PROJECT_AGENT_VALUE = '__universal-project__';
|
|
17
|
+
const AGENTS = [
|
|
18
|
+
{ value: 'amp', label: 'Amp', projectPath: '.agents/skills', globalPath: '.config/agents/skills' },
|
|
19
|
+
{ value: 'kimi-cli', label: 'Kimi Code CLI', projectPath: '.agents/skills', globalPath: '.config/agents/skills' },
|
|
20
|
+
{ value: 'replit', label: 'Replit', projectPath: '.agents/skills', globalPath: '.config/agents/skills' },
|
|
21
|
+
{ value: 'universal', label: 'Universal', projectPath: '.agents/skills', globalPath: '.config/agents/skills' },
|
|
22
|
+
{ value: 'antigravity', label: 'Antigravity', projectPath: '.agents/skills', globalPath: '.gemini/antigravity/skills' },
|
|
23
|
+
{ value: 'augment', label: 'Augment', projectPath: '.augment/skills', globalPath: '.augment/skills' },
|
|
24
|
+
{ value: 'bob', label: 'IBM Bob', projectPath: '.bob/skills', globalPath: '.bob/skills' },
|
|
25
|
+
{ value: 'claude-code', label: 'Claude Code', projectPath: '.claude/skills', globalPath: '.claude/skills' },
|
|
26
|
+
{ value: 'openclaw', label: 'OpenClaw', projectPath: 'skills', globalPath: '.openclaw/skills' },
|
|
27
|
+
{ value: 'cline', label: 'Cline', projectPath: '.agents/skills', globalPath: '.agents/skills' },
|
|
28
|
+
{ value: 'warp', label: 'Warp', projectPath: '.agents/skills', globalPath: '.agents/skills' },
|
|
29
|
+
{ value: 'codebuddy', label: 'CodeBuddy', projectPath: '.codebuddy/skills', globalPath: '.codebuddy/skills' },
|
|
30
|
+
{ value: 'codex', label: 'Codex', projectPath: '.agents/skills', globalPath: '.codex/skills' },
|
|
31
|
+
{ value: 'command-code', label: 'Command Code', projectPath: '.commandcode/skills', globalPath: '.commandcode/skills' },
|
|
32
|
+
{ value: 'continue', label: 'Continue', projectPath: '.continue/skills', globalPath: '.continue/skills' },
|
|
33
|
+
{ value: 'cortex', label: 'Cortex Code', projectPath: '.cortex/skills', globalPath: '.snowflake/cortex/skills' },
|
|
34
|
+
{ value: 'crush', label: 'Crush', projectPath: '.crush/skills', globalPath: '.config/crush/skills' },
|
|
35
|
+
{ value: 'cursor', label: 'Cursor', projectPath: '.agents/skills', globalPath: '.cursor/skills' },
|
|
36
|
+
{ value: 'deepagents', label: 'Deep Agents', projectPath: '.agents/skills', globalPath: '.deepagents/agent/skills' },
|
|
37
|
+
{ value: 'droid', label: 'Droid', projectPath: '.factory/skills', globalPath: '.factory/skills' },
|
|
38
|
+
{ value: 'firebender', label: 'Firebender', projectPath: '.agents/skills', globalPath: '.firebender/skills' },
|
|
39
|
+
{ value: 'gemini-cli', label: 'Gemini CLI', projectPath: '.agents/skills', globalPath: '.gemini/skills' },
|
|
40
|
+
{ value: 'github-copilot', label: 'GitHub Copilot', projectPath: '.agents/skills', globalPath: '.copilot/skills' },
|
|
41
|
+
{ value: 'goose', label: 'Goose', projectPath: '.goose/skills', globalPath: '.config/goose/skills' },
|
|
42
|
+
{ value: 'junie', label: 'Junie', projectPath: '.junie/skills', globalPath: '.junie/skills' },
|
|
43
|
+
{ value: 'iflow-cli', label: 'iFlow CLI', projectPath: '.iflow/skills', globalPath: '.iflow/skills' },
|
|
44
|
+
{ value: 'kilo', label: 'Kilo Code', projectPath: '.kilocode/skills', globalPath: '.kilocode/skills' },
|
|
45
|
+
{ value: 'kiro-cli', label: 'Kiro CLI', projectPath: '.kiro/skills', globalPath: '.kiro/skills' },
|
|
46
|
+
{ value: 'kode', label: 'Kode', projectPath: '.kode/skills', globalPath: '.kode/skills' },
|
|
47
|
+
{ value: 'mcpjam', label: 'MCPJam', projectPath: '.mcpjam/skills', globalPath: '.mcpjam/skills' },
|
|
48
|
+
{ value: 'mistral-vibe', label: 'Mistral Vibe', projectPath: '.vibe/skills', globalPath: '.vibe/skills' },
|
|
49
|
+
{ value: 'mux', label: 'Mux', projectPath: '.mux/skills', globalPath: '.mux/skills' },
|
|
50
|
+
{ value: 'opencode', label: 'OpenCode', projectPath: '.agents/skills', globalPath: '.config/opencode/skills' },
|
|
51
|
+
{ value: 'openhands', label: 'OpenHands', projectPath: '.openhands/skills', globalPath: '.openhands/skills' },
|
|
52
|
+
{ value: 'pi', label: 'Pi', projectPath: '.pi/skills', globalPath: '.pi/agent/skills' },
|
|
53
|
+
{ value: 'qoder', label: 'Qoder', projectPath: '.qoder/skills', globalPath: '.qoder/skills' },
|
|
54
|
+
{ value: 'qwen-code', label: 'Qwen Code', projectPath: '.qwen/skills', globalPath: '.qwen/skills' },
|
|
55
|
+
{ value: 'roo', label: 'Roo Code', projectPath: '.roo/skills', globalPath: '.roo/skills' },
|
|
56
|
+
{ value: 'trae', label: 'Trae', projectPath: '.trae/skills', globalPath: '.trae/skills' },
|
|
57
|
+
{ value: 'trae-cn', label: 'Trae CN', projectPath: '.trae/skills', globalPath: '.trae-cn/skills' },
|
|
58
|
+
{ value: 'windsurf', label: 'Windsurf', projectPath: '.windsurf/skills', globalPath: '.codeium/windsurf/skills' },
|
|
59
|
+
{ value: 'zencoder', label: 'Zencoder', projectPath: '.zencoder/skills', globalPath: '.zencoder/skills' },
|
|
60
|
+
{ value: 'neovate', label: 'Neovate', projectPath: '.neovate/skills', globalPath: '.neovate/skills' },
|
|
61
|
+
{ value: 'pochi', label: 'Pochi', projectPath: '.pochi/skills', globalPath: '.pochi/skills' },
|
|
62
|
+
{ value: 'adal', label: 'AdaL', projectPath: '.adal/skills', globalPath: '.adal/skills' },
|
|
63
|
+
];
|
|
64
|
+
const usage = `
|
|
65
|
+
skillshare <command> [options]
|
|
66
|
+
|
|
67
|
+
Commands:
|
|
68
|
+
login [local]
|
|
69
|
+
logout
|
|
70
|
+
whoami
|
|
71
|
+
list
|
|
72
|
+
show <skillId> [version]
|
|
73
|
+
upload <directory> --version <version> [--skill-id <id>] [--name <name> --description <desc>]
|
|
74
|
+
install <skillId> [version] --target <directory>
|
|
75
|
+
install --skill-version-id <id> --target <directory>
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
--token <token> Bearer token for API requests
|
|
79
|
+
`;
|
|
80
|
+
async function main() {
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
const command = args[0];
|
|
83
|
+
if (!command) {
|
|
84
|
+
await runInteractive();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (command === '--help' || command === '-h') {
|
|
88
|
+
console.log(usage.trim());
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const options = await parseOptions(args.slice(1));
|
|
92
|
+
switch (command) {
|
|
93
|
+
case 'login': {
|
|
94
|
+
await login(options.positionals[0] === 'local' ? LOCAL_URL : PROD_URL);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
case 'logout': {
|
|
98
|
+
await logoutSession();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case 'whoami':
|
|
102
|
+
case 'list':
|
|
103
|
+
case 'show':
|
|
104
|
+
case 'upload':
|
|
105
|
+
case 'install': {
|
|
106
|
+
const client = await createClient(await resolveActiveBaseUrl(), options.token);
|
|
107
|
+
switch (command) {
|
|
108
|
+
case 'whoami': {
|
|
109
|
+
const user = await client.get('/api/auth/me');
|
|
110
|
+
console.log(JSON.stringify(user, null, 2));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case 'list': {
|
|
114
|
+
const skills = await client.get('/api/skills');
|
|
115
|
+
for (const skill of skills) {
|
|
116
|
+
console.log(`${skill.id}\t${skill.latestVersion}\t${skill.name}`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case 'show': {
|
|
121
|
+
const skillId = options.positionals[0];
|
|
122
|
+
const version = options.positionals[1];
|
|
123
|
+
if (!skillId) {
|
|
124
|
+
throw new Error('show requires <skillId>');
|
|
125
|
+
}
|
|
126
|
+
const resource = version
|
|
127
|
+
? `/api/skills/${skillId}/versions/${version}`
|
|
128
|
+
: `/api/skills/${skillId}`;
|
|
129
|
+
const data = await client.get(resource);
|
|
130
|
+
console.log(JSON.stringify(data, null, 2));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'upload': {
|
|
134
|
+
const directory = options.positionals[0];
|
|
135
|
+
const version = options.flags.version;
|
|
136
|
+
if (!directory || !version) {
|
|
137
|
+
throw new Error('upload requires <directory> --version <version>');
|
|
138
|
+
}
|
|
139
|
+
const localSkill = await loadLocalSkillContext(directory);
|
|
140
|
+
const skillId = options.flags['skill-id']
|
|
141
|
+
? parsePositiveInteger(options.flags['skill-id'], '--skill-id')
|
|
142
|
+
: localSkill.metadata?.skillId;
|
|
143
|
+
if (!skillId) {
|
|
144
|
+
const created = await createSkillFromDirectory(client, localSkill, {
|
|
145
|
+
name: options.flags.name,
|
|
146
|
+
description: options.flags.description,
|
|
147
|
+
version,
|
|
148
|
+
});
|
|
149
|
+
console.log(JSON.stringify(created.response, null, 2));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
153
|
+
skillId,
|
|
154
|
+
version,
|
|
155
|
+
failIfMissing: false,
|
|
156
|
+
});
|
|
157
|
+
console.log(JSON.stringify(published.response, null, 2));
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
case 'install': {
|
|
162
|
+
const skillVersionId = options.flags['skill-version-id'];
|
|
163
|
+
const skillId = options.positionals[0];
|
|
164
|
+
const version = options.positionals[1];
|
|
165
|
+
const target = options.flags.target;
|
|
166
|
+
if (skillVersionId) {
|
|
167
|
+
if (!target) {
|
|
168
|
+
throw new Error('install with --skill-version-id requires --target <directory>');
|
|
169
|
+
}
|
|
170
|
+
const parsedSkillVersionId = Number.parseInt(skillVersionId, 10);
|
|
171
|
+
if (!Number.isFinite(parsedSkillVersionId) || parsedSkillVersionId <= 0) {
|
|
172
|
+
throw new Error('--skill-version-id must be a positive number');
|
|
173
|
+
}
|
|
174
|
+
const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
|
|
175
|
+
const skill = await client.get(`/api/skills/${detail.skillId}`);
|
|
176
|
+
await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
|
|
177
|
+
await writeSkillVersionToDirectory(client, {
|
|
178
|
+
skillId: detail.skillId,
|
|
179
|
+
skillName: skill.name,
|
|
180
|
+
skillDescription: skill.description,
|
|
181
|
+
version: detail.version,
|
|
182
|
+
files: detail.files,
|
|
183
|
+
targetRoot: target,
|
|
184
|
+
});
|
|
185
|
+
console.log(`Installed skillVersion ${parsedSkillVersionId} (${detail.skillId}@${detail.version}) to ${target}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!skillId || !target) {
|
|
189
|
+
throw new Error('install requires <skillId> [version] --target <directory> or --skill-version-id <id> --target <directory>');
|
|
190
|
+
}
|
|
191
|
+
const skill = await client.get(`/api/skills/${skillId}`);
|
|
192
|
+
const effectiveVersion = version ?? skill.latestVersion;
|
|
193
|
+
if (!effectiveVersion) {
|
|
194
|
+
throw new Error(`Skill '${skillId}' does not have a published version.`);
|
|
195
|
+
}
|
|
196
|
+
const detail = await client.get(`/api/skills/${skillId}/versions/${effectiveVersion}`);
|
|
197
|
+
await client.post(`/api/skills/${skillId}/versions/${effectiveVersion}/downloads`);
|
|
198
|
+
await writeSkillVersionToDirectory(client, {
|
|
199
|
+
skillId: skill.id,
|
|
200
|
+
skillName: skill.name,
|
|
201
|
+
skillDescription: skill.description,
|
|
202
|
+
version: effectiveVersion,
|
|
203
|
+
files: detail.files,
|
|
204
|
+
targetRoot: target,
|
|
205
|
+
});
|
|
206
|
+
console.log(`Installed ${skillId}@${effectiveVersion} to ${target}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
default:
|
|
213
|
+
throw new Error(`Unknown command '${command}'`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function runInteractive() {
|
|
217
|
+
intro('Skillshare');
|
|
218
|
+
const baseUrl = await resolveActiveBaseUrl();
|
|
219
|
+
await ensureInteractiveLogin(baseUrl);
|
|
220
|
+
const client = await createClient(baseUrl);
|
|
221
|
+
if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
|
|
222
|
+
const localMetadata = await readLocalSkillMetadata(process.cwd());
|
|
223
|
+
const shouldUploadCurrent = await promptConfirm(localMetadata
|
|
224
|
+
? `Current directory contains ${SKILL_FILE_NAME}. Upload a new version of ${localMetadata.skillName || `skill ${localMetadata.skillId}`} now?`
|
|
225
|
+
: `Current directory contains ${SKILL_FILE_NAME}. Upload it now?`, true);
|
|
226
|
+
if (shouldUploadCurrent) {
|
|
227
|
+
await runInteractiveUpload(client, process.cwd());
|
|
228
|
+
outro('Done.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const action = await select({
|
|
233
|
+
message: 'What do you want to do?',
|
|
234
|
+
options: [
|
|
235
|
+
{ value: 'install', label: 'Install a skill', hint: 'Browse registry and install to agents' },
|
|
236
|
+
{ value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
if (isCancel(action)) {
|
|
240
|
+
cancel('Cancelled.');
|
|
241
|
+
process.exit(0);
|
|
242
|
+
}
|
|
243
|
+
if (action === 'install') {
|
|
244
|
+
await runInteractiveInstall(client);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
await runInteractiveUpload(client);
|
|
248
|
+
}
|
|
249
|
+
outro('Done.');
|
|
250
|
+
}
|
|
251
|
+
async function ensureInteractiveLogin(baseUrl) {
|
|
252
|
+
const session = await loadSession(baseUrl);
|
|
253
|
+
if (!session) {
|
|
254
|
+
await login(baseUrl);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (new Date(session.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
258
|
+
await login(baseUrl);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const client = await createClient(baseUrl);
|
|
263
|
+
await client.get('/api/auth/me');
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
await login(baseUrl);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function runInteractiveInstall(client) {
|
|
270
|
+
const skills = await client.get('/api/skills');
|
|
271
|
+
if (skills.length === 0) {
|
|
272
|
+
throw new Error('No skills available in the registry.');
|
|
273
|
+
}
|
|
274
|
+
const skill = await promptForSkill(skills);
|
|
275
|
+
const detail = await client.get(`/api/skills/${skill.id}`);
|
|
276
|
+
const selectedVersion = await promptForVersion(detail);
|
|
277
|
+
const scope = await promptForInstallScope();
|
|
278
|
+
const selectedAgents = await promptForAgents(scope);
|
|
279
|
+
if (selectedAgents.length === 0) {
|
|
280
|
+
throw new Error('Select at least one agent.');
|
|
281
|
+
}
|
|
282
|
+
const versionDetail = await client.get(`/api/skills/${skill.id}/versions/${selectedVersion}`);
|
|
283
|
+
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
284
|
+
const installName = createInstalledSkillDirectoryName(skill);
|
|
285
|
+
const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
|
|
286
|
+
for (const installTarget of installTargets) {
|
|
287
|
+
await writeSkillVersionToDirectory(client, {
|
|
288
|
+
skillId: skill.id,
|
|
289
|
+
skillName: skill.name,
|
|
290
|
+
skillDescription: skill.description,
|
|
291
|
+
version: selectedVersion,
|
|
292
|
+
files: versionDetail.files,
|
|
293
|
+
targetRoot: installTarget.targetRoot,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
console.log(`Installed ${skill.name}@${selectedVersion} to ${installTargets.length} location(s) for ${selectedAgents.length} agent(s) in ${scope} scope.`);
|
|
297
|
+
}
|
|
298
|
+
async function runInteractiveUpload(client, initialDirectory) {
|
|
299
|
+
const directory = initialDirectory
|
|
300
|
+
?? await promptText('Skill directory', '.', `Enter the directory containing ${SKILL_FILE_NAME}`);
|
|
301
|
+
const localSkill = await loadLocalSkillContext(directory);
|
|
302
|
+
if (!localSkill.metadata?.skillId) {
|
|
303
|
+
const version = await promptText('Initial version', '0.1.0');
|
|
304
|
+
const created = await createSkillFromDirectory(client, localSkill, { version });
|
|
305
|
+
console.log(`Created skill ${created.detail.id}: ${created.detail.name}@${version}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const skill = await client.get(`/api/skills/${localSkill.metadata.skillId}`);
|
|
309
|
+
const localVersion = normalizeText(localSkill.metadata.skillVersion);
|
|
310
|
+
const latestVersion = normalizeText(skill.latestVersion);
|
|
311
|
+
if (latestVersion && localVersion && latestVersion !== localVersion) {
|
|
312
|
+
const shouldDownloadLatest = await promptConfirm(`Local version is ${localVersion}, but the server latest is ${latestVersion}. Download it first? Local file changes will be overwritten.`, false);
|
|
313
|
+
if (shouldDownloadLatest) {
|
|
314
|
+
const latestDetail = await client.get(`/api/skills/${skill.id}/versions/${latestVersion}`);
|
|
315
|
+
await client.post(`/api/skills/${skill.id}/versions/${latestVersion}/downloads`);
|
|
316
|
+
await writeSkillVersionToDirectory(client, {
|
|
317
|
+
skillId: skill.id,
|
|
318
|
+
skillName: skill.name,
|
|
319
|
+
skillDescription: skill.description,
|
|
320
|
+
version: latestVersion,
|
|
321
|
+
files: latestDetail.files,
|
|
322
|
+
targetRoot: localSkill.directory,
|
|
323
|
+
});
|
|
324
|
+
console.log(`Downloaded ${skill.name}@${latestVersion} to ${localSkill.directory}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const version = await promptText('New version');
|
|
329
|
+
const published = await publishSkillFromDirectory(client, localSkill, {
|
|
330
|
+
skillId: skill.id,
|
|
331
|
+
version,
|
|
332
|
+
failIfMissing: true,
|
|
333
|
+
});
|
|
334
|
+
console.log(`Published ${published.detail.name}@${published.response.version}`);
|
|
335
|
+
}
|
|
336
|
+
async function promptForSkill(skills) {
|
|
337
|
+
let query = '';
|
|
338
|
+
while (true) {
|
|
339
|
+
const queryValue = await text({
|
|
340
|
+
message: 'Search skills',
|
|
341
|
+
placeholder: query || 'Type part of the skill name or description',
|
|
342
|
+
defaultValue: query,
|
|
343
|
+
validate(value) {
|
|
344
|
+
if (!(value ?? '').trim()) {
|
|
345
|
+
return 'Enter at least one search term.';
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
if (isCancel(queryValue)) {
|
|
350
|
+
cancel('Cancelled.');
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
query = queryValue.trim();
|
|
354
|
+
const filtered = filterSkills(skills, query).slice(0, 20);
|
|
355
|
+
if (filtered.length === 0) {
|
|
356
|
+
console.log(`No skills match "${query}". Try a different search.`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const selected = await select({
|
|
360
|
+
message: 'Select a skill',
|
|
361
|
+
options: [
|
|
362
|
+
...filtered.map((skill) => ({
|
|
363
|
+
value: String(skill.id),
|
|
364
|
+
label: `${skill.name}${skill.latestVersion ? ` (${skill.latestVersion})` : ''}`,
|
|
365
|
+
hint: skill.description,
|
|
366
|
+
})),
|
|
367
|
+
{ value: '__search_again__', label: 'Search again', hint: 'Change the filter query' },
|
|
368
|
+
],
|
|
369
|
+
});
|
|
370
|
+
if (isCancel(selected)) {
|
|
371
|
+
cancel('Cancelled.');
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
if (selected === '__search_again__') {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const skill = skills.find((item) => String(item.id) === selected);
|
|
378
|
+
if (!skill) {
|
|
379
|
+
throw new Error('Selected skill was not found.');
|
|
380
|
+
}
|
|
381
|
+
return skill;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function promptForVersion(detail) {
|
|
385
|
+
const selected = await select({
|
|
386
|
+
message: 'Select a version to install',
|
|
387
|
+
options: detail.versions.map((version) => ({
|
|
388
|
+
value: version.version,
|
|
389
|
+
label: `${version.version}${version.isLatest ? ' (latest)' : ''}`,
|
|
390
|
+
hint: `${new Date(version.created).toLocaleString()} · ${version.files.length} files`,
|
|
391
|
+
})),
|
|
392
|
+
});
|
|
393
|
+
if (isCancel(selected)) {
|
|
394
|
+
cancel('Cancelled.');
|
|
395
|
+
process.exit(0);
|
|
396
|
+
}
|
|
397
|
+
return selected;
|
|
398
|
+
}
|
|
399
|
+
async function promptForInstallScope() {
|
|
400
|
+
const selected = await select({
|
|
401
|
+
message: 'Install scope',
|
|
402
|
+
options: [
|
|
403
|
+
{ value: 'project', label: 'Project (default)', hint: 'Install into the current working directory' },
|
|
404
|
+
{ value: 'global', label: 'Global', hint: 'Install into your user profile' },
|
|
405
|
+
],
|
|
406
|
+
});
|
|
407
|
+
if (isCancel(selected)) {
|
|
408
|
+
cancel('Cancelled.');
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
return selected;
|
|
412
|
+
}
|
|
413
|
+
async function promptForAgents(scope) {
|
|
414
|
+
const ordered = AGENTS
|
|
415
|
+
.map((agent) => ({ agent, detected: isAgentDetected(agent, scope) }))
|
|
416
|
+
.sort((left, right) => Number(right.detected) - Number(left.detected) || left.agent.label.localeCompare(right.agent.label));
|
|
417
|
+
const universalAgents = scope === 'project'
|
|
418
|
+
? ordered.filter(({ agent }) => isUniversalProjectAgent(agent))
|
|
419
|
+
: [];
|
|
420
|
+
const specificAgents = scope === 'project'
|
|
421
|
+
? ordered.filter(({ agent }) => !isUniversalProjectAgent(agent))
|
|
422
|
+
: ordered;
|
|
423
|
+
const initialValues = scope === 'project' && universalAgents.length > 0
|
|
424
|
+
? [UNIVERSAL_PROJECT_AGENT_VALUE]
|
|
425
|
+
: [];
|
|
426
|
+
const selected = await multiselect({
|
|
427
|
+
message: 'Select agents',
|
|
428
|
+
options: [
|
|
429
|
+
...(scope === 'project' && universalAgents.length > 0
|
|
430
|
+
? [{
|
|
431
|
+
value: UNIVERSAL_PROJECT_AGENT_VALUE,
|
|
432
|
+
label: `Universal (${UNIVERSAL_PROJECT_SKILLS_PATH})`,
|
|
433
|
+
hint: `${universalAgents[0]?.detected ? 'detected · ' : ''}Shared project path for ${universalAgents.length} agents`,
|
|
434
|
+
}]
|
|
435
|
+
: []),
|
|
436
|
+
...specificAgents.map(({ agent, detected }) => ({
|
|
437
|
+
value: agent.value,
|
|
438
|
+
label: agent.label,
|
|
439
|
+
hint: `${detected ? 'detected · ' : ''}${scope === 'global' ? `~/${agent.globalPath}` : `./${agent.projectPath}`}`,
|
|
440
|
+
})),
|
|
441
|
+
],
|
|
442
|
+
initialValues,
|
|
443
|
+
required: false,
|
|
444
|
+
});
|
|
445
|
+
if (isCancel(selected)) {
|
|
446
|
+
cancel('Cancelled.');
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
449
|
+
const selectedValues = new Set(selected);
|
|
450
|
+
const includesUniversalProjectAgents = scope === 'project' && selectedValues.has(UNIVERSAL_PROJECT_AGENT_VALUE);
|
|
451
|
+
return ordered
|
|
452
|
+
.map(({ agent }) => agent)
|
|
453
|
+
.filter((agent) => selectedValues.has(agent.value) || (includesUniversalProjectAgents && isUniversalProjectAgent(agent)));
|
|
454
|
+
}
|
|
455
|
+
async function promptText(message, initialValue = '', placeholder) {
|
|
456
|
+
const value = await text({
|
|
457
|
+
message,
|
|
458
|
+
defaultValue: initialValue,
|
|
459
|
+
placeholder,
|
|
460
|
+
validate(input) {
|
|
461
|
+
if (!(input ?? '').trim()) {
|
|
462
|
+
return `${message} is required.`;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
if (isCancel(value)) {
|
|
467
|
+
cancel('Cancelled.');
|
|
468
|
+
process.exit(0);
|
|
469
|
+
}
|
|
470
|
+
return value.trim();
|
|
471
|
+
}
|
|
472
|
+
async function promptConfirm(message, initialValue = false) {
|
|
473
|
+
const value = await confirm({
|
|
474
|
+
message,
|
|
475
|
+
initialValue,
|
|
476
|
+
});
|
|
477
|
+
if (isCancel(value)) {
|
|
478
|
+
cancel('Cancelled.');
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
async function loadLocalSkillContext(directory) {
|
|
484
|
+
const resolvedDirectory = resolve(directory);
|
|
485
|
+
const scanStatus = spinner({ indicator: 'timer' });
|
|
486
|
+
scanStatus.start(`Scanning ${resolvedDirectory}`);
|
|
487
|
+
let filePlans;
|
|
488
|
+
try {
|
|
489
|
+
filePlans = await collectFilePlans(resolvedDirectory);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
scanStatus.clear();
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
scanStatus.stop(`Found ${filePlans.length} file${filePlans.length === 1 ? '' : 's'} in ${resolvedDirectory}`);
|
|
496
|
+
const totalBytes = filePlans.reduce((sum, file) => sum + file.size, 0);
|
|
497
|
+
const readProgress = progress({ max: Math.max(filePlans.length, 1), size: 32 });
|
|
498
|
+
if (filePlans.length > 0) {
|
|
499
|
+
readProgress.start(`Reading files 0/${filePlans.length}`);
|
|
500
|
+
}
|
|
501
|
+
let files;
|
|
502
|
+
try {
|
|
503
|
+
files = await collectFiles(filePlans, (completed, total, relativePath) => {
|
|
504
|
+
readProgress.advance(1, `Reading files ${completed}/${total} · ${relativePath}`);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
readProgress.clear();
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
if (filePlans.length > 0) {
|
|
512
|
+
readProgress.stop(`Prepared ${files.length} file${files.length === 1 ? '' : 's'} (${formatByteSize(totalBytes)})`);
|
|
513
|
+
}
|
|
514
|
+
validateSkillDirectory(files);
|
|
515
|
+
return {
|
|
516
|
+
directory: resolvedDirectory,
|
|
517
|
+
files,
|
|
518
|
+
totalBytes,
|
|
519
|
+
metadata: await readLocalSkillMetadata(resolvedDirectory),
|
|
520
|
+
defaults: deriveSkillDefaults(files, resolvedDirectory),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function createSkillFromDirectory(client, localSkill, input) {
|
|
524
|
+
const name = normalizeText(input.name) ?? localSkill.defaults.name;
|
|
525
|
+
const description = normalizeText(input.description) ?? localSkill.defaults.description;
|
|
526
|
+
const uploadStatus = spinner({ indicator: 'timer' });
|
|
527
|
+
uploadStatus.start(`Uploading ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
528
|
+
try {
|
|
529
|
+
const response = await client.createSkill({
|
|
530
|
+
name,
|
|
531
|
+
description,
|
|
532
|
+
version: input.version,
|
|
533
|
+
files: localSkill.files,
|
|
534
|
+
});
|
|
535
|
+
uploadStatus.message(`Fetching saved metadata for skill ${response.id}`);
|
|
536
|
+
const detail = await client.get(`/api/skills/${response.id}`);
|
|
537
|
+
await writeLocalSkillMetadata(localSkill.directory, createLocalSkillMetadata(detail, detail.latestVersion ?? input.version));
|
|
538
|
+
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
539
|
+
return { response, detail };
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
uploadStatus.clear();
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function publishSkillFromDirectory(client, localSkill, input) {
|
|
547
|
+
if (input.failIfMissing && !localSkill.metadata?.skillId) {
|
|
548
|
+
throw new Error(`Missing ${SKILL_METADATA_FILE_NAME}. Create the skill first or restore the metadata file.`);
|
|
549
|
+
}
|
|
550
|
+
const uploadStatus = spinner({ indicator: 'timer' });
|
|
551
|
+
uploadStatus.start(`Uploading ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
552
|
+
try {
|
|
553
|
+
const response = await client.publishVersion({
|
|
554
|
+
skillId: input.skillId,
|
|
555
|
+
version: input.version,
|
|
556
|
+
files: localSkill.files,
|
|
557
|
+
});
|
|
558
|
+
uploadStatus.message(`Fetching saved metadata for skill ${input.skillId}`);
|
|
559
|
+
const detail = await client.get(`/api/skills/${input.skillId}`);
|
|
560
|
+
await writeLocalSkillMetadata(localSkill.directory, createLocalSkillMetadata(detail, response.version));
|
|
561
|
+
uploadStatus.stop(`Uploaded ${localSkill.files.length} file${localSkill.files.length === 1 ? '' : 's'} (${formatByteSize(localSkill.totalBytes)})`);
|
|
562
|
+
return { response, detail };
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
uploadStatus.clear();
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function writeSkillVersionToDirectory(client, input) {
|
|
570
|
+
for (const file of input.files) {
|
|
571
|
+
const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${file.nameWithPath}`);
|
|
572
|
+
const destination = join(input.targetRoot, file.nameWithPath);
|
|
573
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
574
|
+
await writeFile(destination, Buffer.from(await response.arrayBuffer()));
|
|
575
|
+
}
|
|
576
|
+
await writeLocalSkillMetadata(input.targetRoot, {
|
|
577
|
+
skillId: input.skillId,
|
|
578
|
+
skillName: input.skillName,
|
|
579
|
+
skillDescription: input.skillDescription,
|
|
580
|
+
skillVersion: input.version,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function deriveSkillDefaults(files, directory) {
|
|
584
|
+
const skillFile = files.find((file) => file.relativePath === SKILL_FILE_NAME);
|
|
585
|
+
if (!skillFile) {
|
|
586
|
+
throw new Error(`The skill directory must contain ${SKILL_FILE_NAME}.`);
|
|
587
|
+
}
|
|
588
|
+
const parsedSkillFile = parseSkillFile(skillFile.content.toString('utf8'));
|
|
589
|
+
const name = normalizeText(parsedSkillFile.attributes.name ?? parsedSkillFile.attributes.title)
|
|
590
|
+
?? extractMarkdownTitle(parsedSkillFile.body)
|
|
591
|
+
?? humanizeSkillName(basename(directory));
|
|
592
|
+
const description = normalizeText(parsedSkillFile.attributes.description)
|
|
593
|
+
?? extractMarkdownDescription(parsedSkillFile.body)
|
|
594
|
+
?? `Skill published from ${basename(directory)}.`;
|
|
595
|
+
return { name, description };
|
|
596
|
+
}
|
|
597
|
+
function parseSkillFile(content) {
|
|
598
|
+
const normalized = content.replaceAll('\r\n', '\n');
|
|
599
|
+
if (!normalized.startsWith('---\n')) {
|
|
600
|
+
return {
|
|
601
|
+
attributes: {},
|
|
602
|
+
body: normalized,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const lines = normalized.split('\n');
|
|
606
|
+
let closingLineIndex = -1;
|
|
607
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
608
|
+
if (lines[index]?.trim() === '---') {
|
|
609
|
+
closingLineIndex = index;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (closingLineIndex === -1) {
|
|
614
|
+
return {
|
|
615
|
+
attributes: {},
|
|
616
|
+
body: normalized,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
attributes: parseFrontmatter(lines.slice(1, closingLineIndex)),
|
|
621
|
+
body: lines.slice(closingLineIndex + 1).join('\n'),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function parseFrontmatter(lines) {
|
|
625
|
+
const attributes = {};
|
|
626
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
627
|
+
const line = lines[index]?.trimEnd() ?? '';
|
|
628
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
629
|
+
if (!match) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const rawKey = match[1];
|
|
633
|
+
const rawValue = match[2];
|
|
634
|
+
if (!rawKey || rawValue === undefined) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const key = rawKey.toLowerCase();
|
|
638
|
+
const value = rawValue.trim();
|
|
639
|
+
if (value === '|' || value === '>') {
|
|
640
|
+
const blockLines = [];
|
|
641
|
+
let nextIndex = index + 1;
|
|
642
|
+
while (nextIndex < lines.length) {
|
|
643
|
+
const blockLine = lines[nextIndex] ?? '';
|
|
644
|
+
if (blockLine.length > 0 && !/^\s/.test(blockLine)) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
blockLines.push(blockLine.replace(/^\s+/, ''));
|
|
648
|
+
nextIndex += 1;
|
|
649
|
+
}
|
|
650
|
+
attributes[key] = value === '>'
|
|
651
|
+
? blockLines.join(' ').replace(/\s+/g, ' ').trim()
|
|
652
|
+
: blockLines.join('\n').trim();
|
|
653
|
+
index = nextIndex - 1;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
attributes[key] = unquoteFrontmatterValue(value);
|
|
657
|
+
}
|
|
658
|
+
return attributes;
|
|
659
|
+
}
|
|
660
|
+
function unquoteFrontmatterValue(value) {
|
|
661
|
+
const trimmed = value.trim();
|
|
662
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
663
|
+
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
664
|
+
return trimmed.slice(1, -1).trim();
|
|
665
|
+
}
|
|
666
|
+
return trimmed;
|
|
667
|
+
}
|
|
668
|
+
function extractMarkdownTitle(markdown) {
|
|
669
|
+
for (const line of markdown.split('\n')) {
|
|
670
|
+
const trimmed = line.trim();
|
|
671
|
+
if (!trimmed) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const heading = /^#\s+(.+)$/.exec(trimmed);
|
|
675
|
+
if (heading?.[1]) {
|
|
676
|
+
return heading[1].trim();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
function extractMarkdownDescription(markdown) {
|
|
682
|
+
const paragraphs = markdown
|
|
683
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
684
|
+
.split(/\n\s*\n/g);
|
|
685
|
+
for (const paragraph of paragraphs) {
|
|
686
|
+
const lines = paragraph
|
|
687
|
+
.split('\n')
|
|
688
|
+
.map((line) => line.trim())
|
|
689
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
690
|
+
const text = lines.join(' ').replace(/\s+/g, ' ').trim();
|
|
691
|
+
if (text) {
|
|
692
|
+
return text;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
function humanizeSkillName(value) {
|
|
698
|
+
const normalized = value
|
|
699
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
700
|
+
.trim()
|
|
701
|
+
.split(/\s+/)
|
|
702
|
+
.filter(Boolean)
|
|
703
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
704
|
+
.join(' ');
|
|
705
|
+
return normalized || 'Untitled Skill';
|
|
706
|
+
}
|
|
707
|
+
function createLocalSkillMetadata(detail, version) {
|
|
708
|
+
return {
|
|
709
|
+
skillId: detail.id,
|
|
710
|
+
skillName: detail.name,
|
|
711
|
+
skillDescription: detail.description,
|
|
712
|
+
skillVersion: version,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function parsePositiveInteger(value, optionName) {
|
|
716
|
+
const parsedValue = Number.parseInt(value, 10);
|
|
717
|
+
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
|
|
718
|
+
throw new Error(`${optionName} must be a positive number`);
|
|
719
|
+
}
|
|
720
|
+
return parsedValue;
|
|
721
|
+
}
|
|
722
|
+
function normalizeText(value) {
|
|
723
|
+
const trimmed = value?.trim();
|
|
724
|
+
return trimmed ? trimmed : null;
|
|
725
|
+
}
|
|
726
|
+
function filterSkills(skills, query) {
|
|
727
|
+
const normalized = query.trim().toLowerCase();
|
|
728
|
+
return skills.filter((skill) => skill.name.toLowerCase().includes(normalized) ||
|
|
729
|
+
skill.description.toLowerCase().includes(normalized));
|
|
730
|
+
}
|
|
731
|
+
function isAgentDetected(agent, scope) {
|
|
732
|
+
const basePath = getAgentBaseDir(agent, scope);
|
|
733
|
+
return existsSync(basePath);
|
|
734
|
+
}
|
|
735
|
+
function isUniversalProjectAgent(agent) {
|
|
736
|
+
return agent.projectPath === UNIVERSAL_PROJECT_SKILLS_PATH;
|
|
737
|
+
}
|
|
738
|
+
function getAgentBaseDir(agent, scope) {
|
|
739
|
+
return scope === 'global'
|
|
740
|
+
? join(homedir(), agent.globalPath)
|
|
741
|
+
: join(process.cwd(), agent.projectPath);
|
|
742
|
+
}
|
|
743
|
+
function resolveInstallTargets(agents, scope, installName) {
|
|
744
|
+
const targets = new Map();
|
|
745
|
+
for (const agent of agents) {
|
|
746
|
+
const targetRoot = join(getAgentBaseDir(agent, scope), installName);
|
|
747
|
+
if (!targets.has(targetRoot)) {
|
|
748
|
+
targets.set(targetRoot, { targetRoot });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return [...targets.values()];
|
|
752
|
+
}
|
|
753
|
+
function createInstalledSkillDirectoryName(skill) {
|
|
754
|
+
const normalized = skill.name
|
|
755
|
+
.toLowerCase()
|
|
756
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
757
|
+
.replace(/^-+|-+$/g, '');
|
|
758
|
+
return normalized.length > 0 ? normalized : `skill-${skill.id}`;
|
|
759
|
+
}
|
|
760
|
+
async function login(baseUrl) {
|
|
761
|
+
baseUrl = normalizeBaseUrl(baseUrl);
|
|
762
|
+
const state = cryptoRandomHex(16);
|
|
763
|
+
const codeVerifier = createPkceVerifier();
|
|
764
|
+
const codeChallenge = createPkceChallenge(codeVerifier);
|
|
765
|
+
const callback = await waitForLoginCallback();
|
|
766
|
+
const loginUrl = `${baseUrl}/api/cli/login?redirectUri=${encodeURIComponent(callback.redirectUri)}` +
|
|
767
|
+
`&codeChallenge=${encodeURIComponent(codeChallenge)}` +
|
|
768
|
+
`&state=${encodeURIComponent(state)}`;
|
|
769
|
+
await openBrowser(loginUrl);
|
|
770
|
+
console.log(`Opening browser for ${baseUrl}`);
|
|
771
|
+
const authCode = await callback.wait(state);
|
|
772
|
+
const response = await fetchJson(`${baseUrl}/api/cli/token`, {
|
|
773
|
+
method: 'POST',
|
|
774
|
+
headers: {
|
|
775
|
+
'Content-Type': 'application/json',
|
|
776
|
+
Accept: 'application/json',
|
|
777
|
+
},
|
|
778
|
+
body: JSON.stringify({
|
|
779
|
+
code: authCode,
|
|
780
|
+
redirectUri: callback.redirectUri,
|
|
781
|
+
codeVerifier,
|
|
782
|
+
}),
|
|
783
|
+
});
|
|
784
|
+
await storeSession({
|
|
785
|
+
baseUrl,
|
|
786
|
+
accessToken: response.accessToken,
|
|
787
|
+
accessTokenExpiresAt: response.accessTokenExpiresAt,
|
|
788
|
+
refreshToken: response.refreshToken,
|
|
789
|
+
refreshTokenExpiresAt: response.refreshTokenExpiresAt,
|
|
790
|
+
user: response.user,
|
|
791
|
+
});
|
|
792
|
+
console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
|
|
793
|
+
}
|
|
794
|
+
async function parseOptions(args) {
|
|
795
|
+
const flags = {};
|
|
796
|
+
const positionals = [];
|
|
797
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
798
|
+
const current = args[index];
|
|
799
|
+
if (!current) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (current.startsWith('--')) {
|
|
803
|
+
const key = current.slice(2);
|
|
804
|
+
const value = args[index + 1];
|
|
805
|
+
if (!value || value.startsWith('--')) {
|
|
806
|
+
throw new Error(`Missing value for ${current}`);
|
|
807
|
+
}
|
|
808
|
+
flags[key] = value;
|
|
809
|
+
index += 1;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
positionals.push(current);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
token: flags.token,
|
|
817
|
+
flags,
|
|
818
|
+
positionals,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async function resolveActiveBaseUrl() {
|
|
822
|
+
const session = await loadAnySession();
|
|
823
|
+
if (session?.baseUrl) {
|
|
824
|
+
return normalizeBaseUrl(session.baseUrl);
|
|
825
|
+
}
|
|
826
|
+
return PROD_URL;
|
|
827
|
+
}
|
|
828
|
+
async function createClient(baseUrl, explicitToken) {
|
|
829
|
+
const resolvedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
830
|
+
async function loadStoredSessionOrThrow() {
|
|
831
|
+
const session = await loadSession(resolvedBaseUrl);
|
|
832
|
+
if (!session) {
|
|
833
|
+
throw new Error(`No stored session for ${resolvedBaseUrl}. Run "skillshare login${resolvedBaseUrl === LOCAL_URL ? ' local' : ''}".`);
|
|
834
|
+
}
|
|
835
|
+
return session;
|
|
836
|
+
}
|
|
837
|
+
async function refreshStoredSession(session) {
|
|
838
|
+
const currentSession = session ?? await loadStoredSessionOrThrow();
|
|
839
|
+
if (new Date(currentSession.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
840
|
+
throw new Error(`Stored session for ${resolvedBaseUrl} is no longer valid. Run "skillshare login${resolvedBaseUrl === LOCAL_URL ? ' local' : ''}" again.`);
|
|
841
|
+
}
|
|
842
|
+
const response = await fetch(`${resolvedBaseUrl}/api/cli/refresh`, {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: {
|
|
845
|
+
'Content-Type': 'application/json',
|
|
846
|
+
Accept: 'application/json',
|
|
847
|
+
},
|
|
848
|
+
body: JSON.stringify({
|
|
849
|
+
refreshToken: currentSession.refreshToken,
|
|
850
|
+
}),
|
|
851
|
+
});
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
throw new Error(`Stored session for ${resolvedBaseUrl} is no longer valid. Run "skillshare login${resolvedBaseUrl === LOCAL_URL ? ' local' : ''}" again.`);
|
|
854
|
+
}
|
|
855
|
+
const refreshed = await response.json();
|
|
856
|
+
const nextSession = {
|
|
857
|
+
...currentSession,
|
|
858
|
+
accessToken: refreshed.accessToken,
|
|
859
|
+
accessTokenExpiresAt: refreshed.accessTokenExpiresAt,
|
|
860
|
+
refreshToken: refreshed.refreshToken,
|
|
861
|
+
refreshTokenExpiresAt: refreshed.refreshTokenExpiresAt,
|
|
862
|
+
user: refreshed.user,
|
|
863
|
+
};
|
|
864
|
+
await storeSession(nextSession);
|
|
865
|
+
return nextSession;
|
|
866
|
+
}
|
|
867
|
+
async function getAccessToken(options) {
|
|
868
|
+
if (explicitToken) {
|
|
869
|
+
return explicitToken;
|
|
870
|
+
}
|
|
871
|
+
const session = await loadStoredSessionOrThrow();
|
|
872
|
+
if (!options?.forceRefresh && new Date(session.accessTokenExpiresAt).getTime() > Date.now() + 60_000) {
|
|
873
|
+
return session.accessToken;
|
|
874
|
+
}
|
|
875
|
+
const refreshedSession = await refreshStoredSession(session);
|
|
876
|
+
return refreshedSession.accessToken;
|
|
877
|
+
}
|
|
878
|
+
async function sendAuthorized(path, init, retryUnauthorized = true) {
|
|
879
|
+
const response = await fetch(`${resolvedBaseUrl}${path}`, {
|
|
880
|
+
...init,
|
|
881
|
+
headers: await buildHeaders(() => getAccessToken(), init?.headers),
|
|
882
|
+
});
|
|
883
|
+
if (response.status !== 401 || explicitToken || !retryUnauthorized) {
|
|
884
|
+
return response;
|
|
885
|
+
}
|
|
886
|
+
const refreshedToken = await getAccessToken({ forceRefresh: true });
|
|
887
|
+
return fetch(`${resolvedBaseUrl}${path}`, {
|
|
888
|
+
...init,
|
|
889
|
+
headers: await buildHeaders(() => Promise.resolve(refreshedToken), init?.headers),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
async get(path) {
|
|
894
|
+
const response = await sendAuthorized(path);
|
|
895
|
+
return parseJson(response);
|
|
896
|
+
},
|
|
897
|
+
async post(path) {
|
|
898
|
+
const response = await sendAuthorized(path, {
|
|
899
|
+
method: 'POST',
|
|
900
|
+
});
|
|
901
|
+
return parseJson(response);
|
|
902
|
+
},
|
|
903
|
+
async rawGet(path) {
|
|
904
|
+
const response = await sendAuthorized(path);
|
|
905
|
+
if (!response.ok) {
|
|
906
|
+
throw new Error(await formatErrorResponse(response));
|
|
907
|
+
}
|
|
908
|
+
return response;
|
|
909
|
+
},
|
|
910
|
+
async createSkill(input) {
|
|
911
|
+
const formData = new FormData();
|
|
912
|
+
formData.append('metadata', JSON.stringify({
|
|
913
|
+
name: input.name,
|
|
914
|
+
description: input.description,
|
|
915
|
+
version: input.version,
|
|
916
|
+
}));
|
|
917
|
+
for (const file of input.files) {
|
|
918
|
+
formData.append('files', new Blob([toBlobPart(file.content)], { type: file.contentType }), file.relativePath);
|
|
919
|
+
}
|
|
920
|
+
const response = await sendAuthorized('/api/skills', {
|
|
921
|
+
method: 'POST',
|
|
922
|
+
body: formData,
|
|
923
|
+
});
|
|
924
|
+
return parseJson(response);
|
|
925
|
+
},
|
|
926
|
+
async publishVersion(input) {
|
|
927
|
+
const formData = new FormData();
|
|
928
|
+
formData.append('metadata', JSON.stringify({
|
|
929
|
+
version: input.version,
|
|
930
|
+
}));
|
|
931
|
+
for (const file of input.files) {
|
|
932
|
+
formData.append('files', new Blob([toBlobPart(file.content)], { type: file.contentType }), file.relativePath);
|
|
933
|
+
}
|
|
934
|
+
const response = await sendAuthorized(`/api/skills/${input.skillId}/versions`, {
|
|
935
|
+
method: 'POST',
|
|
936
|
+
body: formData,
|
|
937
|
+
});
|
|
938
|
+
return parseJson(response);
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
async function buildHeaders(getAccessToken, initialHeaders) {
|
|
943
|
+
const headers = new Headers(initialHeaders);
|
|
944
|
+
if (!headers.has('Accept')) {
|
|
945
|
+
headers.set('Accept', 'application/json');
|
|
946
|
+
}
|
|
947
|
+
headers.set('Authorization', `Bearer ${await getAccessToken()}`);
|
|
948
|
+
return headers;
|
|
949
|
+
}
|
|
950
|
+
async function parseJson(response) {
|
|
951
|
+
if (!response.ok) {
|
|
952
|
+
throw new Error(await formatErrorResponse(response));
|
|
953
|
+
}
|
|
954
|
+
return response.json();
|
|
955
|
+
}
|
|
956
|
+
async function formatErrorResponse(response) {
|
|
957
|
+
const fallbackMessage = `Request failed with ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`;
|
|
958
|
+
let body = '';
|
|
959
|
+
try {
|
|
960
|
+
body = (await response.text()).trim();
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return fallbackMessage;
|
|
964
|
+
}
|
|
965
|
+
if (!body) {
|
|
966
|
+
return fallbackMessage;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const parsed = JSON.parse(body);
|
|
970
|
+
const message = typeof parsed.error === 'string'
|
|
971
|
+
? parsed.error
|
|
972
|
+
: typeof parsed.message === 'string'
|
|
973
|
+
? parsed.message
|
|
974
|
+
: null;
|
|
975
|
+
if (message) {
|
|
976
|
+
return `${fallbackMessage} ${message}`;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
// Fall back to the raw response body when the server returned non-JSON text.
|
|
981
|
+
}
|
|
982
|
+
return body.length > 300
|
|
983
|
+
? `${fallbackMessage} ${body.slice(0, 297)}...`
|
|
984
|
+
: `${fallbackMessage} ${body}`;
|
|
985
|
+
}
|
|
986
|
+
async function fetchJson(input, init) {
|
|
987
|
+
const response = await fetch(input, init);
|
|
988
|
+
return parseJson(response);
|
|
989
|
+
}
|
|
990
|
+
async function loadSession(baseUrl) {
|
|
991
|
+
try {
|
|
992
|
+
const session = JSON.parse(await readFile(SESSION_PATH, 'utf8'));
|
|
993
|
+
return session.baseUrl === baseUrl ? session : null;
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async function loadAnySession() {
|
|
1000
|
+
try {
|
|
1001
|
+
return JSON.parse(await readFile(SESSION_PATH, 'utf8'));
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function storeSession(session) {
|
|
1008
|
+
await mkdir(APP_DIR, { recursive: true });
|
|
1009
|
+
await writeFile(SESSION_PATH, JSON.stringify(session), {
|
|
1010
|
+
mode: process.platform === 'win32' ? undefined : 0o600,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
async function logoutSession() {
|
|
1014
|
+
try {
|
|
1015
|
+
await rm(SESSION_PATH);
|
|
1016
|
+
console.log('Logged out.');
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
if (error?.code === 'ENOENT') {
|
|
1020
|
+
console.log('No active session.');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async function openBrowser(url) {
|
|
1027
|
+
if (process.platform === 'darwin') {
|
|
1028
|
+
const result = await runProcess('open', [url], { allowFailure: true });
|
|
1029
|
+
if (result.exitCode !== 0) {
|
|
1030
|
+
throw new Error(result.stderr.trim() || 'Failed to open browser.');
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (process.platform === 'win32') {
|
|
1035
|
+
const result = await runProcess('cmd', ['/c', 'start', '', url], { allowFailure: true });
|
|
1036
|
+
if (result.exitCode !== 0) {
|
|
1037
|
+
throw new Error(result.stderr.trim() || 'Failed to open browser.');
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const result = await runProcess('xdg-open', [url], { allowFailure: true });
|
|
1042
|
+
if (result.exitCode !== 0) {
|
|
1043
|
+
throw new Error(result.stderr.trim() || 'Failed to open browser.');
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function waitForLoginCallback() {
|
|
1047
|
+
let resolveStatePromise = () => { };
|
|
1048
|
+
let rejectStatePromise = () => { };
|
|
1049
|
+
const statePromise = new Promise((resolve, reject) => {
|
|
1050
|
+
resolveStatePromise = resolve;
|
|
1051
|
+
rejectStatePromise = reject;
|
|
1052
|
+
});
|
|
1053
|
+
const server = createServer((request, response) => {
|
|
1054
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
1055
|
+
const code = url.searchParams.get('code');
|
|
1056
|
+
const state = url.searchParams.get('state');
|
|
1057
|
+
const error = url.searchParams.get('error');
|
|
1058
|
+
if (error) {
|
|
1059
|
+
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1060
|
+
response.end(`Login failed: ${error}`);
|
|
1061
|
+
rejectStatePromise(new Error(`Login failed: ${error}`));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!code || !state) {
|
|
1065
|
+
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1066
|
+
response.end('Missing code or state.');
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1070
|
+
response.end('Skillshare login completed. You can close this tab.');
|
|
1071
|
+
resolveStatePromise(JSON.stringify({ code, state }));
|
|
1072
|
+
});
|
|
1073
|
+
await new Promise((resolve, reject) => {
|
|
1074
|
+
server.once('error', reject);
|
|
1075
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
1076
|
+
});
|
|
1077
|
+
const address = server.address();
|
|
1078
|
+
if (!address || typeof address === 'string') {
|
|
1079
|
+
throw new Error('Failed to bind local callback server.');
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
1083
|
+
async wait(expectedState) {
|
|
1084
|
+
try {
|
|
1085
|
+
const payload = JSON.parse(await statePromise);
|
|
1086
|
+
if (payload.state !== expectedState) {
|
|
1087
|
+
throw new Error('Login state mismatch.');
|
|
1088
|
+
}
|
|
1089
|
+
return payload.code;
|
|
1090
|
+
}
|
|
1091
|
+
finally {
|
|
1092
|
+
server.close();
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
async function readLocalSkillMetadata(directory) {
|
|
1098
|
+
const metadataPath = join(resolve(directory), SKILL_METADATA_FILE_NAME);
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
1101
|
+
const skillId = Number(parsed.skillId);
|
|
1102
|
+
if (!Number.isInteger(skillId) || skillId <= 0) {
|
|
1103
|
+
throw new Error(`Invalid skillId in ${SKILL_METADATA_FILE_NAME}.`);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
skillId,
|
|
1107
|
+
skillName: typeof parsed.skillName === 'string' ? parsed.skillName : '',
|
|
1108
|
+
skillDescription: typeof parsed.skillDescription === 'string' ? parsed.skillDescription : '',
|
|
1109
|
+
skillVersion: typeof parsed.skillVersion === 'string' ? parsed.skillVersion : '',
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
if (error?.code === 'ENOENT') {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
throw new Error(`Failed to read ${metadataPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function writeLocalSkillMetadata(directory, metadata) {
|
|
1120
|
+
await mkdir(directory, { recursive: true });
|
|
1121
|
+
await writeFile(join(directory, SKILL_METADATA_FILE_NAME), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
1122
|
+
}
|
|
1123
|
+
async function collectFilePlans(root, current = root) {
|
|
1124
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
1125
|
+
const files = [];
|
|
1126
|
+
for (const entry of entries) {
|
|
1127
|
+
const entryPath = join(current, entry.name);
|
|
1128
|
+
if (entry.isDirectory()) {
|
|
1129
|
+
files.push(...(await collectFilePlans(root, entryPath)));
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const entryStat = await stat(entryPath);
|
|
1133
|
+
if (!entryStat.isFile()) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const relativePath = relative(root, entryPath).replaceAll('\\', '/');
|
|
1137
|
+
if (relativePath === SKILL_METADATA_FILE_NAME) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
files.push({
|
|
1141
|
+
absolutePath: entryPath,
|
|
1142
|
+
relativePath,
|
|
1143
|
+
size: entryStat.size,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
1147
|
+
}
|
|
1148
|
+
async function collectFiles(filePlans, onFileRead) {
|
|
1149
|
+
const files = [];
|
|
1150
|
+
for (let index = 0; index < filePlans.length; index += 1) {
|
|
1151
|
+
const filePlan = filePlans[index];
|
|
1152
|
+
if (!filePlan) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
files.push({
|
|
1156
|
+
relativePath: filePlan.relativePath,
|
|
1157
|
+
content: await readFile(filePlan.absolutePath),
|
|
1158
|
+
contentType: guessContentType(filePlan.relativePath),
|
|
1159
|
+
});
|
|
1160
|
+
onFileRead?.(index + 1, filePlans.length, filePlan.relativePath);
|
|
1161
|
+
}
|
|
1162
|
+
return files;
|
|
1163
|
+
}
|
|
1164
|
+
function validateSkillDirectory(files) {
|
|
1165
|
+
if (files.length === 0) {
|
|
1166
|
+
throw new Error('The skill directory is empty.');
|
|
1167
|
+
}
|
|
1168
|
+
if (!files.some((file) => file.relativePath === SKILL_FILE_NAME)) {
|
|
1169
|
+
throw new Error(`The skill directory must contain ${SKILL_FILE_NAME}.`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function guessContentType(path) {
|
|
1173
|
+
if (path.endsWith('.md'))
|
|
1174
|
+
return 'text/markdown; charset=utf-8';
|
|
1175
|
+
if (path.endsWith('.json'))
|
|
1176
|
+
return 'application/json; charset=utf-8';
|
|
1177
|
+
if (path.endsWith('.yaml') || path.endsWith('.yml'))
|
|
1178
|
+
return 'application/yaml; charset=utf-8';
|
|
1179
|
+
if (path.endsWith('.txt'))
|
|
1180
|
+
return 'text/plain; charset=utf-8';
|
|
1181
|
+
return 'application/octet-stream';
|
|
1182
|
+
}
|
|
1183
|
+
function formatByteSize(bytes) {
|
|
1184
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
1185
|
+
return '0 B';
|
|
1186
|
+
}
|
|
1187
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1188
|
+
let value = bytes;
|
|
1189
|
+
let unitIndex = 0;
|
|
1190
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
1191
|
+
value /= 1024;
|
|
1192
|
+
unitIndex += 1;
|
|
1193
|
+
}
|
|
1194
|
+
const decimals = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
1195
|
+
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
1196
|
+
}
|
|
1197
|
+
function normalizeBaseUrl(url) {
|
|
1198
|
+
return url.replace(/\/+$/, '');
|
|
1199
|
+
}
|
|
1200
|
+
function cryptoRandomHex(bytes) {
|
|
1201
|
+
return randomBytes(bytes).toString('hex');
|
|
1202
|
+
}
|
|
1203
|
+
function createPkceVerifier() {
|
|
1204
|
+
return toBase64Url(randomBytes(32));
|
|
1205
|
+
}
|
|
1206
|
+
function createPkceChallenge(verifier) {
|
|
1207
|
+
return toBase64Url(createHash('sha256').update(verifier, 'ascii').digest());
|
|
1208
|
+
}
|
|
1209
|
+
function toBase64Url(bytes) {
|
|
1210
|
+
return Buffer.from(bytes)
|
|
1211
|
+
.toString('base64')
|
|
1212
|
+
.replaceAll('+', '-')
|
|
1213
|
+
.replaceAll('/', '_')
|
|
1214
|
+
.replaceAll('=', '');
|
|
1215
|
+
}
|
|
1216
|
+
function toBlobPart(buffer) {
|
|
1217
|
+
return Uint8Array.from(buffer);
|
|
1218
|
+
}
|
|
1219
|
+
function getAppDir() {
|
|
1220
|
+
if (process.platform === 'win32') {
|
|
1221
|
+
return join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'skillshare');
|
|
1222
|
+
}
|
|
1223
|
+
return join(homedir(), '.config', 'skillshare');
|
|
1224
|
+
}
|
|
1225
|
+
async function runProcess(command, args, options) {
|
|
1226
|
+
const child = spawn(command, args, {
|
|
1227
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1228
|
+
});
|
|
1229
|
+
let stdout = '';
|
|
1230
|
+
let stderr = '';
|
|
1231
|
+
child.stdout.on('data', (chunk) => {
|
|
1232
|
+
stdout += chunk.toString();
|
|
1233
|
+
});
|
|
1234
|
+
child.stderr.on('data', (chunk) => {
|
|
1235
|
+
stderr += chunk.toString();
|
|
1236
|
+
});
|
|
1237
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
1238
|
+
child.once('error', reject);
|
|
1239
|
+
child.once('close', (code) => resolve(code ?? 1));
|
|
1240
|
+
});
|
|
1241
|
+
if (exitCode !== 0 && !options?.allowFailure) {
|
|
1242
|
+
throw new Error(stderr.trim() || `${command} failed with exit code ${exitCode}.`);
|
|
1243
|
+
}
|
|
1244
|
+
return { exitCode, stdout, stderr };
|
|
1245
|
+
}
|
|
1246
|
+
main().catch((error) => {
|
|
1247
|
+
process.exitCode = 1;
|
|
1248
|
+
const message = error instanceof Error
|
|
1249
|
+
? error.message.trim() || 'Unexpected error.'
|
|
1250
|
+
: String(error).trim() || 'Unexpected error.';
|
|
1251
|
+
process.stderr.write(`${message}\n`);
|
|
1252
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quadient/skillshare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skillshare CLI for installing and publishing Quadient skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillshare": "./bin/skillshare"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsx index.ts",
|
|
16
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
17
|
+
"start": "node ./dist/index.js",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.6.0",
|
|
25
|
+
"tsx": "^4.20.5",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.2.0"
|
|
33
|
+
}
|
|
34
|
+
}
|