myskillshub 1.0.4 → 1.0.6
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/package.json +1 -1
- package/src/commands/publish.js +337 -19
package/package.json
CHANGED
package/src/commands/publish.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const axios = require('axios');
|
|
7
|
+
const readline = require('readline');
|
|
6
8
|
const oraModule = require('ora');
|
|
7
9
|
const ora = oraModule.default || oraModule;
|
|
8
10
|
const archiver = require('archiver');
|
|
@@ -17,6 +19,10 @@ program
|
|
|
17
19
|
.argument('[directory]', 'Directory containing skill to publish (default: current directory)')
|
|
18
20
|
.option('--verbose', 'Verbose output')
|
|
19
21
|
.option('-a, --api-url <url>', 'SkillHub API URL', 'http://localhost:3000')
|
|
22
|
+
.option('--username <username>', 'Login username (or use SKILLSHUB_USERNAME)')
|
|
23
|
+
.option('--password <password>', 'Login password (or use SKILLSHUB_PASSWORD)')
|
|
24
|
+
.option('--category-id <id>', 'Category ID for publish')
|
|
25
|
+
.option('--category-slug <slug>', 'Category slug for publish (auto resolve to ID)')
|
|
20
26
|
.action(async (directory, options) => {
|
|
21
27
|
const targetDirectory = directory || '.';
|
|
22
28
|
const spinner = ora('Starting publish process...').start();
|
|
@@ -40,16 +46,27 @@ program
|
|
|
40
46
|
|
|
41
47
|
// Parse skill.md content (basic parsing)
|
|
42
48
|
const skillData = parseSkillMd(skillMdContent);
|
|
49
|
+
const frontmatter = parseFrontmatter(skillMdContent);
|
|
50
|
+
skillData.description = normalizeDescription(frontmatter.description || skillData.description, skillData.name);
|
|
51
|
+
skillData.tags = normalizeTags(frontmatter.tags || []);
|
|
43
52
|
spinner.succeed(chalk.green(`Found skill: ${skillData.name}`));
|
|
44
53
|
|
|
54
|
+
spinner.start('Resolving category...');
|
|
55
|
+
const categoryId = await resolveCategoryId(options.apiUrl, options, frontmatter);
|
|
56
|
+
spinner.succeed(chalk.green(`Resolved category_id: ${categoryId}`));
|
|
57
|
+
|
|
58
|
+
spinner.start('Authenticating...');
|
|
59
|
+
const auth = await getAuthCookie(options.apiUrl, options, spinner);
|
|
60
|
+
spinner.succeed(chalk.green(auth.fromCache ? 'Authenticated with cached session' : 'Login succeeded'));
|
|
61
|
+
|
|
45
62
|
// Create ZIP package
|
|
46
|
-
spinner.
|
|
63
|
+
spinner.start('Creating ZIP package...');
|
|
47
64
|
const zipPath = await createZipPackage(targetDirectory);
|
|
48
65
|
spinner.succeed(chalk.green(`ZIP package created: ${zipPath}`));
|
|
49
66
|
|
|
50
67
|
// Upload to SkillHub API
|
|
51
|
-
spinner.
|
|
52
|
-
const result = await uploadSkill(zipPath, skillData, options.apiUrl);
|
|
68
|
+
spinner.start('Uploading to SkillHub API...');
|
|
69
|
+
const result = await uploadSkill(zipPath, skillData, categoryId, options.apiUrl, auth.cookie);
|
|
53
70
|
|
|
54
71
|
spinner.succeed(chalk.green('Skill published successfully!'));
|
|
55
72
|
console.log(chalk.cyan(`Skill slug: ${result.slug}`));
|
|
@@ -75,6 +92,68 @@ function resolveSkillFilePath(directory) {
|
|
|
75
92
|
return null;
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
function parseFrontmatter(content) {
|
|
96
|
+
const text = String(content || '');
|
|
97
|
+
if (!text.startsWith('---')) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const end = text.indexOf('---', 3);
|
|
102
|
+
if (end < 0) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const block = text.slice(3, end).trim();
|
|
107
|
+
const lines = block.split('\n');
|
|
108
|
+
const result = {};
|
|
109
|
+
let currentArrayKey = null;
|
|
110
|
+
|
|
111
|
+
for (const rawLine of lines) {
|
|
112
|
+
const line = rawLine.trim();
|
|
113
|
+
if (!line || line.startsWith('#')) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (line.startsWith('- ')) {
|
|
118
|
+
if (currentArrayKey) {
|
|
119
|
+
if (!Array.isArray(result[currentArrayKey])) {
|
|
120
|
+
result[currentArrayKey] = [];
|
|
121
|
+
}
|
|
122
|
+
result[currentArrayKey].push(stripQuotes(line.slice(2).trim()));
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
currentArrayKey = null;
|
|
128
|
+
const idx = line.indexOf(':');
|
|
129
|
+
if (idx < 0) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const key = line.slice(0, idx).trim();
|
|
134
|
+
const value = line.slice(idx + 1).trim();
|
|
135
|
+
if (!value) {
|
|
136
|
+
currentArrayKey = key;
|
|
137
|
+
result[key] = [];
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
142
|
+
const arr = value.slice(1, -1).trim();
|
|
143
|
+
result[key] = arr ? arr.split(',').map((item) => stripQuotes(item.trim())) : [];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result[key] = stripQuotes(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripQuotes(value) {
|
|
154
|
+
return String(value || '').replace(/^['"]|['"]$/g, '');
|
|
155
|
+
}
|
|
156
|
+
|
|
78
157
|
function parseSkillMd(content) {
|
|
79
158
|
const lines = content.split('\n');
|
|
80
159
|
const skillData = {
|
|
@@ -83,7 +162,8 @@ function parseSkillMd(content) {
|
|
|
83
162
|
version: '1.0.0',
|
|
84
163
|
author: '',
|
|
85
164
|
keywords: [],
|
|
86
|
-
installCommand: ''
|
|
165
|
+
installCommand: '',
|
|
166
|
+
tags: []
|
|
87
167
|
};
|
|
88
168
|
|
|
89
169
|
let inDescription = false;
|
|
@@ -116,6 +196,242 @@ function parseSkillMd(content) {
|
|
|
116
196
|
return skillData;
|
|
117
197
|
}
|
|
118
198
|
|
|
199
|
+
function normalizeDescription(description, skillName) {
|
|
200
|
+
const text = String(description || '').trim();
|
|
201
|
+
if (text.length >= 10 && text.length <= 500) {
|
|
202
|
+
return text;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fallback = `Skill "${skillName || 'unknown'}" published via myskillshub CLI.`;
|
|
206
|
+
return fallback.slice(0, 500);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeTags(tags) {
|
|
210
|
+
if (!Array.isArray(tags)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return tags.map((item) => String(item || '').trim()).filter(Boolean).slice(0, 10);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function resolveCategoryId(apiUrl, options, frontmatter) {
|
|
217
|
+
if (options.categoryId) {
|
|
218
|
+
const id = parseInt(options.categoryId, 10);
|
|
219
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
220
|
+
throw new Error(`Invalid --category-id: ${options.categoryId}`);
|
|
221
|
+
}
|
|
222
|
+
return id;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (frontmatter.category_id) {
|
|
226
|
+
const id = parseInt(frontmatter.category_id, 10);
|
|
227
|
+
if (Number.isInteger(id) && id > 0) {
|
|
228
|
+
return id;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const slugCandidate = options.categorySlug || frontmatter.category_slug || frontmatter.category;
|
|
233
|
+
if (!slugCandidate) {
|
|
234
|
+
throw new Error('category_id is required. Use --category-id <id> or --category-slug <slug>.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const response = await axios.get(`${apiUrl}/api/categories/${encodeURIComponent(String(slugCandidate).trim())}`);
|
|
238
|
+
const category = response.data?.data?.category;
|
|
239
|
+
if (!response.data?.success || !category?.id) {
|
|
240
|
+
throw new Error(`Failed to resolve category slug: ${slugCandidate}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return category.id;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function getAuthCookie(apiUrl, options, spinner) {
|
|
247
|
+
const normalizedApiUrl = normalizeApiUrl(apiUrl);
|
|
248
|
+
const resolvedUsername = options.username || process.env.SKILLSHUB_USERNAME || '';
|
|
249
|
+
const resolvedPassword = options.password || process.env.SKILLSHUB_PASSWORD || '';
|
|
250
|
+
const hasCompleteExplicitCredentials = Boolean(resolvedUsername && resolvedPassword);
|
|
251
|
+
|
|
252
|
+
if (!hasCompleteExplicitCredentials) {
|
|
253
|
+
const cachedCookie = getCachedCookie(normalizedApiUrl);
|
|
254
|
+
if (cachedCookie) {
|
|
255
|
+
const isValid = await isSessionValid(normalizedApiUrl, cachedCookie);
|
|
256
|
+
if (isValid) {
|
|
257
|
+
return { cookie: cachedCookie, fromCache: true };
|
|
258
|
+
}
|
|
259
|
+
removeCachedCookie(normalizedApiUrl);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const credentials = await resolveCredentials(options, spinner);
|
|
264
|
+
const cookie = await loginAndGetCookie(normalizedApiUrl, credentials.username, credentials.password);
|
|
265
|
+
saveCachedCookie(normalizedApiUrl, cookie);
|
|
266
|
+
return { cookie, fromCache: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeApiUrl(apiUrl) {
|
|
270
|
+
return String(apiUrl || '').trim().replace(/\/+$/, '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getSessionStorePath() {
|
|
274
|
+
return path.join(os.homedir(), '.myskillshub', 'session.json');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readSessionStore() {
|
|
278
|
+
const sessionStorePath = getSessionStorePath();
|
|
279
|
+
if (!fs.existsSync(sessionStorePath)) {
|
|
280
|
+
return { sessions: {} };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const raw = fs.readFileSync(sessionStorePath, 'utf8');
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
if (parsed && typeof parsed === 'object' && parsed.sessions && typeof parsed.sessions === 'object') {
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
return { sessions: {} };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return { sessions: {} };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeSessionStore(data) {
|
|
296
|
+
const sessionStorePath = getSessionStorePath();
|
|
297
|
+
const sessionStoreDir = path.dirname(sessionStorePath);
|
|
298
|
+
fs.mkdirSync(sessionStoreDir, { recursive: true, mode: 0o700 });
|
|
299
|
+
fs.writeFileSync(sessionStorePath, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getCachedCookie(apiUrl) {
|
|
303
|
+
const store = readSessionStore();
|
|
304
|
+
return store.sessions?.[apiUrl]?.cookie || null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function saveCachedCookie(apiUrl, cookie) {
|
|
308
|
+
const store = readSessionStore();
|
|
309
|
+
store.sessions = store.sessions || {};
|
|
310
|
+
store.sessions[apiUrl] = {
|
|
311
|
+
cookie,
|
|
312
|
+
updatedAt: new Date().toISOString()
|
|
313
|
+
};
|
|
314
|
+
writeSessionStore(store);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function removeCachedCookie(apiUrl) {
|
|
318
|
+
const store = readSessionStore();
|
|
319
|
+
if (store.sessions && store.sessions[apiUrl]) {
|
|
320
|
+
delete store.sessions[apiUrl];
|
|
321
|
+
writeSessionStore(store);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function isSessionValid(apiUrl, cookie) {
|
|
326
|
+
try {
|
|
327
|
+
const response = await axios.get(`${apiUrl}/api/auth/me`, {
|
|
328
|
+
headers: {
|
|
329
|
+
Cookie: cookie
|
|
330
|
+
},
|
|
331
|
+
validateStatus: () => true
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return response.status === 200 && Boolean(response.data?.success);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function resolveCredentials(options, spinner) {
|
|
341
|
+
let username = options.username || process.env.SKILLSHUB_USERNAME || '';
|
|
342
|
+
let password = options.password || process.env.SKILLSHUB_PASSWORD || '';
|
|
343
|
+
const needsPrompt = !username || !password;
|
|
344
|
+
|
|
345
|
+
if (needsPrompt && !process.stdin.isTTY) {
|
|
346
|
+
throw new Error('Missing credentials in non-interactive mode. Use --username/--password or SKILLSHUB_USERNAME/SKILLSHUB_PASSWORD.');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!username) {
|
|
350
|
+
if (spinner && spinner.isSpinning) {
|
|
351
|
+
spinner.stop();
|
|
352
|
+
}
|
|
353
|
+
username = await promptInput('SkillHub username: ');
|
|
354
|
+
if (spinner && !spinner.isSpinning) {
|
|
355
|
+
spinner.start('Logging in...');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!password) {
|
|
360
|
+
if (spinner && spinner.isSpinning) {
|
|
361
|
+
spinner.stop();
|
|
362
|
+
}
|
|
363
|
+
password = await promptPassword('SkillHub password: ');
|
|
364
|
+
if (spinner && !spinner.isSpinning) {
|
|
365
|
+
spinner.start('Logging in...');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!username || !password) {
|
|
370
|
+
throw new Error('Publish requires username and password.');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { username, password };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function promptInput(question) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const rl = readline.createInterface({
|
|
379
|
+
input: process.stdin,
|
|
380
|
+
output: process.stdout,
|
|
381
|
+
terminal: true
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
rl.question(question, (answer) => {
|
|
385
|
+
rl.close();
|
|
386
|
+
resolve(String(answer || '').trim());
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function promptPassword(question) {
|
|
392
|
+
return new Promise((resolve) => {
|
|
393
|
+
const rl = readline.createInterface({
|
|
394
|
+
input: process.stdin,
|
|
395
|
+
output: process.stdout,
|
|
396
|
+
terminal: true
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
rl.stdoutMuted = true;
|
|
400
|
+
const originalWriteToOutput = rl._writeToOutput;
|
|
401
|
+
rl._writeToOutput = function writeToOutput(stringToWrite) {
|
|
402
|
+
if (rl.stdoutMuted) {
|
|
403
|
+
if (stringToWrite === '\n' || stringToWrite === '\r\n') {
|
|
404
|
+
rl.output.write(stringToWrite);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
rl.output.write('*');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
originalWriteToOutput.call(rl, stringToWrite);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
rl.question(question, (password) => {
|
|
414
|
+
rl.history = [];
|
|
415
|
+
rl.close();
|
|
416
|
+
resolve(String(password || '').trim());
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function loginAndGetCookie(apiUrl, username, password) {
|
|
422
|
+
const response = await axios.post(`${apiUrl}/api/auth/login`, {
|
|
423
|
+
username,
|
|
424
|
+
password
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const setCookie = response.headers['set-cookie'];
|
|
428
|
+
if (!Array.isArray(setCookie) || setCookie.length === 0) {
|
|
429
|
+
throw new Error('Login succeeded but no session cookie returned');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return setCookie.map((cookie) => cookie.split(';')[0]).join('; ');
|
|
433
|
+
}
|
|
434
|
+
|
|
119
435
|
function createZipPackage(directory) {
|
|
120
436
|
return new Promise((resolve, reject) => {
|
|
121
437
|
const baseName = path.basename(path.resolve(directory));
|
|
@@ -153,26 +469,28 @@ function createZipPackage(directory) {
|
|
|
153
469
|
});
|
|
154
470
|
}
|
|
155
471
|
|
|
156
|
-
async function uploadSkill(zipPath, skillData, apiUrl) {
|
|
472
|
+
async function uploadSkill(zipPath, skillData, categoryId, apiUrl, authCookie) {
|
|
157
473
|
const formData = new FormData();
|
|
158
474
|
formData.append('file', fs.createReadStream(zipPath));
|
|
159
|
-
formData.append('
|
|
475
|
+
formData.append('title', skillData.name);
|
|
160
476
|
formData.append('description', skillData.description);
|
|
161
|
-
formData.append('
|
|
162
|
-
formData.append('
|
|
163
|
-
formData.append('keywords', JSON.stringify(skillData.keywords));
|
|
164
|
-
formData.append('installCommand', skillData.installCommand);
|
|
477
|
+
formData.append('category_id', String(categoryId));
|
|
478
|
+
formData.append('tags', JSON.stringify(skillData.tags || []));
|
|
165
479
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
fs.unlinkSync(zipPath);
|
|
480
|
+
try {
|
|
481
|
+
const response = await axios.post(`${apiUrl}/api/skills/publish`, formData, {
|
|
482
|
+
headers: {
|
|
483
|
+
...formData.getHeaders(),
|
|
484
|
+
Cookie: authCookie
|
|
485
|
+
}
|
|
486
|
+
});
|
|
174
487
|
|
|
175
|
-
|
|
488
|
+
return response.data?.data || response.data;
|
|
489
|
+
} finally {
|
|
490
|
+
if (fs.existsSync(zipPath)) {
|
|
491
|
+
fs.unlinkSync(zipPath);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
176
494
|
}
|
|
177
495
|
|
|
178
496
|
module.exports = program;
|