myskillshub 1.0.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myskillshub",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "CLI tool for SkillHub skill management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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.text = 'Creating ZIP package...';
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.text = 'Uploading to SkillHub API...';
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('name', skillData.name);
475
+ formData.append('title', skillData.name);
160
476
  formData.append('description', skillData.description);
161
- formData.append('version', skillData.version);
162
- formData.append('author', skillData.author);
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
- const response = await axios.post(`${apiUrl}/api/skills/publish`, formData, {
167
- headers: {
168
- ...formData.getHeaders()
169
- }
170
- });
171
-
172
- // Clean up ZIP file after successful upload
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
- return response.data;
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;